2025-09-13 12:20:50 -04:00
#!/usr/bin/env node
2025-09-15 08:49:07 -04:00
import { GoogleGenerativeAI } from '@google/generative-ai'
import { GoogleAIFileManager } from '@google/generative-ai/server'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import {
CallToolRequestSchema ,
ListToolsRequestSchema ,
} from '@modelcontextprotocol/sdk/types.js'
import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
2025-09-13 12:20:50 -04:00
2025-09-15 08:49:07 -04:00
const apiKey = process . env . GEMINI_API_KEY
2025-09-13 12:20:50 -04:00
if ( ! apiKey ) {
2025-09-15 08:49:07 -04:00
console . error ( '❌ GEMINI_API_KEY environment variable is required' )
console . error ( '' )
console . error ( 'To fix this:' )
console . error ( '' )
console . error ( '1. Get your API key from: https://aistudio.google.com/apikey' )
console . error ( '' )
console . error ( '2. Add to your shell profile:' )
console . error ( ' For macOS/Linux (add to ~/.zshrc or ~/.bashrc):' )
console . error ( " export GEMINI_API_KEY='your-actual-api-key-here'" )
console . error ( '' )
console . error ( ' For Windows PowerShell:' )
console . error (
" [System.Environment]::SetEnvironmentVariable('GEMINI_API_KEY', 'your-key', 'User')" ,
)
console . error ( '' )
console . error ( '3. Reload your terminal:' )
console . error ( ' source ~/.zshrc (or source ~/.bashrc)' )
console . error ( '' )
console . error ( '4. Restart Claude Code' )
console . error ( '' )
console . error ( 'For detailed instructions, see GEMINI_VISION_SETUP.md' )
process . exit ( 1 )
2025-09-13 12:20:50 -04:00
}
2025-09-15 08:49:07 -04:00
const genAI = new GoogleGenerativeAI ( apiKey )
const fileManager = new GoogleAIFileManager ( apiKey )
const model = genAI . getGenerativeModel ({ model : 'gemini-2.5-flash' })
2025-09-13 12:20:50 -04:00
// Expand home directory in paths
function expandPath ( filepath ) {
2025-09-15 08:49:07 -04:00
if ( filepath . startsWith ( '~/' )) {
return path . join ( os . homedir (), filepath . slice ( 2 ))
2025-09-13 12:20:50 -04:00
}
2025-09-15 08:49:07 -04:00
return filepath
2025-09-13 12:20:50 -04:00
}
// Helper function to wait/sleep
function sleep ( ms ) {
2025-09-15 08:49:07 -04:00
return new Promise (( resolve ) => setTimeout ( resolve , ms ))
2025-09-13 12:20:50 -04:00
}
// Upload file to Gemini
async function uploadFile ( filePath ) {
2025-09-15 08:49:07 -04:00
const expandedPath = expandPath ( filePath )
2025-09-13 12:20:50 -04:00
try {
2025-09-15 08:49:07 -04:00
await fs . access ( expandedPath )
2025-09-13 12:20:50 -04:00
} catch {
2025-09-15 08:49:07 -04:00
throw new Error ( `File not found: ${ filePath } ` )
2025-09-13 12:20:50 -04:00
}
2025-09-15 08:49:07 -04:00
const ext = path . extname ( expandedPath ). toLowerCase ()
2025-09-13 12:20:50 -04:00
const mimeTypes = {
'.bmp' : 'image/bmp' ,
'.doc' : 'application/msword' ,
2025-09-15 08:49:07 -04:00
'.docx' :
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ,
'.gif' : 'image/gif' ,
'.jpeg' : 'image/jpeg' ,
'.jpg' : 'image/jpeg' ,
2025-09-13 12:20:50 -04:00
'.odt' : 'application/vnd.oasis.opendocument.text' ,
2025-09-15 08:49:07 -04:00
'.pdf' : 'application/pdf' ,
'.png' : 'image/png' ,
2025-09-13 12:20:50 -04:00
'.rtf' : 'application/rtf' ,
2025-09-15 08:49:07 -04:00
'.txt' : 'text/plain' ,
'.webp' : 'image/webp' ,
2025-09-13 12:20:50 -04:00
// Video formats
'.3gp' : 'video/3gpp' ,
2025-09-15 08:49:07 -04:00
'.avi' : 'video/x-msvideo' ,
'.flv' : 'video/x-flv' ,
2025-09-13 12:20:50 -04:00
'.m4v' : 'video/x-m4v' ,
2025-09-15 08:49:07 -04:00
'.mkv' : 'video/x-matroska' ,
'.mov' : 'video/quicktime' ,
'.mp4' : 'video/mp4' ,
'.webm' : 'video/webm' ,
'.wmv' : 'video/x-ms-wmv' ,
}
2025-09-13 12:20:50 -04:00
const uploadResult = await fileManager . uploadFile ( expandedPath , {
mimeType : mimeTypes [ ext ] || 'application/octet-stream' ,
2025-09-15 08:49:07 -04:00
})
2025-09-13 12:20:50 -04:00
2025-09-15 08:49:07 -04:00
let file = uploadResult . file
2025-09-13 12:20:50 -04:00
// For video files, poll until the file is in ACTIVE state
2025-09-15 08:49:07 -04:00
const videoExtensions = [
'.mp4' ,
'.avi' ,
'.mov' ,
'.webm' ,
'.mkv' ,
'.wmv' ,
'.flv' ,
'.3gp' ,
'.m4v' ,
]
2025-09-13 12:20:50 -04:00
if ( videoExtensions . includes ( ext )) {
2025-09-15 08:49:07 -04:00
console . error (
`Waiting for video file to process: ${ path . basename ( filePath ) } ` ,
)
let attempts = 0
const maxAttempts = 60 // Max 5 minutes (60 * 5 seconds)
2025-09-13 12:20:50 -04:00
while ( file . state !== 'ACTIVE' && attempts < maxAttempts ) {
2025-09-15 08:49:07 -04:00
await sleep ( 5000 ) // Wait 5 seconds
attempts ++
2025-09-13 12:20:50 -04:00
// Get updated file status
2025-09-15 08:49:07 -04:00
const fileStatus = await fileManager . getFile ( file . name )
file = fileStatus
2025-09-13 12:20:50 -04:00
2025-09-15 08:49:07 -04:00
console . error (
`Video processing status: ${ file . state } (attempt ${ attempts } / ${ maxAttempts } )` ,
)
2025-09-13 12:20:50 -04:00
if ( file . state === 'FAILED' ) {
2025-09-15 08:49:07 -04:00
throw new Error ( `Video processing failed for: ${ filePath } ` )
2025-09-13 12:20:50 -04:00
}
}
if ( file . state !== 'ACTIVE' ) {
2025-09-15 08:49:07 -04:00
throw new Error (
`Video processing timeout for: ${ filePath } . File state: ${ file . state } ` ,
)
2025-09-13 12:20:50 -04:00
}
2025-09-15 08:49:07 -04:00
console . error ( 'Video file is ready for analysis' )
2025-09-13 12:20:50 -04:00
}
2025-09-15 08:49:07 -04:00
return file
2025-09-13 12:20:50 -04:00
}
// Tool handlers
2025-09-15 08:49:07 -04:00
async function analyzeDocument ( args ) {
const documentPath = args . document_path
const prompt =
args . prompt || 'Analyze this document and provide a comprehensive summary'
const file = await uploadFile ( documentPath )
2025-09-13 12:20:50 -04:00
const result = await model . generateContent ([
prompt ,
2025-09-15 08:49:07 -04:00
{ fileData : { fileUri : file . uri , mimeType : file . mimeType } },
])
return result . response . text ()
}
async function analyzeImage ( args ) {
const imagePath = args . image_path
const prompt = args . prompt || 'Describe this image in detail'
const file = await uploadFile ( imagePath )
const result = await model . generateContent ([
prompt ,
{ fileData : { fileUri : file . uri , mimeType : file . mimeType } },
])
return result . response . text ()
2025-09-13 12:20:50 -04:00
}
async function analyzeMultiple ( args ) {
2025-09-15 08:49:07 -04:00
const imagePaths = args . image_paths
const prompt = args . prompt || 'Analyze these images'
2025-09-13 12:20:50 -04:00
2025-09-15 08:49:07 -04:00
const content = [ prompt ]
for ( const imagePath of imagePaths ) {
const file = await uploadFile ( imagePath )
content . push ({ fileData : { fileUri : file . uri , mimeType : file . mimeType } })
}
const result = await model . generateContent ( content )
return result . response . text ()
2025-09-13 12:20:50 -04:00
}
async function compareImages ( args ) {
2025-09-15 08:49:07 -04:00
const image1Path = args . image1_path
const image2Path = args . image2_path
const focus = args . focus || 'differences'
2025-09-13 12:20:50 -04:00
const prompts = {
2025-09-15 08:49:07 -04:00
changes : 'Describe what has changed between the first and second image.' ,
differences :
'Compare these two images and describe all the differences you can find.' ,
similarities :
'Compare these two images and describe what they have in common.' ,
}
2025-09-13 12:20:50 -04:00
const [ file1 , file2 ] = await Promise . all ([
uploadFile ( image1Path ),
2025-09-15 08:49:07 -04:00
uploadFile ( image2Path ),
])
2025-09-13 12:20:50 -04:00
const result = await model . generateContent ([
prompts [ focus ] || prompts . differences ,
2025-09-15 08:49:07 -04:00
{ fileData : { fileUri : file1 . uri , mimeType : file1 . mimeType } },
{ fileData : { fileUri : file2 . uri , mimeType : file2 . mimeType } },
])
return result . response . text ()
}
async function extractText ( args ) {
const imagePath = args . image_path
const format = args . format || 'plain'
const prompts = {
markdown :
'Extract all text from this image and format it in markdown, preserving structure.' ,
plain :
'Extract and transcribe all text from this image. Return only the text, nothing else.' ,
structured :
'Extract all text from this image and organize it with clear sections and structure.' ,
}
const file = await uploadFile ( imagePath )
const result = await model . generateContent ([
prompts [ format ] || prompts . plain ,
{ fileData : { fileUri : file . uri , mimeType : file . mimeType } },
])
return result . response . text ()
2025-09-13 12:20:50 -04:00
}
async function suggestFilename ( args ) {
2025-09-15 08:49:07 -04:00
const imagePath = args . image_path
const maxLength = args . max_length || 60
const includeDate = args . include_date || false
2025-09-13 12:20:50 -04:00
const prompt = `Analyze this image and suggest a descriptive filename for it.
Requirements:
- Maximum ${ maxLength } characters (not including extension)
- Use title case with spaces (will be converted to hyphens)
- Be specific and descriptive about the content
- ${ includeDate ? 'Include YYYY-MM-DD prefix if a date is visible in the image' : 'Do not include date prefix' }
- Focus on the main subject or purpose of the image
- For screenshots: include the application or website name
- For diagrams: include the type and subject
- For photos: include the subject and context
2025-09-15 08:49:07 -04:00
- Return ONLY the filename suggestion, no explanation or extension`
const file = await uploadFile ( imagePath )
2025-09-13 12:20:50 -04:00
const result = await model . generateContent ([
prompt ,
2025-09-15 08:49:07 -04:00
{ fileData : { fileUri : file . uri , mimeType : file . mimeType } },
])
2025-09-13 12:20:50 -04:00
// Clean up the suggestion and format it
2025-09-15 08:49:07 -04:00
let suggestion = result . response . text (). trim ()
2025-09-13 12:20:50 -04:00
// Remove any file extension if accidentally included
2025-09-15 08:49:07 -04:00
suggestion = suggestion . replace ( /\.(png|jpg|jpeg|gif|webp|pdf)$/i , '' )
2025-09-13 12:20:50 -04:00
// Replace spaces with hyphens
2025-09-15 08:49:07 -04:00
suggestion = suggestion . replace ( /\s+/g , ' ' ). replace ( / /g , ' - ' )
2025-09-13 12:20:50 -04:00
// Ensure it doesn't exceed max length
if ( suggestion . length > maxLength ) {
2025-09-15 08:49:07 -04:00
suggestion = suggestion . substring ( 0 , maxLength ). replace ( / - $/ , '' )
2025-09-13 12:20:50 -04:00
}
2025-09-15 08:49:07 -04:00
return suggestion
2025-09-13 12:20:50 -04:00
}
// Analyze video files or YouTube URLs
async function analyzeVideo ( args ) {
2025-09-15 08:49:07 -04:00
const videoPath = args . video_path
const youtubeUrl = args . youtube_url
const prompt =
args . prompt ||
'Summarize this video in detail, including key moments and any text or speech content'
2025-09-13 12:20:50 -04:00
if ( ! videoPath && ! youtubeUrl ) {
2025-09-15 08:49:07 -04:00
throw new Error ( 'Either video_path or youtube_url is required' )
2025-09-13 12:20:50 -04:00
}
if ( videoPath && youtubeUrl ) {
2025-09-15 08:49:07 -04:00
throw new Error ( 'Please provide either video_path or youtube_url, not both' )
2025-09-13 12:20:50 -04:00
}
2025-09-15 08:49:07 -04:00
let fileData
2025-09-13 12:20:50 -04:00
if ( youtubeUrl ) {
// YouTube URLs can be passed directly to the API
2025-09-15 08:49:07 -04:00
fileData = { fileUri : youtubeUrl }
2025-09-13 12:20:50 -04:00
} else {
// Upload local video file
2025-09-15 08:49:07 -04:00
const file = await uploadFile ( videoPath )
fileData = { fileUri : file . uri , mimeType : file . mimeType }
2025-09-13 12:20:50 -04:00
}
2025-09-15 08:49:07 -04:00
const result = await model . generateContent ([ prompt , { fileData }])
2025-09-13 12:20:50 -04:00
2025-09-15 08:49:07 -04:00
return result . response . text ()
2025-09-13 12:20:50 -04:00
}
// Create MCP server
const server = new Server (
2025-09-15 08:49:07 -04:00
{ name : 'gemini-vision' , version : '1.0.0' },
{ capabilities : { tools : {} } },
)
2025-09-13 12:20:50 -04:00
// List available tools
server . setRequestHandler ( ListToolsRequestSchema , async () => ({
tools : [
{
2025-09-15 08:49:07 -04:00
description :
'Analyze an image - transcribe text, describe content, or answer questions' ,
2025-09-13 12:20:50 -04:00
inputSchema : {
properties : {
2025-09-15 08:49:07 -04:00
image_path : { description : 'Path to the image file' , type : 'string' },
prompt : {
default : 'Describe this image' ,
description : 'What to do with the image' ,
type : 'string' ,
},
2025-09-13 12:20:50 -04:00
},
2025-09-15 08:49:07 -04:00
required : [ 'image_path' ],
type : 'object' ,
},
name : 'analyze_image' ,
2025-09-13 12:20:50 -04:00
},
{
2025-09-15 08:49:07 -04:00
description : 'Analyze multiple images at once' ,
2025-09-13 12:20:50 -04:00
inputSchema : {
properties : {
2025-09-15 08:49:07 -04:00
image_paths : {
description : 'List of image paths' ,
items : { type : 'string' },
type : 'array' ,
},
prompt : {
default : 'Analyze these images' ,
description : 'What to do with the images' ,
type : 'string' ,
},
2025-09-13 12:20:50 -04:00
},
2025-09-15 08:49:07 -04:00
required : [ 'image_paths' ],
type : 'object' ,
},
name : 'analyze_multiple' ,
2025-09-13 12:20:50 -04:00
},
{
2025-09-15 08:49:07 -04:00
description : 'Extract and transcribe all text from an image (OCR)' ,
2025-09-13 12:20:50 -04:00
inputSchema : {
properties : {
2025-09-15 08:49:07 -04:00
format : {
default : 'plain' ,
enum : [ 'plain' , 'markdown' , 'structured' ],
type : 'string' ,
},
image_path : { description : 'Path to the image file' , type : 'string' },
2025-09-13 12:20:50 -04:00
},
2025-09-15 08:49:07 -04:00
required : [ 'image_path' ],
type : 'object' ,
},
name : 'extract_text' ,
2025-09-13 12:20:50 -04:00
},
{
2025-09-15 08:49:07 -04:00
description :
'Compare two images and describe differences or similarities' ,
2025-09-13 12:20:50 -04:00
inputSchema : {
properties : {
2025-09-15 08:49:07 -04:00
focus : {
default : 'differences' ,
enum : [ 'differences' , 'similarities' , 'changes' ],
type : 'string' ,
},
image1_path : { description : 'Path to first image' , type : 'string' },
image2_path : { description : 'Path to second image' , type : 'string' },
2025-09-13 12:20:50 -04:00
},
2025-09-15 08:49:07 -04:00
required : [ 'image1_path' , 'image2_path' ],
type : 'object' ,
},
name : 'compare_images' ,
2025-09-13 12:20:50 -04:00
},
{
2025-09-15 08:49:07 -04:00
description :
'Analyze an image and suggest a descriptive filename (without extension)' ,
2025-09-13 12:20:50 -04:00
inputSchema : {
properties : {
2025-09-15 08:49:07 -04:00
image_path : { description : 'Path to the image file' , type : 'string' },
include_date : {
default : false ,
description : 'Include date prefix in suggestion' ,
type : 'boolean' ,
},
max_length : {
default : 60 ,
description : 'Maximum filename length' ,
type : 'number' ,
},
2025-09-13 12:20:50 -04:00
},
2025-09-15 08:49:07 -04:00
required : [ 'image_path' ],
type : 'object' ,
},
name : 'suggest_image_filename' ,
2025-09-13 12:20:50 -04:00
},
{
2025-09-15 08:49:07 -04:00
description :
'Analyze video files or YouTube URLs - extract content, summarize, transcribe speech, identify objects/text. Provide either video_path OR youtube_url' ,
2025-09-13 12:20:50 -04:00
inputSchema : {
properties : {
2025-09-15 08:49:07 -04:00
prompt : {
default : 'Summarize this video in detail' ,
description : 'What to analyze in the video' ,
type : 'string' ,
},
video_path : {
description : 'Path to local video file (MP4, AVI, MOV, etc.)' ,
type : 'string' ,
},
youtube_url : {
description :
'YouTube video URL (e.g., https://www.youtube.com/watch?v=...)' ,
type : 'string' ,
},
2025-09-13 12:20:50 -04:00
},
2025-09-15 08:49:07 -04:00
required : [],
type : 'object' ,
},
name : 'analyze_video' ,
2025-09-13 12:20:50 -04:00
},
{
2025-09-15 08:49:07 -04:00
description :
'Analyze a PDF or document with custom prompts - extract specific information, find mentions of topics, summarize sections, etc.' ,
2025-09-13 12:20:50 -04:00
inputSchema : {
properties : {
2025-09-15 08:49:07 -04:00
document_path : {
description :
'Path to the document file (PDF, DOC, DOCX, ODT, RTF, TXT)' ,
type : 'string' ,
},
prompt : {
default :
'Analyze this document and provide a comprehensive summary' ,
description : 'What to analyze or extract from the document' ,
type : 'string' ,
},
2025-09-13 12:20:50 -04:00
},
2025-09-15 08:49:07 -04:00
required : [ 'document_path' ],
type : 'object' ,
},
name : 'analyze_document' ,
},
],
}))
2025-09-13 12:20:50 -04:00
// Handle tool calls
server . setRequestHandler ( CallToolRequestSchema , async ( request ) => {
2025-09-15 08:49:07 -04:00
const { arguments : args , name } = request . params
2025-09-13 12:20:50 -04:00
try {
2025-09-15 08:49:07 -04:00
let result
2025-09-13 12:20:50 -04:00
switch ( name ) {
2025-09-15 08:49:07 -04:00
case 'analyze_document' :
result = await analyzeDocument ( args )
break
case 'analyze_image' :
result = await analyzeImage ( args )
break
case 'analyze_multiple' :
result = await analyzeMultiple ( args )
break
case 'analyze_video' :
result = await analyzeVideo ( args )
break
case 'compare_images' :
result = await compareImages ( args )
break
case 'extract_text' :
result = await extractText ( args )
break
case 'suggest_image_filename' :
result = await suggestFilename ( args )
break
2025-09-13 12:20:50 -04:00
default :
2025-09-15 08:49:07 -04:00
throw new Error ( `Unknown tool: ${ name } ` )
2025-09-13 12:20:50 -04:00
}
2025-09-15 08:49:07 -04:00
2025-09-13 12:20:50 -04:00
return {
2025-09-15 08:49:07 -04:00
content : [{ text : result , type : 'text' }],
}
2025-09-13 12:20:50 -04:00
} catch ( error ) {
2025-09-15 08:49:07 -04:00
throw new Error ( `Tool execution failed: ${ error . message } ` )
2025-09-13 12:20:50 -04:00
}
2025-09-15 08:49:07 -04:00
})
2025-09-13 12:20:50 -04:00
// Start server
async function main () {
2025-09-15 08:49:07 -04:00
const transport = new StdioServerTransport ()
await server . connect ( transport )
console . error ( '🚀 Gemini Vision MCP Server running' )
2025-09-13 12:20:50 -04:00
}
2025-09-15 08:49:07 -04:00
main (). catch ( console . error )