Skip to main content
Glama

Ontology MCP

by bigdata-coss
gemini-service.ts25.3 kB
import axios, { AxiosError, AxiosRequestConfig } from 'axios'; import { McpError } from '@modelcontextprotocol/sdk/types.js'; // 환경 변수에서 API 키 로드 const GEMINI_API_KEY = process.env.GEMINI_API_KEY || ''; const GEMINI_BASE_URL = process.env.GEMINI_BASE_URL || 'https://generativelanguage.googleapis.com/v1'; /** * Google Gemini API와 상호작용하기 위한 서비스 클래스 */ class GeminiService { private apiKey: string; private baseUrl: string; constructor(apiKey: string = GEMINI_API_KEY, baseUrl: string = GEMINI_BASE_URL) { this.apiKey = apiKey; this.baseUrl = baseUrl; } /** * 오류를 포맷팅하는 헬퍼 메서드 */ private formatError(error: unknown): Error { if (axios.isAxiosError(error)) { const axiosError = error as AxiosError; // API 응답 오류 처리 if (axiosError.response) { const status = axiosError.response.status; const data = axiosError.response.data as any; // 다양한 HTTP 상태 코드에 따른 오류 처리 if (status === 400) { return new Error(data?.error?.message || '잘못된 요청입니다.'); } else if (status === 401) { return new Error('API 키가 잘못되었거나 누락되었습니다.'); } else if (status === 403) { return new Error('요청한 리소스에 대한 접근 권한이 없습니다.'); } else if (status === 404) { return new Error('요청한 리소스를 찾을 수 없습니다.'); } else if (status === 429) { return new Error('API 요청 한도를 초과했습니다.'); } else if (status >= 500) { return new Error('Gemini API 서버 오류가 발생했습니다.'); } // 기타 응답 오류 return new Error(data?.error?.message || `Gemini API 오류: ${status}`); } // 요청 오류 처리 (네트워크 문제 등) if (axiosError.request && !axiosError.response) { return new Error('네트워크 오류: Gemini API에 연결할 수 없습니다.'); } } // 기타 오류 처리 return new Error(error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.'); } /** * API 요청 기본 설정을 준비하는 헬퍼 메서드 */ private getRequestConfig(): AxiosRequestConfig { if (!this.apiKey) { throw new Error('Gemini API 키가 설정되어 있지 않습니다.'); } return { headers: { 'Content-Type': 'application/json', }, params: { key: this.apiKey, }, }; } /** * Gemini 모델을 사용하여 텍스트를 생성합니다. */ async generateText({ model, prompt, temperature = 0.7, max_tokens = 1024, topK = 40, topP = 0.95, }: { model: string; prompt: string; temperature?: number; max_tokens?: number; topK?: number; topP?: number; }) { try { const config = this.getRequestConfig(); const url = `${this.baseUrl}/models/${model}:generateContent`; const response = await axios.post( url, { contents: [ { parts: [ { text: prompt, }, ], }, ], generationConfig: { temperature, maxOutputTokens: max_tokens, topK, topP, }, }, config ); // 응답에서 생성된 텍스트 추출 const generatedText = response.data.candidates?.[0]?.content?.parts?.[0]?.text || ''; return { text: generatedText, model: model, usage: { completion_tokens: response.data.usageMetadata?.candidatesTokenCount || 0, prompt_tokens: response.data.usageMetadata?.promptTokenCount || 0, total_tokens: (response.data.usageMetadata?.promptTokenCount || 0) + (response.data.usageMetadata?.candidatesTokenCount || 0), }, }; } catch (error) { throw this.formatError(error); } } /** * Gemini 모델을 사용하여 채팅 대화를 완성합니다. */ async chatCompletion({ model, messages, temperature = 0.7, max_tokens = 1024, topK = 40, topP = 0.95, }: { model: string; messages: Array<{ role: string; content: string }>; temperature?: number; max_tokens?: number; topK?: number; topP?: number; }) { try { const config = this.getRequestConfig(); const url = `${this.baseUrl}/models/${model}:generateContent`; // OpenAI 형식의 메시지를 Gemini 형식으로 변환 const geminiContents = []; let currentRole = null; let currentParts: any[] = []; for (const message of messages) { // 'system' 메시지는 'user' 역할로 변환하되 prefix를 추가 if (message.role === 'system') { geminiContents.push({ role: 'user', parts: [{ text: `[system] ${message.content}` }], }); continue; } // 역할이 변경되면 새 항목 시작 if (message.role !== currentRole && currentParts.length > 0) { geminiContents.push({ role: currentRole === 'assistant' ? 'model' : 'user', parts: currentParts, }); currentParts = []; } currentRole = message.role; currentParts.push({ text: message.content }); } // 마지막 메시지 추가 if (currentParts.length > 0) { geminiContents.push({ role: currentRole === 'assistant' ? 'model' : 'user', parts: currentParts, }); } const response = await axios.post( url, { contents: geminiContents, generationConfig: { temperature, maxOutputTokens: max_tokens, topK, topP, }, }, config ); // 응답에서 생성된 텍스트 추출 const generatedContent = response.data.candidates?.[0]?.content?.parts?.[0]?.text || ''; return { message: { role: 'assistant', content: generatedContent, }, model: model, usage: { completion_tokens: response.data.usageMetadata?.candidatesTokenCount || 0, prompt_tokens: response.data.usageMetadata?.promptTokenCount || 0, total_tokens: (response.data.usageMetadata?.promptTokenCount || 0) + (response.data.usageMetadata?.candidatesTokenCount || 0), }, }; } catch (error) { throw this.formatError(error); } } /** * 사용 가능한 Gemini 모델 목록을 조회합니다. */ async listModels() { try { const config = this.getRequestConfig(); const url = `${this.baseUrl}/models`; const response = await axios.get(url, config); // Gemini 모델만 필터링 (ID에 'gemini'가 포함된 모델) const geminiModels = response.data.models?.filter( (model: any) => model.name && model.name.includes('gemini') ) || []; // 필요한 정보만 매핑 return geminiModels.map((model: any) => ({ id: model.name.split('/').pop(), name: model.displayName || model.name, description: model.description || '', created: model.createTime || '', updated: model.updateTime || '', supports: { chat: model.supportedGenerationMethods?.includes('generateContent') || false, completion: model.supportedGenerationMethods?.includes('generateContent') || false, embeddings: model.supportedGenerationMethods?.includes('embedContent') || false, }, })); } catch (error) { throw this.formatError(error); } } /** * 텍스트 프롬프트에서 이미지를 생성합니다. */ async generateImage({ model, prompt, numberOfImages, aspectRatio, personGeneration, saveDir = './temp', fileName, }: { model: string; prompt: string; numberOfImages?: number; aspectRatio?: string; personGeneration?: string; saveDir?: string; fileName?: string; }) { try { // Imagen 모델을 사용하는 경우 if (model.startsWith('imagen-')) { return await this.generateImageWithImagen({ model, prompt, numberOfImages, aspectRatio, personGeneration, saveDir, fileName, }); } // Gemini 모델이면서 이미지 편집 모델을 사용하는 경우 if (model.endsWith('-image-generation-editing')) { return await this.generateImageWithGeminiEdit({ model, prompt, saveDir, fileName, }); } // 기본적으로 Gemini 모델을 사용 return await this.generateImageWithGemini({ model, prompt, saveDir, fileName, }); } catch (error) { throw this.formatError(error); } } /** * Imagen 모델을 사용하여 이미지를 생성합니다. (내부 사용) */ private async generateImageWithImagen({ model, prompt, numberOfImages = 1, aspectRatio = '1:1', personGeneration = 'ALLOW_ADULT', saveDir = './temp', fileName = `imagen-${Date.now()}`, }: { model: string; prompt: string; numberOfImages?: number; aspectRatio?: string; personGeneration?: string; saveDir?: string; fileName?: string; }) { const config = this.getRequestConfig(); const url = `${this.baseUrl}/models/${model}:generateImages`; const response = await axios.post( url, { prompt, config: { numberOfImages, aspectRatio, personGeneration } }, config ); // 이미지 응답 처리 const generatedImages = response.data.generatedImages || []; const savedFiles = []; const fs = await import('fs'); const path = await import('path'); // 저장 디렉토리가 없으면 생성 if (!fs.existsSync(saveDir)) { fs.mkdirSync(saveDir, { recursive: true }); } // 이미지 저장 for (let i = 0; i < generatedImages.length; i++) { const imageData = generatedImages[i]?.image?.imageBytes; if (imageData) { const buffer = Buffer.from(imageData, 'base64'); const filePath = path.join(saveDir, `${fileName}-${i + 1}.png`); fs.writeFileSync(filePath, buffer); savedFiles.push(filePath); } } return { model: model, prompt: prompt, images: savedFiles, count: savedFiles.length, text: [], }; } /** * Gemini 모델을 사용하여 텍스트 프롬프트에서 이미지를 생성합니다. (내부 사용) */ private async generateImageWithGemini({ model, prompt, saveDir = './temp', fileName = `gemini-${Date.now()}`, }: { model: string; prompt: string; saveDir?: string; fileName?: string; }) { const config = this.getRequestConfig(); const url = `${this.baseUrl}/models/${model}:generateContent`; const response = await axios.post( url, { contents: [ { parts: [ { text: prompt, }, ], }, ], }, config ); // 파일 시스템 모듈 임포트 const fs = await import('fs'); const path = await import('path'); // 저장 디렉토리가 없으면 생성 if (!fs.existsSync(saveDir)) { fs.mkdirSync(saveDir, { recursive: true }); } // 응답 처리 const parts = response.data.candidates?.[0]?.content?.parts || []; const result: { text: string[]; images: string[]; } = { text: [], images: [], }; for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (part.text) { result.text.push(part.text); } else if (part.inlineData) { const imageData = part.inlineData.data; const buffer = Buffer.from(imageData, 'base64'); const filePath = path.join(saveDir, `${fileName}-${i + 1}.png`); fs.writeFileSync(filePath, buffer); result.images.push(filePath); } } return { model: model, prompt: prompt, text: result.text, images: result.images, count: result.images.length, }; } /** * Gemini 모델을 사용하여 이미지를 편집합니다. (내부 사용) */ private async generateImageWithGeminiEdit({ model, prompt, saveDir = './temp', fileName = `gemini-edited-${Date.now()}`, }: { model: string; prompt: string; saveDir?: string; fileName?: string; }) { const config = this.getRequestConfig(); const url = `${this.baseUrl}/models/${model}:generateContent`; const response = await axios.post( url, { contents: [ { parts: [ { text: prompt, }, ], }, ], }, config ); // 파일 시스템 모듈 임포트 const fs = await import('fs'); const path = await import('path'); // 저장 디렉토리가 없으면 생성 if (!fs.existsSync(saveDir)) { fs.mkdirSync(saveDir, { recursive: true }); } // 응답 처리 const parts = response.data.candidates?.[0]?.content?.parts || []; const result: { text: string[]; images: string[]; } = { text: [], images: [], }; for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (part.text) { result.text.push(part.text); } else if (part.inlineData) { const imageData = part.inlineData.data; const buffer = Buffer.from(imageData, 'base64'); const filePath = path.join(saveDir, `${fileName}-${i + 1}.png`); fs.writeFileSync(filePath, buffer); result.images.push(filePath); } } return { model: model, prompt: prompt, text: result.text, images: result.images, count: result.images.length, }; } /** * Veo 모델을 사용하여 비디오를 생성합니다. */ async generateVideos({ model, prompt, image = null, numberOfVideos = 1, aspectRatio = '16:9', personGeneration = 'dont_allow', durationSeconds = 5, saveDir = './temp', fileName = `veo-${Date.now()}`, }: { model: string; prompt: string; image?: { imageBytes: string; mimeType: string } | null; numberOfVideos?: number; aspectRatio?: string; personGeneration?: string; durationSeconds?: number; saveDir?: string; fileName?: string; }) { try { const config = this.getRequestConfig(); const url = `${this.baseUrl}/models/${model}:generateVideos`; const requestData: any = { prompt: { text: prompt, }, config: { aspectRatio, numberOfVideos, durationSeconds, personGeneration, } }; // 이미지가 제공된 경우 추가 if (image) { requestData.image = image; } // 비디오 생성 요청 시작 const response = await axios.post(url, requestData, config); // 작업 ID 가져오기 const operationName = response.data.name; if (!operationName) { throw new Error('비디오 생성 작업을 시작할 수 없습니다.'); } // 비동기 작업 상태 확인 및 완료 대기 const operationUrl = `${this.baseUrl}/${operationName}`; let operation: { done: boolean; response: any } = { done: false, response: null }; while (!operation.done) { // 10초 대기 await new Promise(resolve => setTimeout(resolve, 10000)); // 작업 상태 확인 const statusResponse = await axios.get(operationUrl, config); operation = statusResponse.data; } // 비디오 다운로드 및 저장 const fs = await import('fs'); const path = await import('path'); // 저장 디렉토리가 없으면 생성 if (!fs.existsSync(saveDir)) { fs.mkdirSync(saveDir, { recursive: true }); } const savedFiles = []; const generatedVideos = operation.response?.generatedVideos || []; for (let i = 0; i < generatedVideos.length; i++) { const videoUri = generatedVideos[i]?.video?.uri; if (videoUri) { // API 키 추가 const downloadUrl = `${videoUri}&key=${this.apiKey}`; // 비디오 다운로드 const videoResponse = await axios.get(downloadUrl, { responseType: 'arraybuffer' }); const filePath = path.join(saveDir, `${fileName}-${i + 1}.mp4`); fs.writeFileSync(filePath, Buffer.from(videoResponse.data)); savedFiles.push(filePath); } } return { model: model, prompt: prompt, videos: savedFiles, count: savedFiles.length, }; } catch (error) { throw this.formatError(error); } } /** * Gemini 모델을 사용하여 멀티모달 콘텐츠(텍스트 및 이미지)를 생성합니다. */ async generateMultimodalContent({ model, contents, temperature = 0.7, max_tokens = 1024, saveDir = './temp', fileName = `gemini-multimodal-${Date.now()}`, }: { model: string; contents: Array<{ text?: string; inlineData?: { mimeType: string; data: string } }>; temperature?: number; max_tokens?: number; saveDir?: string; fileName?: string; }) { try { const config = this.getRequestConfig(); const url = `${this.baseUrl}/models/${model}:generateContent`; const response = await axios.post( url, { contents, generationConfig: { temperature, maxOutputTokens: max_tokens, }, }, config ); // 파일 시스템 모듈 임포트 const fs = await import('fs'); const path = await import('path'); // 저장 디렉토리가 없으면 생성 if (!fs.existsSync(saveDir)) { fs.mkdirSync(saveDir, { recursive: true }); } // 응답 처리 const parts = response.data.candidates?.[0]?.content?.parts || []; const result: { text: string[]; images: string[]; } = { text: [], images: [], }; for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (part.text) { result.text.push(part.text); } else if (part.inlineData) { const imageData = part.inlineData.data; const buffer = Buffer.from(imageData, 'base64'); const filePath = path.join(saveDir, `${fileName}-${i + 1}.png`); fs.writeFileSync(filePath, buffer); result.images.push(filePath); } } return { model: model, text: result.text, images: result.images, }; } catch (error) { throw this.formatError(error); } } /** * Gemini 모델을 사용하여 텍스트 프롬프트에서 이미지를 생성합니다. * 공식 Gemini API 예제와 일치하게 구현되었습니다. * * @returns 생성된 텍스트와 이미지 파일 경로 정보 */ async generateGeminiImage({ model, prompt, saveDir = './temp', fileName = `gemini-${Date.now()}`, }: { model: string; prompt: string; saveDir?: string; fileName?: string; }) { try { if (!model.includes('gemini')) { throw new Error('이 메서드는 Gemini 모델만 지원합니다. 모델 이름에 "gemini"가 포함되어야 합니다.'); } const config = this.getRequestConfig(); const url = `${this.baseUrl}/models/${model}:generateContent`; // responseModalities 필드 제거 - API가 해당 필드를 인식하지 않음 const response = await axios.post( url, { contents: [ { parts: [ { text: prompt, }, ], }, ], }, config ); // 파일 시스템 모듈 임포트 const fs = await import('fs'); const path = await import('path'); // 저장 디렉토리가 없으면 생성 if (!fs.existsSync(saveDir)) { fs.mkdirSync(saveDir, { recursive: true }); } // 응답 처리 const parts = response.data.candidates?.[0]?.content?.parts || []; const result: { text: string[]; images: string[]; } = { text: [], images: [], }; for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (part.text) { result.text.push(part.text); } else if (part.inlineData) { const imageData = part.inlineData.data; const buffer = Buffer.from(imageData, 'base64'); const filePath = path.join(saveDir, `${fileName}-${i + 1}.png`); fs.writeFileSync(filePath, buffer); result.images.push(filePath); } } return { model: model, prompt: prompt, text: result.text, images: result.images, count: result.images.length, }; } catch (error) { throw this.formatError(error); } } /** * Gemini 모델을 사용하여 이미지를 편집합니다. * 공식 Gemini API 예제와 일치하게 구현되었습니다. * * @param model - 사용할 Gemini 모델 ID (예: gemini-2.0-flash-exp-image-generation) * @param prompt - 이미지 편집을 위한 텍스트 프롬프트 (예: "이미지에 라마를 추가해 주세요") * @param imageData - Base64로 인코딩된 이미지 데이터 * @param imageMimeType - 이미지의 MIME 타입 (기본값: 'image/png') * @param saveDir - 생성된 이미지를 저장할 디렉토리 * @param fileName - 저장할 이미지 파일 이름 (확장자 제외) * @returns 생성된 텍스트와 이미지 파일 경로 정보 */ async editGeminiImage({ model, prompt, imageData, imageMimeType = 'image/png', saveDir = './temp', fileName = `gemini-edited-${Date.now()}`, }: { model: string; prompt: string; imageData: string; imageMimeType?: string; saveDir?: string; fileName?: string; }) { try { if (!model.includes('gemini')) { throw new Error('이 메서드는 Gemini 모델만 지원합니다. 모델 이름에 "gemini"가 포함되어야 합니다.'); } if (!imageData) { throw new Error('이미지 데이터가 필요합니다. Base64로 인코딩된, 유효한 이미지 데이터를 제공하세요.'); } const config = this.getRequestConfig(); const url = `${this.baseUrl}/models/${model}:generateContent`; // responseModalities 필드 제거 - API가 해당 필드를 인식하지 않음 const response = await axios.post( url, { contents: [ { parts: [ { text: prompt }, { inlineData: { mimeType: imageMimeType, data: imageData, }, }, ], }, ], }, config ); // 파일 시스템 모듈 임포트 const fs = await import('fs'); const path = await import('path'); // 저장 디렉토리가 없으면 생성 if (!fs.existsSync(saveDir)) { fs.mkdirSync(saveDir, { recursive: true }); } // 응답 처리 const parts = response.data.candidates?.[0]?.content?.parts || []; const result: { text: string[]; images: string[]; } = { text: [], images: [], }; for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (part.text) { result.text.push(part.text); } else if (part.inlineData) { const imageData = part.inlineData.data; const buffer = Buffer.from(imageData, 'base64'); const filePath = path.join(saveDir, `${fileName}-${i + 1}.png`); fs.writeFileSync(filePath, buffer); result.images.push(filePath); } } return { model: model, prompt: prompt, text: result.text, images: result.images, count: result.images.length, }; } catch (error) { throw this.formatError(error); } } } // 싱글톤 인스턴스 생성 및 내보내기 const geminiService = new GeminiService(); export default geminiService;

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/bigdata-coss/agent_mcp'

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