index.ts•14 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";
import { GenerationParams, RequestBody } from "./types.js";
// 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: string, maxChunkSize: number = 2000): string[] {
const chunks: string[] = [];
// 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 }: GenerationParams, 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: 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: any) {
console.error("Fetch error:", fetchError.name, fetchError.message);
throw fetchError; // Re-throw to be caught by outer catch
}
} catch (error: any) {
// 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);
});