Skip to main content
Glama

Gemini MCP Server

by dakrin
index.js15.5 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; // Initialize Gemini API key const apiKey = process.env.GEMINI_API_KEY || ''; if (!apiKey) { console.error('ERROR: GEMINI_API_KEY environment variable is not set. Please set it to a valid API key.'); process.exit(1); } // Enable debug mode for verbose logging const DEBUG = process.env.DEBUG === 'true'; const log = DEBUG ? console.log : () => { }; // Log API key length for debugging (don't log the full key for security) console.log(`GEMINI_API_KEY length: ${apiKey.length} characters`); console.log(`GEMINI_API_KEY first 5 chars: ${apiKey.substring(0, 5)}...`); // Beta API endpoint for the 2.5 Pro experimental model const betaModelName = 'models/gemini-2.5-pro-exp-03-25'; const betaApiEndpoint = `https://generativelanguage.googleapis.com/v1beta/${betaModelName}:generateContent?key=${apiKey}`; // Function to manually chunk a long string into reasonable sizes // This helps avoid any potential truncation issues with large responses function chunkText(text, maxChunkSize = 2000) { const chunks = []; // If the text is short enough, just return it as a single chunk if (text.length <= maxChunkSize) { return [text]; } // Try to break at paragraph boundaries when possible let startIndex = 0; while (startIndex < text.length) { // Try to find a paragraph break within the maxChunkSize let endIndex = startIndex + maxChunkSize; if (endIndex >= text.length) { // If we're at the end, just take the rest endIndex = text.length; } else { // Look for a paragraph break before the max size const paragraphBreak = text.lastIndexOf('\n\n', endIndex); if (paragraphBreak > startIndex && paragraphBreak < endIndex) { endIndex = paragraphBreak + 2; // Include the double newline } else { // If no paragraph break, try a single newline const lineBreak = text.lastIndexOf('\n', endIndex); if (lineBreak > startIndex && lineBreak < endIndex) { endIndex = lineBreak + 1; // Include the newline } else { // If no line break, try a sentence end const sentenceEnd = text.lastIndexOf('. ', endIndex); if (sentenceEnd > startIndex && sentenceEnd < endIndex - 1) { endIndex = sentenceEnd + 2; // Include the period and space } // If nothing sensible found, just break at maxChunkSize } } } chunks.push(text.substring(startIndex, endIndex)); startIndex = endIndex; } return chunks; } // Main function to start the MCP server async function main() { console.log("Starting Gemini 2.5 Pro Experimental MCP server..."); // Create an MCP server const server = new McpServer({ name: "gemini-mcp", version: "1.0.0" }); // Add generation tool - uses Gemini 2.5 Pro Experimental server.tool("generateWithGemini", "Generate content with Gemini 2.5 Pro Experimental (beta API)", { prompt: z.string().describe("The prompt to send to Gemini"), temperature: z.number().optional().describe("Temperature (0.0 to 1.0)"), maxTokens: z.number().optional().describe("Maximum output tokens"), safeMode: z.boolean().optional().describe("Enable safe mode for sensitive topics"), useSearch: z.boolean().optional().describe("Enable Google Search grounding tool") }, async ({ prompt, temperature = 0.9, maxTokens = 32000, safeMode = false, useSearch = false }, extra) => { console.log("Generating with Gemini 2.5 Pro, prompt:", prompt); try { log("Sending request to beta API: gemini-2.5-pro-exp-03-25"); // Create request body with optional search tool const requestBody = { contents: [ { role: "user", parts: [{ text: prompt }] } ], generationConfig: { temperature: temperature, topP: 1, topK: 64, maxOutputTokens: maxTokens } }; // Add Google Search grounding tool if requested if (useSearch) { console.log("Adding Google Search grounding tool to request"); requestBody.tools = [ { googleSearch: {} // Empty config means use default settings } ]; } // Enhanced logging for debugging console.log(`Request to beta API (${new Date().toISOString()}): ${betaApiEndpoint.substring(0, 100)}...`); try { // Use the non-streaming API const response = await fetch(betaApiEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); console.log(`Response received (${new Date().toISOString()}), status: ${response.status}`); if (response.ok) { // Capture the full raw response text for debugging and to ensure nothing is lost const rawResponseText = await response.text(); console.log(`Raw API response length: ${rawResponseText.length} characters`); // Save the raw response to a file for verification try { const fs = await import('fs'); const responseDir = '/tmp/gemini-responses'; if (!fs.existsSync(responseDir)) { fs.mkdirSync(responseDir, { recursive: true }); } const timestamp = new Date().toISOString().replace(/:/g, '-'); const rawFilename = `${responseDir}/raw-response-${timestamp}.json`; fs.writeFileSync(rawFilename, rawResponseText); console.log(`Raw API response saved to ${rawFilename}`); } catch (fsError) { console.error("Error saving raw response to file:", fsError); } if (DEBUG) { console.log("First 1000 chars of raw response:", rawResponseText.substring(0, 1000)); } // Parse the JSON from the raw text to avoid any automatic processing/truncation const data = JSON.parse(rawResponseText); // Log response structure for debugging if (DEBUG) { console.log("API response structure:", JSON.stringify(data).substring(0, 1000) + (JSON.stringify(data).length > 1000 ? "..." : "")); } if (data.candidates && data.candidates[0]?.content?.parts) { // Extract and log the full API response for debugging console.log("Full API response structure:", JSON.stringify(Object.keys(data)).substring(0, 500)); if (DEBUG) { console.log("First candidate structure:", JSON.stringify(Object.keys(data.candidates[0])).substring(0, 500)); console.log("Content parts structure:", JSON.stringify(data.candidates[0].content.parts).substring(0, 500)); } // Ensure we correctly extract all text content let text = ""; // Process all parts that might contain text for (const part of data.candidates[0].content.parts) { if (part.text) { text += part.text; } } log("Success with Gemini 2.5 Pro Experimental!"); console.log("USING MODEL: gemini-2.5-pro-exp-03-25"); // Create token usage information if available let tokenInfo = ""; if (data.usageMetadata) { const { promptTokenCount, candidatesTokenCount, totalTokenCount } = data.usageMetadata; tokenInfo = `\n\n[Token usage: ${promptTokenCount} prompt, ${candidatesTokenCount || 0} response, ${totalTokenCount} total]`; } // Create search grounding data if available let searchInfo = ""; if (useSearch && data.candidates[0].groundingMetadata?.webSearchQueries) { const searchQueries = data.candidates[0].groundingMetadata.webSearchQueries; searchInfo = `\n\n[Search queries: ${searchQueries.join(", ")}]`; } // Check text length for debugging console.log(`Response text length: ${text.length} characters`); const fullText = text + tokenInfo + searchInfo; // Save the full response to a file for verification let savedFilename = ""; try { const fs = await import('fs'); const responseDir = '/tmp/gemini-responses'; // Create directory if it doesn't exist if (!fs.existsSync(responseDir)) { fs.mkdirSync(responseDir, { recursive: true }); } // Save the full response to a timestamped file const timestamp = new Date().toISOString().replace(/:/g, '-'); savedFilename = `${responseDir}/response-${timestamp}.txt`; fs.writeFileSync(savedFilename, fullText); console.log(`Full response saved to ${savedFilename}`); // Also save metadata to a JSON file const metadataFilename = `${responseDir}/metadata-${timestamp}.json`; fs.writeFileSync(metadataFilename, JSON.stringify({ responseLength: fullText.length, promptLength: prompt.length, useSearch: useSearch, timestamp: new Date().toISOString(), tokenInfo: data.usageMetadata || null, searchQueries: useSearch ? (data.candidates[0]?.groundingMetadata?.webSearchQueries || null) : null }, null, 2)); } catch (fsError) { console.error("Error saving response to file:", fsError); } // Include the file path info in the response const fileInfo = savedFilename ? `\n\n[Complete response saved to: ${savedFilename}]` : ""; // Return the full text directly without chunking // This gives Claude a chance to display the entire response if it can // Also includes the file path as a backup console.log(`Sending full response (${fullText.length} characters) with file info`); return { content: [{ type: "text", text: fullText + fileInfo }] }; } else { console.error("Invalid API response format:", JSON.stringify(data).substring(0, 500)); throw new Error("Invalid response format from beta API"); } } else { // Try to parse error response let errorMessage = `HTTP error ${response.status}`; try { const errorData = await response.json(); console.error("API error response:", JSON.stringify(errorData).substring(0, 500)); errorMessage = `API error: ${JSON.stringify(errorData)}`; } catch (e) { // If we can't parse JSON, use text const errorText = await response.text(); console.error("API error text:", errorText.substring(0, 500)); errorMessage = `API error: ${errorText}`; } throw new Error(errorMessage); } } catch (fetchError) { console.error("Fetch error:", fetchError.name, fetchError.message); throw fetchError; // Re-throw to be caught by outer catch } } catch (error) { // Handle the case where error is undefined if (!error) { console.error("Undefined error caught in Gemini handler"); return { content: [{ type: "text", text: "The model took too long to respond or the connection was interrupted. This sometimes happens with complex topics. Please try again or rephrase your question." }], isError: true }; } // Normal error handling for defined errors console.error("Error with Gemini 2.5 Pro:", error.name || "UnknownError", error.message || "No message", error.stack || "No stack"); return { content: [{ type: "text", text: `Error using Gemini 2.5 Pro Experimental: ${error.name || 'Unknown'} - ${error.message || 'No error message'}` }], isError: true }; } }); // Add a model info tool server.tool("getModelInfo", "Get information about the Gemini model being used", {}, async () => { return { content: [{ type: "text", text: `Using Gemini 2.5 Pro Experimental (${betaModelName})\n\nThis is Google's latest experimental model from the beta API, with:\n- 1,048,576 token input limit\n- 65,536 token output limit\n- Enhanced reasoning capabilities\n- Improved instruction following` }] }; }); // Start the server with stdio transport const transport = new StdioServerTransport(); console.log("Connecting to transport..."); await server.connect(transport); } // Start the MCP server main().catch((error) => { console.error('Server error:', error); process.exit(1); });

Latest Blog Posts

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/dakrin/mcp-gemini-server'

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