#
tokens: 3575/50000 10/10 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | } 
```