Skip to main content
Glama

MCP Gemini API Server

by techkwon
import { GoogleGenerativeAI, HarmCategory, HarmBlockThreshold } from '@google/generative-ai'; import { logger } from '../utils/logger'; import config from '../config'; import * as fs from 'fs'; import * as path from 'path'; import * as crypto from 'crypto'; // API 키를 환경 변수에서 가져옴 const API_KEY = process.env.GOOGLE_API_KEY; if (!API_KEY) { throw new Error('GOOGLE_API_KEY 환경 변수가 설정되지 않았습니다.'); } const genAI = new GoogleGenerativeAI(API_KEY); // 안정적인 Gemini Pro 모델 사용 const model = genAI.getGenerativeModel({ model: "gemini-pro", }); // Gemini 1.5 Pro 모델 (이미지 생성 설명용) const imageModel = genAI.getGenerativeModel({ model: "gemini-1.5-pro", }); // Gemini 2.0 Flash Experimental 모델 (이미지 생성용) const imageGenerationModel = genAI.getGenerativeModel({ model: "gemini-2.0-flash-exp-image-generation", }); // Gemini Pro Vision 모델 (이미지 분석용) const visionModel = genAI.getGenerativeModel({ model: "gemini-pro-vision", }); // 기본 생성 설정 const defaultGenerationConfig = { temperature: 0.9, topP: 0.8, topK: 40, maxOutputTokens: 2048, }; // 이미지 생성을 위한 설정 const imageGenerationConfig = { temperature: 0.8, topP: 0.8, topK: 40, maxOutputTokens: 2048, }; // 유튜브 분석을 위한 설정 const youtubeGenerationConfig = { temperature: 0.7, topP: 0.8, topK: 40, maxOutputTokens: 2048, }; // 확장된 타입 정의 // @ts-ignore: TypeScript 경고 무시 interface ExtendedPart { text?: string; fileData?: { mimeType: string; fileUri: string; }; } // 옵션 타입 정의 interface GenerateOptions { temperature?: number; maxTokens?: number; topK?: number; topP?: number; } // 이미지 생성 옵션 타입 정의 interface ImageGenerateOptions { resolution?: string; style?: string; quality?: 'standard' | 'hd'; negative_prompt?: string; samples?: number; save_path?: string; temperature?: number; } // 이미지 편집 옵션 타입 정의 interface ImageEditOptions { style?: string; edit_prompt?: string; save_path?: string; } // 생성된 이미지 응답 타입 interface GeneratedImageResponse { imageBase64?: string; imagePath?: string; text?: string; } export const generateContent = async (prompt: string, options?: GenerateOptions): Promise<string> => { try { logger.info({ prompt }, '콘텐츠 생성 시작'); // 생성 구성 설정 const generationConfig = { ...defaultGenerationConfig, ...(options?.temperature !== undefined && { temperature: options.temperature }), ...(options?.maxTokens !== undefined && { maxOutputTokens: options.maxTokens }), ...(options?.topK !== undefined && { topK: options.topK }), ...(options?.topP !== undefined && { topP: options.topP }), }; const result = await model.generateContent({ contents: [{ role: "user", parts: [{ text: prompt }] }], generationConfig, safetySettings: [ { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, }, { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, }, { category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, }, { category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, }, ], }); const response = await result.response; logger.info('콘텐츠 생성 완료'); return response.text(); } catch (error) { logger.error('Gemini API 호출 중 오류:', error); throw new Error('콘텐츠 생성에 실패했습니다.'); } }; // 이미지 파일 저장 함수 const saveImage = async (imageData: string, savePath?: string): Promise<string> => { try { // 이미지 저장 경로 설정 const uploadDir = savePath || path.join(process.cwd(), 'uploads'); // 디렉토리가 없으면 생성 if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } // 고유한 파일명 생성 const uniqueId = crypto.randomBytes(8).toString('hex'); const imagePath = path.join(uploadDir, `generated_image_${uniqueId}.png`); // 이미지 데이터 저장 const buffer = Buffer.from(imageData, 'base64'); fs.writeFileSync(imagePath, buffer); logger.info(`이미지가 저장되었습니다: ${imagePath}`); return imagePath; } catch (error) { logger.error('이미지 저장 중 오류:', error); throw new Error('이미지 저장에 실패했습니다.'); } }; // 이미지 생성 함수 (실제 이미지 생성) export const generateImage = async (prompt: string, options?: ImageGenerateOptions): Promise<GeneratedImageResponse> => { try { logger.info({ prompt }, '이미지 생성 시작'); // 이미지 생성 설정 const config = { ...imageGenerationConfig, ...(options?.temperature !== undefined && { temperature: options.temperature }), }; // Gemini Pro Vision을 사용하여 이미지 설명 생성 const result = await visionModel.generateContent({ contents: [{ role: "user", parts: [{ text: prompt }] }], generationConfig: config, safetySettings: [ { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, }, { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, }, { category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, }, { category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, }, ], }); const response = await result.response; const description = response.text(); // 이미지 생성은 현재 Gemini에서 직접 지원하지 않으므로, // 다른 이미지 생성 API를 사용하거나 사용자에게 안내 메시지를 제공 logger.info('이미지 설명 생성 완료'); return { text: `현재 Gemini API는 직접적인 이미지 생성을 지원하지 않습니다. 다음은 요청하신 이미지에 대한 설명입니다:\n\n${description}`, }; } catch (error) { logger.error('이미지 생성 중 오류:', error); throw new Error('이미지 생성에 실패했습니다.'); } }; // 이미지 편집 함수 export const editImage = async ( imageBase64: string, prompt: string, options?: ImageEditOptions ): Promise<GeneratedImageResponse> => { try { logger.info({ prompt }, '이미지 편집 시작'); // 이미지를 base64에서 디코딩 const imageData = Buffer.from(imageBase64, 'base64'); // Gemini Pro Vision을 사용하여 이미지 분석 및 편집 설명 생성 const result = await visionModel.generateContent({ contents: [ { role: "user", parts: [ { text: prompt }, { inlineData: { mimeType: "image/jpeg", data: imageBase64 } } ] } ], generationConfig: imageGenerationConfig, safetySettings: [ { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, }, { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, }, { category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, }, { category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, }, ], }); const response = await result.response; const editDescription = response.text(); logger.info('이미지 편집 설명 생성 완료'); return { text: `현재 Gemini API는 직접적인 이미지 편집을 지원하지 않습니다. 다음은 요청하신 편집에 대한 설명입니다:\n\n${editDescription}`, imageBase64: imageBase64 // 원본 이미지 반환 }; } catch (error) { logger.error('이미지 편집 중 오류:', error); throw new Error('이미지 편집에 실패했습니다.'); } }; export const geminiClient = { generateContent, generateImage, editImage, analyzeVideo: async (videoUrl: string, query: string): Promise<string> => { try { logger.info({ videoUrl, query }, '비디오 분석 시작'); // NodeJS의 require를 사용하여 유튜브 분석 구현 // 유튜브 비디오 분석을 위한 별도 함수 구현 return await analyzeYouTubeVideoWithNodeCode(videoUrl, query); } catch (error) { logger.error({ error }, '비디오 분석 실패'); // 오류 발생 시 기존 방식으로 대체 try { logger.info('대체 방식으로 비디오 분석 시도'); const videoId = extractYouTubeVideoId(videoUrl); const enhancedPrompt = ` 다음 유튜브 비디오에 대한 분석을 제공해주세요: 비디오 ID: ${videoId} 비디오 URL: ${videoUrl} 분석 요청: ${query || '이 비디오에 대해 분석해주세요. 주요 내용, 핵심 포인트, 요약을 한국어로 알려주세요.'} 다음 단계로 분석해주세요: 1. 비디오의 제목과 채널 추정 2. 비디오의 주요 내용 요약 3. 주요 토픽과 키포인트 4. 비디오의 목적과 대상 시청자층 5. 종합적인 평가 모든 정보를 한국어로 제공해주세요. `; const result = await generateContent(enhancedPrompt, { temperature: 0.3 }); return result; } catch (backupError) { logger.error({ backupError }, '대체 비디오 분석 실패'); throw new Error('비디오 분석에 실패했습니다: ' + (error instanceof Error ? error.message : String(error))); } } }, searchWeb: async (query: string, options?: { limit?: number }): Promise<string> => { try { logger.info({ query, limit: options?.limit }, '웹 검색 시작'); const limitText = options?.limit ? `상위 ${options.limit}개의 결과만 보여주세요.` : ''; // 직접 generateContent 사용 (chatSession 대신) const searchPrompt = ` 다음 주제에 대해 인터넷에서 검색한 정보를 제공해주세요: "${query}" ${limitText} 당신은 검색 엔진입니다. 다음 형식으로 결과를 제공해주세요: 1. 검색 주제에 대한 간략한 개요 (2-3문장) 2. 주요 정보 포인트 (글머리 기호 사용) 3. 추가 상세 정보 모든 응답은 한국어로 작성해주세요. `; // 수정된 옵션으로 직접 호출 const generationConfig = { temperature: 0.3, maxOutputTokens: 2048, topP: 0.8, topK: 40 }; const configuredModel = genAI.getGenerativeModel({ model: "gemini-2.0-flash", generationConfig }); const result = await configuredModel.generateContent(searchPrompt); const responseText = await result.response.text(); // 응답 텍스트 유효성 확인 if (!responseText || responseText.trim() === '') { return '검색 결과를 찾을 수 없습니다. 다른 검색어로 시도해보세요.'; } // 로그에 일부 결과만 기록 logger.info(`웹 검색 완료: ${responseText.substring(0, 100)}...`); return responseText; } catch (error) { logger.error({ error }, '웹 검색 실패'); throw new Error('웹 검색 중 오류가 발생했습니다: ' + (error instanceof Error ? error.message : String(error))); } }, startChat: async (history: Array<{ role: string; content: string }> = [], options?: GenerateOptions): Promise<any> => { try { logger.info({ history, options }, '채팅 세션 시작'); const formattedHistory = history.map(msg => ({ role: msg.role, parts: [{ text: msg.content }], })); // 생성 구성 설정 const generationConfig = { ...defaultGenerationConfig, ...(options?.temperature !== undefined && { temperature: options.temperature }), ...(options?.maxTokens !== undefined && { maxOutputTokens: options.maxTokens }), ...(options?.topK !== undefined && { topK: options.topK }), ...(options?.topP !== undefined && { topP: options.topP }), }; return model.startChat({ generationConfig, history: formattedHistory, }); } catch (error) { logger.error({ error }, '채팅 세션 시작 실패'); throw error; } } }; // YouTube 비디오 ID 추출 함수 function extractYouTubeVideoId(url: string): string { try { // URL이 유효한지 확인 const videoUrl = new URL(url); // youtube.com 또는 youtu.be 도메인인지 확인 if (videoUrl.hostname.includes('youtube.com')) { // 정규 YouTube URL (예: https://www.youtube.com/watch?v=VIDEO_ID) const searchParams = new URLSearchParams(videoUrl.search); const videoId = searchParams.get('v'); if (videoId) return videoId; } else if (videoUrl.hostname.includes('youtu.be')) { // 단축 URL (예: https://youtu.be/VIDEO_ID) const pathSegments = videoUrl.pathname.split('/').filter(Boolean); if (pathSegments.length > 0) return pathSegments[0]; } // ID를 찾지 못한 경우, URL 자체를 반환 return url; } catch (e) { // URL 파싱 오류가 발생하면 원래 URL 반환 return url; } } // YouTube 비디오 분석 함수 (Node.js 방식) async function analyzeYouTubeVideoWithNodeCode(videoUrl: string, query: string): Promise<string> { try { logger.info('Node.js 코드로 YouTube 비디오 분석 시작'); // TypeScript 문법 검사를 피하기 위해 js 파일을 동적으로 로드하는 함수 구현 async function runYouTubeAnalysis() { try { // 임시 JS 파일을 생성하고 실행하는 대신, 직접 Gemini API 호출 const GoogleGenerativeAI = require('@google/generative-ai').GoogleGenerativeAI; const genAI = new GoogleGenerativeAI(API_KEY); const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash", }); const generationConfig = { temperature: 1, topP: 0.95, topK: 40, maxOutputTokens: 8192, responseMimeType: "text/plain", }; // raw 형태의 history 객체 생성 const history = [ { role: "user", parts: [ { fileData: { mimeType: "video/*", fileUri: videoUrl, }, }, {text: query || '이 비디오를 분석해주세요. 주요 내용, 핵심 포인트, 요약을 한국어로 알려주세요.'}, ], }, ]; // @ts-ignore: TypeScript 타입 오류 무시 const chatSession = model.startChat({ generationConfig, history, }); const result = await chatSession.sendMessage("비디오의 내용을 자세히 분석해주세요."); return result.response.text(); } catch (nodeError) { logger.error({ nodeError }, 'Node.js 방식 비디오 분석 실패'); throw nodeError; } } return await runYouTubeAnalysis(); } catch (error) { logger.error({ error }, '비디오 분석 함수 실행 실패'); throw error; } } export default generateContent;

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/techkwon/mcp-gemini'

If you have feedback or need assistance with the MCP directory API, please join our Discord server