# Gemini + MCP 데모 스크립트 v9: 한계점 및 개선 방향 ## 현재 구현의 한계점 실제로 구현하면서 발견한 문제점과 한계를 솔직하게 정리한다. ### 1. 수동 Schema 변환 **문제**: ```typescript // MCP tool schema → Gemini function declaration 변환이 수동 static mcpToolToGeminiFunction(tool: MCPTool): FunctionDeclaration { return { name: tool.name, description: tool.description, parametersJsonSchema: { type: tool.inputSchema.type || 'object', properties: tool.inputSchema.properties || {}, required: tool.inputSchema.required || [] } }; } ``` **한계**: - JSON Schema 복잡한 기능 미지원 (allOf, anyOf, $ref 등) - Nested object 처리 불완전 - 타입 변환 오류 가능성 **개선 방향**: ```typescript // JSON Schema validator 라이브러리 사용 import Ajv from 'ajv'; class ImprovedSchemaConverter { private ajv = new Ajv(); convertWithValidation(tool: MCPTool): FunctionDeclaration { // Schema 유효성 검증 const valid = this.ajv.validateSchema(tool.inputSchema); if (!valid) { throw new Error(`Invalid schema for ${tool.name}`); } // $ref 해결, allOf/anyOf 처리 등 const resolved = this.resolveRefs(tool.inputSchema); return { name: tool.name, description: tool.description, parametersJsonSchema: resolved }; } } ``` ### 2. 에러 핸들링 부족 **문제**: ```typescript // 현재: 간단한 에러 처리만 if (data.error) { throw new Error(`MCP tool error: ${data.error.message}`); } ``` **한계**: - 재시도 로직 없음 - 부분 실패 처리 안 됨 - 타임아웃 미설정 - Circuit breaker 패턴 미적용 **개선 방향**: ```typescript class RobustMCPAdapter { private retryConfig = { maxRetries: 3, baseDelay: 1000, maxDelay: 10000 }; async callToolWithRetry( name: string, args: Record ): Promise { let lastError: Error | null = null; for (let i = 0; i < this.retryConfig.maxRetries; i++) { try { return await this.callToolWithTimeout(name, args, 30000); } catch (error: any) { lastError = error; // 재시도 불가능한 에러는 즉시 throw if (error.message.includes('400') || error.message.includes('401')) { throw error; } // 지수 백오프 const delay = Math.min( this.retryConfig.baseDelay * Math.pow(2, i), this.retryConfig.maxDelay ); console.log(`Retry ${i + 1}/${this.retryConfig.maxRetries} after ${delay}ms`); await new Promise(resolve => setTimeout(resolve, delay)); } } throw lastError || new Error('Unknown error'); } async callToolWithTimeout( name: string, args: Record, timeout: number ): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(this.endpoint, { signal: controller.signal, // ... other options }); return await this.parseResponse(response); } finally { clearTimeout(timeoutId); } } } ``` ### 3. 대화 컨텍스트 관리 미흡 **문제**: ```typescript // 현재: 단순 배열로 관리 private chatHistory: any[] = []; ``` **한계**: - 컨텍스트 길이 제한 미고려 - 토큰 수 계산 안 함 - 오래된 메시지 정리 안 됨 - 메모리 누수 가능성 **개선 방향**: ```typescript class ContextManager { private maxTokens = 100000; // Gemini 컨텍스트 제한 private history: Message[] = []; addMessage(message: Message) { this.history.push(message); this.trimHistory(); } private trimHistory() { let totalTokens = this.estimateTokens(this.history); // 토큰 제한 초과 시 오래된 메시지부터 제거 while (totalTokens > this.maxTokens && this.history.length > 1) { // 시스템 메시지는 보존 const removed = this.history.find(m => m.role !== 'system'); if (!removed) break; this.history.splice(this.history.indexOf(removed), 1); totalTokens = this.estimateTokens(this.history); } } private estimateTokens(messages: Message[]): number { // 간단한 휴리스틱 (실제로는 tiktoken 같은 라이브러리 사용) return messages.reduce((sum, msg) => { const text = JSON.stringify(msg); return sum + Math.ceil(text.length / 4); }, 0); } getHistory(maxMessages?: number): Message[] { if (maxMessages) { return this.history.slice(-maxMessages); } return this.history; } clear() { // 시스템 메시지만 유지 this.history = this.history.filter(m => m.role === 'system'); } } ``` ### 4. 멀티 MCP Server 미지원 **문제**: ```typescript // 현재: 하나의 MCP server만 지원 constructor( geminiApiKey: string, mcpEndpoint: string, // 단일 endpoint mcpToken?: string ) ``` **한계**: - 여러 MCP server 동시 사용 불가 - Tool 이름 충돌 가능성 - 유연성 부족 **개선 방향**: ```typescript interface MCPServerConfig { name: string; endpoint: string; token?: string; priority?: number; // Tool 충돌 시 우선순위 } class MultiMCPClient { private servers: Map = new Map(); async addServer(config: MCPServerConfig) { const adapter = new MCPAdapter(config.endpoint, config.token); this.servers.set(config.name, adapter); } async getAllTools(): Promise { const allTools: FunctionDeclaration[] = []; const toolNames = new Set(); for (const [serverName, adapter] of this.servers) { const tools = await adapter.getTools(); for (const tool of tools) { // 이름 충돌 방지: server prefix 추가 const prefixedName = `${serverName}_${tool.name}`; if (toolNames.has(prefixedName)) { console.warn(`Tool ${prefixedName} already exists, skipping`); continue; } toolNames.add(prefixedName); allTools.push( SchemaConverter.mcpToolToGeminiFunction({ ...tool, name: prefixedName }) ); } } return allTools; } async callTool(toolName: string, args: Record): Promise { // server_toolname 형식 파싱 const [serverName, ...nameParts] = toolName.split('_'); const actualToolName = nameParts.join('_'); const adapter = this.servers.get(serverName); if (!adapter) { throw new Error(`Unknown server: ${serverName}`); } return await adapter.callTool(actualToolName, args); } } ``` ### 5. 캐싱 전략 부재 **문제**: - MCP tools 목록을 매번 새로 가져옴 - 동일한 tool 호출 결과 재사용 안 함 - 불필요한 네트워크 요청 **개선 방향**: ```typescript class CachedMCPAdapter extends MCPAdapter { private toolsCache: { data: MCPTool[] | null; timestamp: number; ttl: number; } = { data: null, timestamp: 0, ttl: 3600000 }; // 1시간 private resultCache = new Map(); async getTools(): Promise { const now = Date.now(); // 캐시 유효성 확인 if (this.toolsCache.data && (now - this.toolsCache.timestamp) < this.toolsCache.ttl) { return this.toolsCache.data; } // 캐시 갱신 const tools = await super.getTools(); this.toolsCache = { data: tools, timestamp: now, ttl: this.toolsCache.ttl }; return tools; } async callTool( name: string, args: Record ): Promise { // Idempotent한 tool만 캐싱 (GET 성격의 도구) const cacheableTools = ['search_docs', 'get_project', 'list_projects']; if (cacheableTools.includes(name)) { const cacheKey = `${name}:${JSON.stringify(args)}`; const cached = this.resultCache.get(cacheKey); if (cached && (Date.now() - cached.timestamp) < 60000) { // 1분 return cached.result; } const result = await super.callTool(name, args); this.resultCache.set(cacheKey, { result, timestamp: Date.now() }); return result; } return await super.callTool(name, args); } } ``` ### 6. 타입 안전성 개선 필요 **문제**: ```typescript // 현재: any 타입 남발 private chatHistory: any[] = []; async callTool(name: string, args: Record): Promise ``` **개선 방향**: ```typescript // 엄격한 타입 정의 interface Message { role: 'user' | 'assistant' | 'system'; parts: Part[]; } interface Part { text?: string; functionCall?: FunctionCall; functionResponse?: FunctionResponse; } interface FunctionCall { name: string; args: Record; } interface FunctionResponse { name: string; response: { result?: string; error?: string; }; } class TypeSafeGeminiClient { private chatHistory: Message[] = []; async callTool( name: string, args: Record ): Promise { // ... implementation with proper type checking } } ``` ## 추가 기능 제안 ### 1. Streaming 지원 ```typescript class StreamingMCPClient { async *chatStream(message: string): AsyncGenerator { const chat = this.model.startChat({ history: this.chatHistory }); const result = await chat.sendMessageStream(message); for await (const chunk of result.stream) { const text = chunk.text(); if (text) { yield text; } // Function call 처리 if (chunk.functionCalls) { const results = await this.handleFunctionCalls(chunk.functionCalls); const followUp = await chat.sendMessageStream(results); for await (const followUpChunk of followUp.stream) { yield followUpChunk.text(); } } } } } ``` ### 2. 로깅 및 모니터링 ```typescript interface LogEntry { timestamp: number; type: 'tool_call' | 'error' | 'response'; data: any; } class MonitoredMCPClient { private logs: LogEntry[] = []; async callTool(name: string, args: Record): Promise { const startTime = Date.now(); try { const result = await super.callTool(name, args); this.log({ timestamp: Date.now(), type: 'tool_call', data: { name, args, result: result.substring(0, 100), // 일부만 duration: Date.now() - startTime } }); return result; } catch (error: any) { this.log({ timestamp: Date.now(), type: 'error', data: { name, args, error: error.message, duration: Date.now() - startTime } }); throw error; } } getLogs(filter?: { type?: string; since?: number }): LogEntry[] { let filtered = this.logs; if (filter?.type) { filtered = filtered.filter(log => log.type === filter.type); } if (filter?.since) { filtered = filtered.filter(log => log.timestamp >= filter.since); } return filtered; } exportMetrics() { const metrics = { totalCalls: this.logs.filter(l => l.type === 'tool_call').length, errors: this.logs.filter(l => l.type === 'error').length, avgDuration: 0, toolUsage: {} as Record }; const toolCalls = this.logs.filter(l => l.type === 'tool_call'); if (toolCalls.length > 0) { metrics.avgDuration = toolCalls.reduce( (sum, log) => sum + log.data.duration, 0 ) / toolCalls.length; toolCalls.forEach(log => { const name = log.data.name; metrics.toolUsage[name] = (metrics.toolUsage[name] || 0) + 1; }); } return metrics; } } ``` ### 3. Tool 권한 관리 ```typescript interface ToolPermission { toolName: string; allowed: boolean; requiresConfirmation?: boolean; } class SecureMCPClient { private permissions: Map = new Map(); setPermission(toolName: string, permission: ToolPermission) { this.permissions.set(toolName, permission); } async callTool(name: string, args: Record): Promise { const permission = this.permissions.get(name); if (permission && !permission.allowed) { throw new Error(`Tool ${name} is not allowed`); } if (permission?.requiresConfirmation) { const confirmed = await this.requestUserConfirmation(name, args); if (!confirmed) { throw new Error(`User denied permission for ${name}`); } } return await super.callTool(name, args); } private async requestUserConfirmation( name: string, args: Record ): Promise { // 실제 구현에서는 사용자 입력 대기 console.log(`\n⚠️ Tool ${name} requires confirmation`); console.log(`Arguments: ${JSON.stringify(args, null, 2)}`); console.log(`Allow? (y/n)`); // ... user input handling return true; // 예시 } } ``` ## 프로덕션 준비 체크리스트 실제 프로덕션에 배포하기 전 확인할 사항: - [ ] 모든 환경 변수 안전하게 관리 (Vault, Secret Manager) - [ ] Rate limiting 구현 - [ ] 에러 로깅 및 모니터링 (Sentry, DataDog 등) - [ ] 재시도 로직 with 지수 백오프 - [ ] Circuit breaker 패턴 - [ ] Timeout 설정 - [ ] 입력 검증 (XSS, Injection 방지) - [ ] API 키 rotation 전략 - [ ] 비용 모니터링 및 알람 - [ ] 부하 테스트 - [ ] 장애 복구 계획 ## 다음 단계 v10에서는 이 프로젝트 전체를 회고하고, 배운 점과 감상을 정리할 것이다. --- **작성일**: 2025-11-26 **카테고리**: 한계점 분석 및 개선 방안