# 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: -------------------------------------------------------------------------------- ``` 1 | # Server Configuration 2 | PORT=8000 3 | HOST=0.0.0.0 4 | NODE_ENV=development 5 | 6 | # API Keys 7 | GOOGLE_API_KEY=your-google-api-key-here 8 | YOUTUBE_API_KEY=your-youtube-api-key-here 9 | 10 | # Logging 11 | LOG_LEVEL=debug ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Environment 8 | .env 9 | .env.local 10 | .env.*.local 11 | 12 | # Build 13 | dist/ 14 | build/ 15 | 16 | # IDE 17 | .idea/ 18 | .vscode/ 19 | *.swp 20 | *.swo 21 | 22 | # OS 23 | .DS_Store 24 | Thumbs.db ``` -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- ```typescript 1 | import pino from 'pino'; 2 | 3 | // stderr로 로그 출력 4 | export const logger = pino({ 5 | level: process.env.DEBUG === 'true' ? 'debug' : 'info', 6 | formatters: { 7 | level: (label) => { 8 | return { level: label }; 9 | }, 10 | }, 11 | timestamp: () => `,"time":"${new Date().toISOString()}"`, 12 | }, process.stderr); ``` -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- ```javascript 1 | module.exports = { 2 | apps: [{ 3 | name: 'mcp-gemini', 4 | script: 'dist/index.js', 5 | instances: 1, 6 | autorestart: true, 7 | watch: false, 8 | max_memory_restart: '1G', 9 | env: { 10 | NODE_ENV: 'development', 11 | PORT: 3000 12 | }, 13 | env_production: { 14 | NODE_ENV: 'production', 15 | PORT: 3000 16 | } 17 | }] 18 | }; ``` -------------------------------------------------------------------------------- /src/types/gemini.ts: -------------------------------------------------------------------------------- ```typescript 1 | export interface ChatMessage { 2 | role: string; 3 | content: string; 4 | } 5 | 6 | export interface GenerateRequest { 7 | prompt: string; 8 | } 9 | 10 | export interface VideoAnalysisRequest { 11 | videoUrl: string; 12 | query: string; 13 | } 14 | 15 | export interface SearchRequest { 16 | query: string; 17 | } 18 | 19 | export interface ChatStartRequest { 20 | history?: ChatMessage[]; 21 | } 22 | 23 | export interface ChatStartResponse { 24 | sessionId: string; 25 | } 26 | 27 | export interface ApiResponse<T> { 28 | result: T; 29 | error?: string; 30 | } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["ES2020"], 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "declaration": true, 15 | "sourceMap": true 16 | }, 17 | "include": ["src/**/*"], 18 | "exclude": ["node_modules", "dist", "tests"] 19 | } ``` -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import dotenv from 'dotenv'; 2 | import { GenerationConfig } from '@google/generative-ai'; 3 | 4 | // 환경 변수 로드 5 | dotenv.config(); 6 | 7 | export interface ServerConfig { 8 | port: number; 9 | host: string; 10 | nodeEnv: string; 11 | googleApiKey: string; 12 | logLevel: string; 13 | defaultConfig: GenerationConfig; 14 | } 15 | 16 | // 환경 변수 유효성 검사 17 | const validateEnv = (): void => { 18 | if (!process.env.GOOGLE_API_KEY) { 19 | throw new Error('GOOGLE_API_KEY is required'); 20 | } 21 | }; 22 | 23 | // 설정 객체 생성 24 | export const config: ServerConfig = { 25 | port: parseInt(process.env.PORT || '8000', 10), 26 | host: process.env.HOST || '0.0.0.0', 27 | nodeEnv: process.env.NODE_ENV || 'development', 28 | googleApiKey: process.env.GOOGLE_API_KEY || '', 29 | logLevel: process.env.LOG_LEVEL || 'info', 30 | defaultConfig: { 31 | temperature: 1, 32 | topP: 0.95, 33 | topK: 40, 34 | maxOutputTokens: 8192, 35 | }, 36 | }; 37 | 38 | validateEnv(); 39 | 40 | export default config; ``` -------------------------------------------------------------------------------- /src/api/google.ts: -------------------------------------------------------------------------------- ```typescript 1 | import axios from 'axios'; 2 | import { logger } from '../utils/logger'; 3 | 4 | const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY; 5 | const SEARCH_ENGINE_ID = process.env.SEARCH_ENGINE_ID || '017576662512468239146:omuauf_lfve'; 6 | 7 | interface SearchResult { 8 | title: string; 9 | link: string; 10 | snippet: string; 11 | } 12 | 13 | export async function searchWeb(query: string): Promise<string> { 14 | try { 15 | const response = await axios.get('https://www.googleapis.com/customsearch/v1', { 16 | params: { 17 | key: GOOGLE_API_KEY, 18 | cx: SEARCH_ENGINE_ID, 19 | q: query 20 | } 21 | }); 22 | 23 | const items = response.data.items as SearchResult[]; 24 | if (!items || items.length === 0) { 25 | return '검색 결과가 없습니다.'; 26 | } 27 | 28 | const results = items.slice(0, 5).map(item => { 29 | return `제목: ${item.title}\n링크: ${item.link}\n내용: ${item.snippet}\n`; 30 | }).join('\n'); 31 | 32 | return results; 33 | } catch (error) { 34 | logger.error('웹 검색 실패', error); 35 | throw new Error('웹 검색 중 오류가 발생했습니다.'); 36 | } 37 | } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "mcp-gemini", 3 | "version": "1.0.0", 4 | "description": "MCP server for Google Gemini API integration", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "pm2 start ecosystem.config.js --env production", 9 | "dev": "ts-node src/index.ts", 10 | "test": "jest", 11 | "test:watch": "jest --watch", 12 | "lint": "eslint src/**/*.ts", 13 | "format": "prettier --write \"src/**/*.ts\"", 14 | "stop": "pm2 stop mcp-gemini", 15 | "restart": "pm2 restart mcp-gemini", 16 | "logs": "pm2 logs mcp-gemini", 17 | "status": "pm2 status" 18 | }, 19 | "keywords": [ 20 | "mcp", 21 | "gemini", 22 | "ai", 23 | "claude" 24 | ], 25 | "author": "", 26 | "license": "ISC", 27 | "dependencies": { 28 | "@fastify/cors": "^8.5.0", 29 | "@google/generative-ai": "^0.1.3", 30 | "@modelcontextprotocol/sdk": "^1.8.0", 31 | "axios": "^1.8.4", 32 | "dotenv": "^16.4.7", 33 | "fastify": "^4.29.0", 34 | "googleapis": "^148.0.0", 35 | "json-rpc-2.0": "^1.7.0", 36 | "pino": "^8.21.0", 37 | "pino-pretty": "^10.3.1", 38 | "typescript": "^5.0.0", 39 | "zod": "^3.24.2", 40 | "pm2": "^5.3.1" 41 | }, 42 | "devDependencies": { 43 | "@types/jest": "^29.0.0", 44 | "@types/node": "^20.0.0", 45 | "jest": "^29.0.0", 46 | "ts-jest": "^29.0.0", 47 | "ts-node": "^10.0.0" 48 | } 49 | } 50 | ``` -------------------------------------------------------------------------------- /src/mcp.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { generateContent } from './api/gemini'; 2 | import { analyzeVideo, getVideoInfo } from './api/youtube'; 3 | import { searchWeb } from './api/google'; 4 | 5 | interface JsonRpcRequest { 6 | jsonrpc: string; 7 | id: number; 8 | method: string; 9 | params: any; 10 | } 11 | 12 | interface JsonRpcResponse { 13 | jsonrpc: string; 14 | id: number; 15 | result?: any; 16 | error?: { 17 | code: number; 18 | message: string; 19 | data?: any; 20 | }; 21 | } 22 | 23 | type ToolFunction = (input: string) => Promise<string>; 24 | 25 | interface Tools { 26 | [key: string]: ToolFunction; 27 | } 28 | 29 | const SUPPORTED_TOOLS: Tools = { 30 | 'generate': generateContent, 31 | 'analyze-video': async (input: string) => { 32 | try { 33 | const [videoUrl, query] = input.split('|'); 34 | console.error(`비디오 분석 중: URL=${videoUrl}, 질문=${query}`); 35 | const videoInfo = await getVideoInfo(videoUrl); 36 | return await analyzeVideo(videoInfo, query); 37 | } catch (error) { 38 | console.error('비디오 분석 오류:', error); 39 | throw error; 40 | } 41 | }, 42 | 'search': searchWeb 43 | }; 44 | 45 | export async function handleJsonRpcMessage(request: JsonRpcRequest): Promise<JsonRpcResponse | null> { 46 | console.error(`메시지 수신: ${JSON.stringify(request)}`); 47 | 48 | if (request.method === 'initialize') { 49 | console.error('초기화 요청 처리 중'); 50 | const response = { 51 | jsonrpc: '2.0', 52 | id: request.id, 53 | result: { 54 | capabilities: { 55 | tools: Object.keys(SUPPORTED_TOOLS).map(name => ({ 56 | name, 57 | description: `${name} 기능`, 58 | parameters: { 59 | type: 'object', 60 | properties: { 61 | input: { type: 'string' } 62 | }, 63 | required: ['input'] 64 | } 65 | })) 66 | } 67 | } 68 | }; 69 | console.error(`초기화 응답: ${JSON.stringify(response)}`); 70 | return response; 71 | } 72 | 73 | if (request.method.startsWith('tools/')) { 74 | const toolName = request.params.name; 75 | console.error(`도구 호출: ${toolName}`); 76 | 77 | const tool = SUPPORTED_TOOLS[toolName]; 78 | 79 | if (!tool) { 80 | console.error(`지원하지 않는 도구: ${toolName}`); 81 | return { 82 | jsonrpc: '2.0', 83 | id: request.id, 84 | error: { 85 | code: -32601, 86 | message: `Tool '${toolName}' not found` 87 | } 88 | }; 89 | } 90 | 91 | try { 92 | console.error(`도구 실행 중: ${toolName}, 파라미터: ${JSON.stringify(request.params.parameters)}`); 93 | const result = await tool(request.params.parameters.input); 94 | console.error(`도구 실행 완료: ${toolName}`); 95 | 96 | const response = { 97 | jsonrpc: '2.0', 98 | id: request.id, 99 | result: { output: result } 100 | }; 101 | 102 | console.error(`도구 응답: ${JSON.stringify(response).substring(0, 100)}...`); 103 | return response; 104 | } catch (err) { 105 | const error = err as Error; 106 | console.error(`도구 실행 오류: ${error.message}`); 107 | return { 108 | jsonrpc: '2.0', 109 | id: request.id, 110 | error: { 111 | code: -32000, 112 | message: 'Internal error', 113 | data: error.message 114 | } 115 | }; 116 | } 117 | } 118 | 119 | console.error(`지원하지 않는 메서드: ${request.method}`); 120 | return null; 121 | } ```