fix: resolve lonely if ESLint warning

This commit is contained in:
Noah Brier
2025-09-15 08:49:07 -04:00
parent 61d14875d5
commit 3909ab476c
29 changed files with 1300 additions and 790 deletions
@@ -5,6 +5,7 @@
## Prerequisites Check
Run these commands to verify you have everything needed:
```bash
node --version # Should be v22+
pnpm --version # Should be installed
@@ -12,19 +13,22 @@ claude --version # Claude Code should be installed
```
If any are missing:
- Node.js: Install from [nodejs.org](https://nodejs.org/) (v22+)
- pnpm: `npm install -g pnpm`
- Claude Code: Download from [claude.ai/code](https://claude.ai/code)
## Step 1: Get Your Gemini API Key
1. Go to [https://aistudio.google.com/apikey](https://aistudio.google.com/apikey)
1. Go to
[https://aistudio.google.com/apikey](https://aistudio.google.com/apikey)
2. Click "Create API Key"
3. Copy the key (starts with `AIzaSy...`)
## Step 2: Set Up Environment Variable
### For Linux/macOS with Bash:
```bash
echo 'export GEMINI_API_KEY="your-actual-api-key-here"' >> ~/.bashrc
source ~/.bashrc
@@ -32,6 +36,7 @@ echo $GEMINI_API_KEY # Verify it shows your key
```
### For Linux/macOS with Zsh:
```bash
echo 'export GEMINI_API_KEY="your-actual-api-key-here"' >> ~/.zshrc
source ~/.zshrc
@@ -39,6 +44,7 @@ echo $GEMINI_API_KEY # Verify it shows your key
```
### For Windows PowerShell:
```powershell
[System.Environment]::SetEnvironmentVariable('GEMINI_API_KEY', 'your-key-here', 'User')
# Restart PowerShell
@@ -50,11 +56,13 @@ $env:GEMINI_API_KEY # Verify it shows your key
**⚠️ CRITICAL: This step MUST be done before adding the MCP server!**
Navigate to your Obsidian vault:
```bash
cd ~/dev/02_Areas/Obsidian # Or wherever your vault is
```
Install the required dependencies:
```bash
# Install npm packages (REQUIRED - do this first!)
pnpm install
@@ -65,9 +73,12 @@ pnpm install
# - Other dependencies from package.json
```
**Common Error Fix**: If you see `Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@modelcontextprotocol/sdk'`, you forgot to run `pnpm install`!
**Common Error Fix**: If you see
`Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@modelcontextprotocol/sdk'`,
you forgot to run `pnpm install`!
**To hide node_modules from Obsidian** (optional but recommended):
1. Open Obsidian
2. Go to Settings → Files & Links → Excluded files
3. Click "Manage"
@@ -79,18 +90,21 @@ This keeps your vault clean while using standard Node.js module resolution.
## Step 4: Register the MCP Server
**For project-scoped installation (recommended for team use):**
```bash
# Add server to project (creates .mcp.json file)
claude mcp add --scope project gemini-vision node .claude/mcp-servers/gemini-vision.mjs
```
**For user-scoped installation (personal use across all projects):**
```bash
# Add server to your user config
claude mcp add --scope user gemini-vision node .claude/mcp-servers/gemini-vision.mjs
```
After adding, you'll need to edit the `.mcp.json` file to add your API key:
```json
{
"mcpServers": {
@@ -107,6 +121,7 @@ After adding, you'll need to edit the `.mcp.json` file to add your API key:
```
**IMPORTANT**:
- The command must be run from the Obsidian vault root directory
- You MUST have run `pnpm install` first
- The `.mcp.json` file is gitignored for security
@@ -114,15 +129,16 @@ After adding, you'll need to edit the `.mcp.json` file to add your API key:
## Step 5: Verify It's Working
1. **Open a NEW Claude Code window** (critical - must be new):
```bash
cd ~/dev/Obsidian
claude
```
2. **Check the server is connected**:
Type `/mcp` in Claude
2. **Check the server is connected**: Type `/mcp` in Claude
You should see:
```
gemini-vision ✔ connected
```
@@ -137,11 +153,13 @@ After adding, you'll need to edit the `.mcp.json` file to add your API key:
### "gemini-vision failed" or not showing in /mcp
1. **MOST COMMON ISSUE - Dependencies not installed**:
```bash
# If you see: Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@modelcontextprotocol/sdk'
# Run this:
pnpm install
```
Then reconnect the MCP server in Claude Code.
2. **Check API key is configured**:
@@ -150,21 +168,24 @@ After adding, you'll need to edit the `.mcp.json` file to add your API key:
- The key should be in the format: `"GEMINI_API_KEY": "AIzaSy..."`
3. **Test server can run directly**:
```bash
export GEMINI_API_KEY="your-api-key-here"
node .claude/mcp-servers/gemini-vision.mjs
```
Should show: "🚀 Gemini Vision MCP Server running"
Press Ctrl+C to exit.
Should show: "🚀 Gemini Vision MCP Server running" Press Ctrl+C to exit.
4. **Re-add the server (for project scope)**:
```bash
claude mcp remove gemini-vision --scope project
claude mcp add --scope project gemini-vision node .claude/mcp-servers/gemini-vision.mjs
# Then edit .mcp.json to add your API key
```
4. **Check logs**:
5. **Check logs**:
```bash
# Find log directory
ls ~/Library/Caches/claude-cli-nodejs/*/mcp-logs-gemini-vision/
@@ -178,12 +199,15 @@ After adding, you'll need to edit the `.mcp.json` file to add your API key:
### "Cannot find module" errors
1. **Verify package.json exists**:
```bash
cat package.json
```
Should show @google/generative-ai and @modelcontextprotocol/sdk
2. **Reinstall dependencies**:
```bash
rm -rf node_modules pnpm-lock.yaml
pnpm install
@@ -197,9 +221,11 @@ After adding, you'll need to edit the `.mcp.json` file to add your API key:
### Server runs but tools don't work
1. **Test API key directly**:
```bash
curl "https://generativelanguage.googleapis.com/v1beta/models?key=$GEMINI_API_KEY"
```
Should return a list of models, not an error.
2. **Check file paths**:
@@ -211,6 +237,7 @@ After adding, you'll need to edit the `.mcp.json` file to add your API key:
Once working, you can use these in Claude:
### Image Analysis
```
# Analyze an image
Use gemini-vision to analyze 05 Attachments/screenshot.png
@@ -229,6 +256,7 @@ Use gemini-vision to analyze multiple: image1.png, image2.png, image3.png
```
### Video Analysis (NEW!)
```
# Analyze a local video file
Use gemini-vision to analyze video 05 Attachments/video.mp4
@@ -240,14 +268,15 @@ Use gemini-vision to analyze YouTube video https://www.youtube.com/watch?v=VIDEO
Use gemini-vision to analyze video file.mp4 and extract all visible text
```
**Note:** Video processing may take 30-60 seconds as files need to reach ACTIVE state before analysis. The server will automatically wait and show progress updates.
**Note:** Video processing may take 30-60 seconds as files need to reach ACTIVE
state before analysis. The server will automatically wait and show progress
updates.
### Supported Formats
**Images:** JPG, JPEG, PNG, GIF, BMP, WebP
**Videos:** MP4, AVI, MOV, WebM, MKV, WMV, FLV, 3GP, M4V
**Documents:** PDF, TXT, DOC, DOCX, ODT, RTF
**Special:** YouTube URLs (direct support without download)
**Images:** JPG, JPEG, PNG, GIF, BMP, WebP **Videos:** MP4, AVI, MOV, WebM, MKV,
WMV, FLV, 3GP, M4V **Documents:** PDF, TXT, DOC, DOCX, ODT, RTF **Special:**
YouTube URLs (direct support without download)
## Quick Reinstall (If Already Set Up Once)
@@ -282,4 +311,4 @@ Then open a new Claude window and test.
---
*Last tested: September 2025*
_Last tested: September 2025_
+14 -4
View File
@@ -4,7 +4,8 @@ Model Context Protocol servers extend Claude Code's capabilities.
## Gemini Vision MCP
Adds powerful image and document analysis capabilities using Google's Gemini model.
Adds powerful image and document analysis capabilities using Google's Gemini
model.
### Features
@@ -21,15 +22,17 @@ Adds powerful image and document analysis capabilities using Google's Gemini mod
- Create a free API key
2. **Add to Environment**
```bash
# Add to ~/.zshrc or ~/.bashrc
export GEMINI_API_KEY='your-key-here'
# Reload shell
source ~/.zshrc
```
3. **Install Dependencies**
```bash
pnpm install
```
@@ -53,24 +56,28 @@ Once configured, these commands become available in Claude Code:
### Usage Examples
**Analyze Screenshot**
```
Analyze the image at 05_Attachments/screenshot.png
and tell me what it contains.
```
**Process Multiple Images**
```
Compare all images in 05_Attachments/Organized/
and identify common themes.
```
**Extract Text**
```
Extract all text from the PDF at
Extract all text from the PDF at
05_Attachments/document.pdf
```
**Rename Images**
```
Suggest better names for all images
in 05_Attachments/ based on their content.
@@ -79,14 +86,17 @@ in 05_Attachments/ based on their content.
### Troubleshooting
**"GEMINI_API_KEY not found"**
- Make sure you've added the key to your shell profile
- Restart your terminal and Claude Code
**"File not found"**
- Use absolute paths or paths relative to vault root
- Check file permissions
**Rate Limits**
- Free tier: 15 requests per minute
- Consider upgrading for heavy usage
@@ -101,4 +111,4 @@ in 05_Attachments/ based on their content.
- [MCP Documentation](https://modelcontextprotocol.io)
- [Gemini API Docs](https://ai.google.dev)
- [Claude Code MCP Guide](https://claude.ai/docs/mcp)
- [Claude Code MCP Guide](https://claude.ai/docs/mcp)
+340 -263
View File
@@ -1,208 +1,246 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListToolsRequestSchema,
CallToolRequestSchema
} from "@modelcontextprotocol/sdk/types.js";
import { GoogleGenerativeAI } from "@google/generative-ai";
import { GoogleAIFileManager } from "@google/generative-ai/server";
import fs from "fs/promises";
import path from "path";
import os from "os";
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'
const apiKey = process.env.GEMINI_API_KEY;
const apiKey = process.env.GEMINI_API_KEY
if (!apiKey) {
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);
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)
}
const genAI = new GoogleGenerativeAI(apiKey);
const fileManager = new GoogleAIFileManager(apiKey);
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" });
const genAI = new GoogleGenerativeAI(apiKey)
const fileManager = new GoogleAIFileManager(apiKey)
const model = genAI.getGenerativeModel({ model: 'gemini-2.5-flash' })
// Expand home directory in paths
function expandPath(filepath) {
if (filepath.startsWith("~/")) {
return path.join(os.homedir(), filepath.slice(2));
if (filepath.startsWith('~/')) {
return path.join(os.homedir(), filepath.slice(2))
}
return filepath;
return filepath
}
// Helper function to wait/sleep
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
return new Promise((resolve) => setTimeout(resolve, ms))
}
// Upload file to Gemini
async function uploadFile(filePath) {
const expandedPath = expandPath(filePath);
const expandedPath = expandPath(filePath)
try {
await fs.access(expandedPath);
await fs.access(expandedPath)
} catch {
throw new Error(`File not found: ${filePath}`);
throw new Error(`File not found: ${filePath}`)
}
const ext = path.extname(expandedPath).toLowerCase();
const ext = path.extname(expandedPath).toLowerCase()
const mimeTypes = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.bmp': 'image/bmp',
'.webp': 'image/webp',
'.pdf': 'application/pdf',
'.txt': 'text/plain',
'.doc': 'application/msword',
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'.docx':
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'.gif': 'image/gif',
'.jpeg': 'image/jpeg',
'.jpg': 'image/jpeg',
'.odt': 'application/vnd.oasis.opendocument.text',
'.pdf': 'application/pdf',
'.png': 'image/png',
'.rtf': 'application/rtf',
'.txt': 'text/plain',
'.webp': 'image/webp',
// Video formats
'.mp4': 'video/mp4',
'.avi': 'video/x-msvideo',
'.mov': 'video/quicktime',
'.webm': 'video/webm',
'.mkv': 'video/x-matroska',
'.wmv': 'video/x-ms-wmv',
'.flv': 'video/x-flv',
'.3gp': 'video/3gpp',
'.avi': 'video/x-msvideo',
'.flv': 'video/x-flv',
'.m4v': 'video/x-m4v',
};
'.mkv': 'video/x-matroska',
'.mov': 'video/quicktime',
'.mp4': 'video/mp4',
'.webm': 'video/webm',
'.wmv': 'video/x-ms-wmv',
}
const uploadResult = await fileManager.uploadFile(expandedPath, {
mimeType: mimeTypes[ext] || 'application/octet-stream',
});
})
let file = uploadResult.file;
let file = uploadResult.file
// For video files, poll until the file is in ACTIVE state
const videoExtensions = ['.mp4', '.avi', '.mov', '.webm', '.mkv', '.wmv', '.flv', '.3gp', '.m4v'];
const videoExtensions = [
'.mp4',
'.avi',
'.mov',
'.webm',
'.mkv',
'.wmv',
'.flv',
'.3gp',
'.m4v',
]
if (videoExtensions.includes(ext)) {
console.error(`Waiting for video file to process: ${path.basename(filePath)}`);
let attempts = 0;
const maxAttempts = 60; // Max 5 minutes (60 * 5 seconds)
console.error(
`Waiting for video file to process: ${path.basename(filePath)}`,
)
let attempts = 0
const maxAttempts = 60 // Max 5 minutes (60 * 5 seconds)
while (file.state !== 'ACTIVE' && attempts < maxAttempts) {
await sleep(5000); // Wait 5 seconds
attempts++;
await sleep(5000) // Wait 5 seconds
attempts++
// Get updated file status
const fileStatus = await fileManager.getFile(file.name);
file = fileStatus;
const fileStatus = await fileManager.getFile(file.name)
file = fileStatus
console.error(`Video processing status: ${file.state} (attempt ${attempts}/${maxAttempts})`);
console.error(
`Video processing status: ${file.state} (attempt ${attempts}/${maxAttempts})`,
)
if (file.state === 'FAILED') {
throw new Error(`Video processing failed for: ${filePath}`);
throw new Error(`Video processing failed for: ${filePath}`)
}
}
if (file.state !== 'ACTIVE') {
throw new Error(`Video processing timeout for: ${filePath}. File state: ${file.state}`);
throw new Error(
`Video processing timeout for: ${filePath}. File state: ${file.state}`,
)
}
console.error('Video file is ready for analysis');
console.error('Video file is ready for analysis')
}
return file;
return file
}
// Tool handlers
async function analyzeImage(args) {
const imagePath = args.image_path;
const prompt = args.prompt || "Describe this image in detail";
const file = await uploadFile(imagePath);
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)
const result = await model.generateContent([
prompt,
{ fileData: { fileUri: file.uri, mimeType: file.mimeType }}
]);
return result.response.text();
{ 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()
}
async function analyzeMultiple(args) {
const imagePaths = args.image_paths;
const prompt = args.prompt || "Analyze these images";
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();
}
const imagePaths = args.image_paths
const prompt = args.prompt || 'Analyze these images'
async function extractText(args) {
const imagePath = args.image_path;
const format = args.format || "plain";
const prompts = {
plain: "Extract and transcribe all text from this image. Return only the text, nothing else.",
markdown: "Extract all text from this image and format it in markdown, preserving structure.",
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();
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()
}
async function compareImages(args) {
const image1Path = args.image1_path;
const image2Path = args.image2_path;
const focus = args.focus || "differences";
const image1Path = args.image1_path
const image2Path = args.image2_path
const focus = args.focus || 'differences'
const prompts = {
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.",
changes: "Describe what has changed between the first and second image."
};
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.',
}
const [file1, file2] = await Promise.all([
uploadFile(image1Path),
uploadFile(image2Path)
]);
uploadFile(image2Path),
])
const result = await model.generateContent([
prompts[focus] || prompts.differences,
{ fileData: { fileUri: file1.uri, mimeType: file1.mimeType }},
{ fileData: { fileUri: file2.uri, mimeType: file2.mimeType }}
]);
return result.response.text();
{ 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()
}
async function suggestFilename(args) {
const imagePath = args.image_path;
const maxLength = args.max_length || 60;
const includeDate = args.include_date || false;
const imagePath = args.image_path
const maxLength = args.max_length || 60
const includeDate = args.include_date || false
const prompt = `Analyze this image and suggest a descriptive filename for it.
Requirements:
- Maximum ${maxLength} characters (not including extension)
@@ -213,218 +251,257 @@ async function suggestFilename(args) {
- For screenshots: include the application or website name
- For diagrams: include the type and subject
- For photos: include the subject and context
- Return ONLY the filename suggestion, no explanation or extension`;
const file = await uploadFile(imagePath);
- Return ONLY the filename suggestion, no explanation or extension`
const file = await uploadFile(imagePath)
const result = await model.generateContent([
prompt,
{ fileData: { fileUri: file.uri, mimeType: file.mimeType }}
]);
{ fileData: { fileUri: file.uri, mimeType: file.mimeType } },
])
// Clean up the suggestion and format it
let suggestion = result.response.text().trim();
let suggestion = result.response.text().trim()
// Remove any file extension if accidentally included
suggestion = suggestion.replace(/\.(png|jpg|jpeg|gif|webp|pdf)$/i, '');
suggestion = suggestion.replace(/\.(png|jpg|jpeg|gif|webp|pdf)$/i, '')
// Replace spaces with hyphens
suggestion = suggestion.replace(/\s+/g, ' ').replace(/ /g, ' - ');
suggestion = suggestion.replace(/\s+/g, ' ').replace(/ /g, ' - ')
// Ensure it doesn't exceed max length
if (suggestion.length > maxLength) {
suggestion = suggestion.substring(0, maxLength).replace(/ - $/, '');
suggestion = suggestion.substring(0, maxLength).replace(/ - $/, '')
}
return suggestion;
}
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);
const result = await model.generateContent([
prompt,
{ fileData: { fileUri: file.uri, mimeType: file.mimeType }}
]);
return result.response.text();
return suggestion
}
// Analyze video files or YouTube URLs
async function analyzeVideo(args) {
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";
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'
if (!videoPath && !youtubeUrl) {
throw new Error("Either video_path or youtube_url is required");
throw new Error('Either video_path or youtube_url is required')
}
if (videoPath && youtubeUrl) {
throw new Error("Please provide either video_path or youtube_url, not both");
throw new Error('Please provide either video_path or youtube_url, not both')
}
let fileData;
let fileData
if (youtubeUrl) {
// YouTube URLs can be passed directly to the API
fileData = { fileUri: youtubeUrl };
fileData = { fileUri: youtubeUrl }
} else {
// Upload local video file
const file = await uploadFile(videoPath);
fileData = { fileUri: file.uri, mimeType: file.mimeType };
const file = await uploadFile(videoPath)
fileData = { fileUri: file.uri, mimeType: file.mimeType }
}
const result = await model.generateContent([
prompt,
{ fileData }
]);
const result = await model.generateContent([prompt, { fileData }])
return result.response.text();
return result.response.text()
}
// Create MCP server
const server = new Server(
{ name: "gemini-vision", version: "1.0.0" },
{ capabilities: { tools: {} }}
);
{ name: 'gemini-vision', version: '1.0.0' },
{ capabilities: { tools: {} } },
)
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "analyze_image",
description: "Analyze an image - transcribe text, describe content, or answer questions",
description:
'Analyze an image - transcribe text, describe content, or answer questions',
inputSchema: {
type: "object",
properties: {
image_path: { type: "string", description: "Path to the image file" },
prompt: { type: "string", description: "What to do with the image", default: "Describe this image" }
image_path: { description: 'Path to the image file', type: 'string' },
prompt: {
default: 'Describe this image',
description: 'What to do with the image',
type: 'string',
},
},
required: ["image_path"]
}
required: ['image_path'],
type: 'object',
},
name: 'analyze_image',
},
{
name: "analyze_multiple",
description: "Analyze multiple images at once",
description: 'Analyze multiple images at once',
inputSchema: {
type: "object",
properties: {
image_paths: { type: "array", items: { type: "string" }, description: "List of image paths" },
prompt: { type: "string", description: "What to do with the images", default: "Analyze these images" }
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',
},
},
required: ["image_paths"]
}
required: ['image_paths'],
type: 'object',
},
name: 'analyze_multiple',
},
{
name: "extract_text",
description: "Extract and transcribe all text from an image (OCR)",
description: 'Extract and transcribe all text from an image (OCR)',
inputSchema: {
type: "object",
properties: {
image_path: { type: "string", description: "Path to the image file" },
format: { type: "string", enum: ["plain", "markdown", "structured"], default: "plain" }
format: {
default: 'plain',
enum: ['plain', 'markdown', 'structured'],
type: 'string',
},
image_path: { description: 'Path to the image file', type: 'string' },
},
required: ["image_path"]
}
required: ['image_path'],
type: 'object',
},
name: 'extract_text',
},
{
name: "compare_images",
description: "Compare two images and describe differences or similarities",
description:
'Compare two images and describe differences or similarities',
inputSchema: {
type: "object",
properties: {
image1_path: { type: "string", description: "Path to first image" },
image2_path: { type: "string", description: "Path to second image" },
focus: { type: "string", enum: ["differences", "similarities", "changes"], default: "differences" }
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' },
},
required: ["image1_path", "image2_path"]
}
required: ['image1_path', 'image2_path'],
type: 'object',
},
name: 'compare_images',
},
{
name: "suggest_image_filename",
description: "Analyze an image and suggest a descriptive filename (without extension)",
description:
'Analyze an image and suggest a descriptive filename (without extension)',
inputSchema: {
type: "object",
properties: {
image_path: { type: "string", description: "Path to the image file" },
max_length: { type: "number", description: "Maximum filename length", default: 60 },
include_date: { type: "boolean", description: "Include date prefix in suggestion", default: false }
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',
},
},
required: ["image_path"]
}
required: ['image_path'],
type: 'object',
},
name: 'suggest_image_filename',
},
{
name: "analyze_video",
description: "Analyze video files or YouTube URLs - extract content, summarize, transcribe speech, identify objects/text. Provide either video_path OR youtube_url",
description:
'Analyze video files or YouTube URLs - extract content, summarize, transcribe speech, identify objects/text. Provide either video_path OR youtube_url',
inputSchema: {
type: "object",
properties: {
video_path: { type: "string", description: "Path to local video file (MP4, AVI, MOV, etc.)" },
youtube_url: { type: "string", description: "YouTube video URL (e.g., https://www.youtube.com/watch?v=...)" },
prompt: { type: "string", description: "What to analyze in the video", default: "Summarize this video in detail" }
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',
},
},
required: []
}
required: [],
type: 'object',
},
name: 'analyze_video',
},
{
name: "analyze_document",
description: "Analyze a PDF or document with custom prompts - extract specific information, find mentions of topics, summarize sections, etc.",
description:
'Analyze a PDF or document with custom prompts - extract specific information, find mentions of topics, summarize sections, etc.',
inputSchema: {
type: "object",
properties: {
document_path: { type: "string", description: "Path to the document file (PDF, DOC, DOCX, ODT, RTF, TXT)" },
prompt: { type: "string", description: "What to analyze or extract from the document", default: "Analyze this document and provide a comprehensive summary" }
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',
},
},
required: ["document_path"]
}
}
]
}));
required: ['document_path'],
type: 'object',
},
name: 'analyze_document',
},
],
}))
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const { arguments: args, name } = request.params
try {
let result;
let result
switch (name) {
case "analyze_image":
result = await analyzeImage(args);
break;
case "analyze_multiple":
result = await analyzeMultiple(args);
break;
case "extract_text":
result = await extractText(args);
break;
case "compare_images":
result = await compareImages(args);
break;
case "suggest_image_filename":
result = await suggestFilename(args);
break;
case "analyze_document":
result = await analyzeDocument(args);
break;
case "analyze_video":
result = await analyzeVideo(args);
break;
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
default:
throw new Error(`Unknown tool: ${name}`);
throw new Error(`Unknown tool: ${name}`)
}
return {
content: [{ type: "text", text: result }]
};
content: [{ text: result, type: 'text' }],
}
} catch (error) {
throw new Error(`Tool execution failed: ${error.message}`);
throw new Error(`Tool execution failed: ${error.message}`)
}
});
})
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("🚀 Gemini Vision MCP Server running");
const transport = new StdioServerTransport()
await server.connect(transport)
console.error('🚀 Gemini Vision MCP Server running')
}
main().catch(console.error);
main().catch(console.error)