Skip to main content
Glama

MCPGame

index.ts20 kB
#!/usr/bin/env node import dotenv from "dotenv"; dotenv.config(); import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListToolsRequestSchema, CallToolRequestSchema, ErrorCode, McpError, ListToolsRequest, CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; import { OpenAI } from "openai"; import { OpenAIImageArgs, isValidOpenAIImageArgs } from "./types.js"; import fs from "fs"; import path from "path"; // Check for OpenAI API key const API_KEY = process.env.OPENAI_API_KEY; if (!API_KEY) { console.error("ERROR: OPENAI_API_KEY environment variable is missing. Ensure it's set in the .env file."); process.exit(1); } console.error("API Key available, length:", API_KEY.length); // Initialize OpenAI client const openai = new OpenAI({ apiKey: API_KEY }); // Define types for response interface OpenAIImageData { url?: string; b64_json?: string; revised_prompt?: string; } interface OpenAIImageResponse { created: number; data: OpenAIImageData[]; } // Define content types for MCP response interface TextContent { type: "text"; text: string; } interface ImageContent { type: "image"; description: string; data: { mediaType: string; base64Data?: string; }; } type Content = TextContent | ImageContent; // Create two possible image directories - one in the server and one in the backend const possiblePaths = [ // User-specified path "/Users/kristianfagerlie/apps/MCPGame/server/openai-server/public/image", // Local path in the OpenAI server directory path.resolve(process.cwd(), "image"), // Path in the backend server "/Users/kristianfagerlie/apps/mcpclient2/mcp-web-client/mcp-backend-server/image", // Path in MCPGame directory "/Users/kristianfagerlie/apps/MCPGame/image" ]; let imageDir = ""; try { console.error("Checking possible image directories:"); // First check if any of the paths exist and are writable for (const dirPath of possiblePaths) { console.error(`Checking directory: ${dirPath}`); if (fs.existsSync(dirPath)) { try { // Try writing a test file to verify permission const testFile = path.join(dirPath, `test-${Date.now()}.txt`); fs.writeFileSync(testFile, "Test write permission"); fs.unlinkSync(testFile); // Clean up test file imageDir = dirPath; console.error(`Using existing writable directory: ${imageDir}`); break; } catch (err) { console.error(`Directory ${dirPath} exists but is not writable: ${err}`); } } else { console.error(`Directory ${dirPath} does not exist`); } } // If no writable directory found, try to create them in order if (!imageDir) { for (const dirPath of possiblePaths) { try { console.error(`Attempting to create directory: ${dirPath}`); fs.mkdirSync(dirPath, { recursive: true }); // Verify it was created and is writable const testFile = path.join(dirPath, `test-${Date.now()}.txt`); fs.writeFileSync(testFile, "Test write permission"); fs.unlinkSync(testFile); // Clean up test file imageDir = dirPath; console.error(`Successfully created and verified directory: ${imageDir}`); break; } catch (err) { console.error(`Failed to create or write to directory ${dirPath}: ${err}`); } } } if (!imageDir) { throw new Error("Could not find or create any writable image directory"); } // Create a permanent marker file to identify this directory const markerFile = path.join(imageDir, "openai-image-directory.txt"); fs.writeFileSync(markerFile, `OpenAI Image Directory created on ${new Date().toISOString()}`); console.error(`Created marker file at ${markerFile}`); console.error(`Final image directory: ${imageDir}`); } catch (error) { console.error(`ERROR during image directory setup: ${error instanceof Error ? error.stack : String(error)}`); // Fall back to the first path if we couldn't set up any directory imageDir = possiblePaths[0]; console.error(`Falling back to directory: ${imageDir}`); } // Create MCP server const server = new Server({ name: "openai-image-server", version: "0.1.0" }, { capabilities: { tools: {} } }); // Set up error handling server.onerror = (error: unknown) => { console.error("MCP Server Error:", error); }; process.on('SIGINT', async () => { await server.close(); process.exit(0); }); // Helper function to save base64 image data to a file function saveBase64Image(base64Data: string, filename: string): string { try { console.error(`Saving base64 image data to file`); console.error(`Target filename: ${filename}`); console.error(`Target directory: ${imageDir}`); const imagePath = path.join(imageDir, filename); console.error(`Full path for image: ${imagePath}`); // Create the directory again just to be sure if (!fs.existsSync(imageDir)) { console.error(`Image directory doesn't exist, creating it: ${imageDir}`); fs.mkdirSync(imageDir, { recursive: true }); } // Convert base64 to buffer and save console.error(`Converting base64 to buffer...`); const buffer = Buffer.from(base64Data, 'base64'); console.error(`Image converted to buffer, size: ${buffer.length} bytes`); // Write the buffer to a file console.error(`Writing image buffer to file: ${imagePath}`); fs.writeFileSync(imagePath, buffer); console.error(`Image saved successfully to ${imagePath}`); // Verify file exists after saving if (fs.existsSync(imagePath)) { const stats = fs.statSync(imagePath); console.error(`Verified file exists: ${imagePath}, size: ${stats.size} bytes`); } else { console.error(`ERROR: Failed to verify file exists at ${imagePath}`); } return imagePath; } catch (error) { console.error(`ERROR while saving base64 image data: ${error instanceof Error ? error.stack : String(error)}`); return ""; } } // Helper function to save image from URL async function saveImageFromUrl(imageUrl: string, filename: string): Promise<string> { try { console.error(`Saving image from URL: ${imageUrl}`); console.error(`Target filename: ${filename}`); console.error(`Target directory: ${imageDir}`); const imagePath = path.join(imageDir, filename); console.error(`Full path for image: ${imagePath}`); // Create the directory again just to be sure if (!fs.existsSync(imageDir)) { console.error(`Image directory doesn't exist, creating it: ${imageDir}`); fs.mkdirSync(imageDir, { recursive: true }); } // Fetch the image from the URL console.error(`Fetching image from URL...`); const response = await fetch(imageUrl); if (!response.ok) { throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`); } console.error(`Image fetched successfully: ${response.status} ${response.statusText}`); // Get the image as a buffer console.error(`Converting image to buffer...`); const arrayBuffer = await response.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); console.error(`Image converted to buffer, size: ${buffer.length} bytes`); // Write the buffer to a file console.error(`Writing image buffer to file: ${imagePath}`); fs.writeFileSync(imagePath, buffer); console.error(`Image saved successfully to ${imagePath}`); // Verify file exists after saving if (fs.existsSync(imagePath)) { const stats = fs.statSync(imagePath); console.error(`Verified file exists: ${imagePath}, size: ${stats.size} bytes`); } else { console.error(`ERROR: Failed to verify file exists at ${imagePath}`); } return imagePath; } catch (error) { console.error(`ERROR while saving image from URL: ${error instanceof Error ? error.stack : String(error)}`); return ""; } } // Set up request handlers server.setRequestHandler( ListToolsRequestSchema, async (_request: ListToolsRequest) => { console.error("Handling ListToolsRequest"); return { tools: [{ name: "generate_image", description: "Generate an image using OpenAI's DALL-E model", inputSchema: { type: "object", properties: { prompt: { type: "string", description: "The text prompt to generate an image from" }, n: { type: "number", description: "Number of images to generate (1-10)", minimum: 1, maximum: 10, default: 1 }, size: { type: "string", description: "Image size (256x256, 512x512, 1024x1024, 1792x1024, or 1024x1792)", enum: ["256x256", "512x512", "1024x1024", "1792x1024", "1024x1792"], default: "1024x1024" }, model: { type: "string", description: "The model to use for image generation", enum: ["gpt-image-1"], default: "gpt-image-1" } }, required: ["prompt"] } }] }; } ); server.setRequestHandler( CallToolRequestSchema, async (request: CallToolRequest) => { console.error("Handling CallToolRequest:", JSON.stringify(request.params)); if (request.params.name !== "generate_image") { console.error("ERROR: Unknown tool requested:", request.params.name); throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } if (!request.params.arguments) { console.error("ERROR: Missing arguments"); throw new McpError( ErrorCode.InvalidParams, "Missing arguments" ); } if (!isValidOpenAIImageArgs(request.params.arguments)) { console.error("ERROR: Invalid arguments format:", JSON.stringify(request.params.arguments)); throw new McpError( ErrorCode.InvalidParams, "Invalid arguments: prompt is required and must be a non-empty string" ); } try { const { prompt, n = 1, size = "1024x1024", model = "gpt-image-1" } = request.params.arguments; console.error(`Generating image for prompt: "${prompt}" with model: ${model}, size: ${size}, n: ${n}`); // Type assertion to ensure size type is correct for OpenAI API const imageSize = size as "256x256" | "512x512" | "1024x1024" | "1792x1024" | "1024x1792"; // Log API request parameters console.error(`Calling OpenAI API with parameters: model=${model}, prompt="${prompt}", n=${n}, size=${imageSize}`); // Request images from OpenAI const response = await openai.images.generate({ model, prompt, n, size: imageSize }) as OpenAIImageResponse; console.error(`Generated images from OpenAI API. Raw response: ${JSON.stringify(response)}`); // Make sure we have a valid response format let imageData: OpenAIImageData[] = []; if (Array.isArray(response)) { // If response itself is an array console.error(`Response is an array with ${response.length} items`); imageData = response; } else if (response.data && Array.isArray(response.data)) { // Standard format console.error(`Response has a data array with ${response.data.length} items`); imageData = response.data; } else if (typeof response === 'string') { // If response is a raw string (might be base64) console.error(`Response is a string of length ${(response as string).length}`); // Create a fake image data object with the string as b64_json imageData = [{ b64_json: response as string }]; } else if (typeof response === 'object') { // If it's a single object, wrap it console.error(`Response is a single object, wrapping in array`); imageData = [response as unknown as OpenAIImageData]; // Special handler for the raw response format we saw in the error if ((response as any).b64_json && typeof (response as any).b64_json === 'string') { console.error(`Found b64_json in response object`); imageData = [{ b64_json: (response as any).b64_json }]; } } else { // Last resort, create a dummy object console.error(`Response format unrecognized, creating empty array`); imageData = []; } console.error(`Parsed ${imageData.length} images from response`); console.error(`Image data structure:`, JSON.stringify(imageData.map(img => ({ keys: Object.keys(img), hasB64: !!img.b64_json, hasB64Json: !!(img as any).b64Json, hasUrl: !!img.url })))); // Create timestamp for filename uniqueness const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); // Create simplified prompt for filename const filePrompt = prompt.substring(0, 30).replace(/[^a-zA-Z0-9]/g, '_'); // Generate filenames and save images const savedImagePaths: string[] = []; console.error(`Processing ${imageData.length} images for saving...`); // Process each image in the response for (let index = 0; index < imageData.length; index++) { const img = imageData[index]; const filename = `${timestamp}_${filePrompt}_${index+1}.png`; // Debug the image data structure console.error(`Image ${index+1} data structure:`, JSON.stringify({ hasB64: !!img.b64_json, hasB64Json: !!(img as any).b64Json, hasUrl: !!img.url, keys: Object.keys(img) })); // Get base64 data, trying both property names const base64Data = img.b64_json || (img as any).b64Json; if (base64Data) { // If the response contains base64 encoded image data console.error(`Processing image ${index+1} with base64 data (length: ${base64Data.length})`); const imagePath = saveBase64Image(base64Data, filename); if (imagePath) { savedImagePaths.push(imagePath); } } else if (img.url) { // If the response contains image URLs console.error(`Processing image ${index+1} with URL: ${img.url}`); try { // Make URL processing synchronous to ensure it's available for the response const imagePath = await saveImageFromUrl(img.url, filename); if (imagePath) { savedImagePaths.push(imagePath); } } catch (error) { console.error(`URL save failed for image ${index+1}: ${error}`); } } else { console.error(`ERROR: Image ${index+1} has neither URL nor base64 data. Raw data:`, JSON.stringify(img)); // If we received a raw base64 string (no b64_json property) if (typeof img === 'object' && img !== null) { // Try to check for any string property that might contain base64 data for (const key of Object.keys(img)) { const value = (img as Record<string, any>)[key]; if (typeof value === 'string' && ( value.startsWith('data:image') || /^[A-Za-z0-9+/=]+$/.test(value) )) { try { let base64Data = value; if (value.includes('data:image')) { base64Data = value.split(',')[1]; } console.error(`Found possible base64 data in property ${key} (length: ${base64Data.length})`); const imagePath = saveBase64Image(base64Data, filename); if (imagePath) { savedImagePaths.push(imagePath); break; } } catch (error) { console.error(`Failed to process property ${key} as base64: ${error}`); } } } } } } console.error(`Successfully saved ${savedImagePaths.length} out of ${imageData.length} images`); // Wait for any pending promises console.error(`Waiting for ${savedImagePaths.length} images to be saved...`); console.error(`Image save promises resolved with results: ${savedImagePaths.join(', ')}`); // Format response text including image save locations const responseText = `Generated ${imageData.length} image(s) for prompt: "${prompt}"\n\n` + imageData.map((img, i) => { const savedPath = i < savedImagePaths.length && savedImagePaths[i] ? `\nSaved to: ${savedImagePaths[i]}` : '\nWARNING: Image could not be saved locally'; // Get base64 data, trying both property names const base64Data = img.b64_json || (img as any).b64Json; if (base64Data) { return `Image ${i+1}: Successfully generated${savedPath}`; } else if (img.url) { return `Image ${i+1}: Successfully generated${savedPath}\nURL: ${img.url}`; } else { return `Image ${i+1}: Failed to generate image.`; } }).join("\n\n"); // Create content array for MCP response - only use text format for compatibility const contentArray: Content[] = [ { type: "text", text: responseText } ]; // Note: We're not adding the image to the content array anymore // The image is saved to disk and the path is included in the responseText console.error(`Processing complete. Returning text-only response with saved image paths.`); return { content: contentArray }; } catch (error) { console.error("ERROR during OpenAI API call:", error instanceof Error ? error.stack : String(error)); // Try to extract base64 data from the error message if possible const errorString = String(error); if (errorString.includes("TUyRVGQ") && errorString.length > 1000) { console.error("The error message appears to contain base64 data. Attempting to save it as an image."); try { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = `${timestamp}_error_recovery.png`; const imagePath = saveBase64Image(errorString, filename); if (imagePath) { return { content: [ { type: "text", text: `Image was successfully extracted from the error response.\nSaved to: ${imagePath}` } ] }; } } catch (saveError) { console.error("Failed to save base64 data from error:", saveError); } } return { content: [ { type: "text", text: `OpenAI API error: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } } ); // Start the server async function run() { console.error("Starting OpenAI Image Generation MCP server"); try { const transport = new StdioServerTransport(); console.error("StdioServerTransport created"); await server.connect(transport); console.error("Server connected to transport"); console.error("OpenAI Image Generation MCP server running on stdio"); } catch (error) { console.error("ERROR starting server:", error); process.exit(1); } } // Main execution run().catch(error => { console.error("Server runtime error:", error); process.exit(1); });

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/AllAboutAI-YT/mcpgame'

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