# Directory Structure ``` ├── .env.example ├── .gitignore ├── ecosystem.config.js ├── llms-install.md ├── mcp-gemini-prd.md ├── package-lock.json ├── package.json ├── README.md ├── src │ ├── api │ │ ├── gemini.ts │ │ ├── google.ts │ │ ├── search.ts │ │ └── youtube.ts │ ├── config.ts │ ├── index.ts │ ├── mcp.ts │ ├── server.ts │ ├── types │ │ └── gemini.ts │ └── utils │ └── logger.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` # Server Configuration PORT=8000 HOST=0.0.0.0 NODE_ENV=development # API Keys GOOGLE_API_KEY=your-google-api-key-here YOUTUBE_API_KEY=your-youtube-api-key-here # Logging LOG_LEVEL=debug ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Dependencies node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* # Environment .env .env.local .env.*.local # Build dist/ build/ # IDE .idea/ .vscode/ *.swp *.swo # OS .DS_Store Thumbs.db ``` -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- ```typescript import pino from 'pino'; // stderr로 로그 출력 export const logger = pino({ level: process.env.DEBUG === 'true' ? 'debug' : 'info', formatters: { level: (label) => { return { level: label }; }, }, timestamp: () => `,"time":"${new Date().toISOString()}"`, }, process.stderr); ``` -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- ```javascript module.exports = { apps: [{ name: 'mcp-gemini', script: 'dist/index.js', instances: 1, autorestart: true, watch: false, max_memory_restart: '1G', env: { NODE_ENV: 'development', PORT: 3000 }, env_production: { NODE_ENV: 'production', PORT: 3000 } }] }; ``` -------------------------------------------------------------------------------- /src/types/gemini.ts: -------------------------------------------------------------------------------- ```typescript export interface ChatMessage { role: string; content: string; } export interface GenerateRequest { prompt: string; } export interface VideoAnalysisRequest { videoUrl: string; query: string; } export interface SearchRequest { query: string; } export interface ChatStartRequest { history?: ChatMessage[]; } export interface ChatStartResponse { sessionId: string; } export interface ApiResponse<T> { result: T; error?: string; } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2020", "module": "commonjs", "lib": ["ES2020"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "moduleResolution": "node", "resolveJsonModule": true, "declaration": true, "sourceMap": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "tests"] } ``` -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- ```typescript import dotenv from 'dotenv'; import { GenerationConfig } from '@google/generative-ai'; // 환경 변수 로드 dotenv.config(); export interface ServerConfig { port: number; host: string; nodeEnv: string; googleApiKey: string; logLevel: string; defaultConfig: GenerationConfig; } // 환경 변수 유효성 검사 const validateEnv = (): void => { if (!process.env.GOOGLE_API_KEY) { throw new Error('GOOGLE_API_KEY is required'); } }; // 설정 객체 생성 export const config: ServerConfig = { port: parseInt(process.env.PORT || '8000', 10), host: process.env.HOST || '0.0.0.0', nodeEnv: process.env.NODE_ENV || 'development', googleApiKey: process.env.GOOGLE_API_KEY || '', logLevel: process.env.LOG_LEVEL || 'info', defaultConfig: { temperature: 1, topP: 0.95, topK: 40, maxOutputTokens: 8192, }, }; validateEnv(); export default config; ``` -------------------------------------------------------------------------------- /src/api/google.ts: -------------------------------------------------------------------------------- ```typescript import axios from 'axios'; import { logger } from '../utils/logger'; const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY; const SEARCH_ENGINE_ID = process.env.SEARCH_ENGINE_ID || '017576662512468239146:omuauf_lfve'; interface SearchResult { title: string; link: string; snippet: string; } export async function searchWeb(query: string): Promise<string> { try { const response = await axios.get('https://www.googleapis.com/customsearch/v1', { params: { key: GOOGLE_API_KEY, cx: SEARCH_ENGINE_ID, q: query } }); const items = response.data.items as SearchResult[]; if (!items || items.length === 0) { return '검색 결과가 없습니다.'; } const results = items.slice(0, 5).map(item => { return `제목: ${item.title}\n링크: ${item.link}\n내용: ${item.snippet}\n`; }).join('\n'); return results; } catch (error) { logger.error('웹 검색 실패', error); throw new Error('웹 검색 중 오류가 발생했습니다.'); } } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "mcp-gemini", "version": "1.0.0", "description": "MCP server for Google Gemini API integration", "main": "dist/index.js", "scripts": { "build": "tsc", "start": "pm2 start ecosystem.config.js --env production", "dev": "ts-node src/index.ts", "test": "jest", "test:watch": "jest --watch", "lint": "eslint src/**/*.ts", "format": "prettier --write \"src/**/*.ts\"", "stop": "pm2 stop mcp-gemini", "restart": "pm2 restart mcp-gemini", "logs": "pm2 logs mcp-gemini", "status": "pm2 status" }, "keywords": [ "mcp", "gemini", "ai", "claude" ], "author": "", "license": "ISC", "dependencies": { "@fastify/cors": "^8.5.0", "@google/generative-ai": "^0.1.3", "@modelcontextprotocol/sdk": "^1.8.0", "axios": "^1.8.4", "dotenv": "^16.4.7", "fastify": "^4.29.0", "googleapis": "^148.0.0", "json-rpc-2.0": "^1.7.0", "pino": "^8.21.0", "pino-pretty": "^10.3.1", "typescript": "^5.0.0", "zod": "^3.24.2", "pm2": "^5.3.1" }, "devDependencies": { "@types/jest": "^29.0.0", "@types/node": "^20.0.0", "jest": "^29.0.0", "ts-jest": "^29.0.0", "ts-node": "^10.0.0" } } ``` -------------------------------------------------------------------------------- /src/mcp.ts: -------------------------------------------------------------------------------- ```typescript import { generateContent } from './api/gemini'; import { analyzeVideo, getVideoInfo } from './api/youtube'; import { searchWeb } from './api/google'; interface JsonRpcRequest { jsonrpc: string; id: number; method: string; params: any; } interface JsonRpcResponse { jsonrpc: string; id: number; result?: any; error?: { code: number; message: string; data?: any; }; } type ToolFunction = (input: string) => Promise<string>; interface Tools { [key: string]: ToolFunction; } const SUPPORTED_TOOLS: Tools = { 'generate': generateContent, 'analyze-video': async (input: string) => { try { const [videoUrl, query] = input.split('|'); console.error(`비디오 분석 중: URL=${videoUrl}, 질문=${query}`); const videoInfo = await getVideoInfo(videoUrl); return await analyzeVideo(videoInfo, query); } catch (error) { console.error('비디오 분석 오류:', error); throw error; } }, 'search': searchWeb }; export async function handleJsonRpcMessage(request: JsonRpcRequest): Promise<JsonRpcResponse | null> { console.error(`메시지 수신: ${JSON.stringify(request)}`); if (request.method === 'initialize') { console.error('초기화 요청 처리 중'); const response = { jsonrpc: '2.0', id: request.id, result: { capabilities: { tools: Object.keys(SUPPORTED_TOOLS).map(name => ({ name, description: `${name} 기능`, parameters: { type: 'object', properties: { input: { type: 'string' } }, required: ['input'] } })) } } }; console.error(`초기화 응답: ${JSON.stringify(response)}`); return response; } if (request.method.startsWith('tools/')) { const toolName = request.params.name; console.error(`도구 호출: ${toolName}`); const tool = SUPPORTED_TOOLS[toolName]; if (!tool) { console.error(`지원하지 않는 도구: ${toolName}`); return { jsonrpc: '2.0', id: request.id, error: { code: -32601, message: `Tool '${toolName}' not found` } }; } try { console.error(`도구 실행 중: ${toolName}, 파라미터: ${JSON.stringify(request.params.parameters)}`); const result = await tool(request.params.parameters.input); console.error(`도구 실행 완료: ${toolName}`); const response = { jsonrpc: '2.0', id: request.id, result: { output: result } }; console.error(`도구 응답: ${JSON.stringify(response).substring(0, 100)}...`); return response; } catch (err) { const error = err as Error; console.error(`도구 실행 오류: ${error.message}`); return { jsonrpc: '2.0', id: request.id, error: { code: -32000, message: 'Internal error', data: error.message } }; } } console.error(`지원하지 않는 메서드: ${request.method}`); return null; } ```