Add main tool-handlers and index files
This commit is contained in:
67
src/index.ts
Normal file
67
src/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env node
|
||||
// OpenRouter Multimodal MCP Server
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
|
||||
import { ToolHandlers } from './tool-handlers.js';
|
||||
|
||||
class OpenRouterMultimodalServer {
|
||||
private server: Server;
|
||||
private toolHandlers!: ToolHandlers; // Using definite assignment assertion
|
||||
|
||||
constructor() {
|
||||
// Get API key and default model from environment variables
|
||||
const apiKey = process.env.OPENROUTER_API_KEY;
|
||||
const defaultModel = process.env.OPENROUTER_DEFAULT_MODEL;
|
||||
|
||||
// Check if API key is provided
|
||||
if (!apiKey) {
|
||||
throw new Error('OPENROUTER_API_KEY environment variable is required');
|
||||
}
|
||||
|
||||
// Initialize the server
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'openrouter-multimodal-server',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Set up error handling
|
||||
this.server.onerror = (error) => console.error('[MCP Error]', error);
|
||||
|
||||
// Initialize tool handlers
|
||||
this.toolHandlers = new ToolHandlers(
|
||||
this.server,
|
||||
apiKey,
|
||||
defaultModel
|
||||
);
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
await this.server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
async run() {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
console.error('OpenRouter Multimodal MCP server running on stdio');
|
||||
console.error('Using API key from environment variable');
|
||||
console.error('Note: To use OpenRouter Multimodal, add the API key to your environment variables:');
|
||||
console.error(' OPENROUTER_API_KEY=your-api-key');
|
||||
if (process.env.OPENROUTER_DEFAULT_MODEL) {
|
||||
console.error(` Using default model: ${process.env.OPENROUTER_DEFAULT_MODEL}`);
|
||||
} else {
|
||||
console.error(' No default model set. You will need to specify a model in each request.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const server = new OpenRouterMultimodalServer();
|
||||
server.run().catch(console.error);
|
||||
347
src/tool-handlers.ts
Normal file
347
src/tool-handlers.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ErrorCode,
|
||||
ListToolsRequestSchema,
|
||||
McpError,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import OpenAI from 'openai';
|
||||
|
||||
import { ModelCache } from './model-cache.js';
|
||||
import { OpenRouterAPIClient } from './openrouter-api.js';
|
||||
|
||||
// Import tool handlers
|
||||
import { handleChatCompletion, ChatCompletionToolRequest } from './tool-handlers/chat-completion.js';
|
||||
import { handleSearchModels, SearchModelsToolRequest } from './tool-handlers/search-models.js';
|
||||
import { handleGetModelInfo, GetModelInfoToolRequest } from './tool-handlers/get-model-info.js';
|
||||
import { handleValidateModel, ValidateModelToolRequest } from './tool-handlers/validate-model.js';
|
||||
import { handleAnalyzeImage, AnalyzeImageToolRequest } from './tool-handlers/analyze-image.js';
|
||||
import { handleMultiImageAnalysis, MultiImageAnalysisToolRequest } from './tool-handlers/multi-image-analysis.js';
|
||||
|
||||
export class ToolHandlers {
|
||||
private server: Server;
|
||||
private openai: OpenAI;
|
||||
private modelCache: ModelCache;
|
||||
private apiClient: OpenRouterAPIClient;
|
||||
private defaultModel?: string;
|
||||
|
||||
constructor(
|
||||
server: Server,
|
||||
apiKey: string,
|
||||
defaultModel?: string
|
||||
) {
|
||||
this.server = server;
|
||||
this.modelCache = ModelCache.getInstance();
|
||||
this.apiClient = new OpenRouterAPIClient(apiKey);
|
||||
this.defaultModel = defaultModel;
|
||||
|
||||
this.openai = new OpenAI({
|
||||
apiKey: apiKey,
|
||||
baseURL: 'https://openrouter.ai/api/v1',
|
||||
defaultHeaders: {
|
||||
'HTTP-Referer': 'https://github.com/stabgan/openrouter-mcp-multimodal',
|
||||
'X-Title': 'OpenRouter MCP Multimodal Server',
|
||||
},
|
||||
});
|
||||
|
||||
this.setupToolHandlers();
|
||||
}
|
||||
|
||||
private setupToolHandlers() {
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [
|
||||
// Chat Completion Tool
|
||||
{
|
||||
name: 'chat_completion',
|
||||
description: 'Send a message to OpenRouter.ai and get a response',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
model: {
|
||||
type: 'string',
|
||||
description: 'The model to use (e.g., "google/gemini-2.5-pro-exp-03-25:free", "undi95/toppy-m-7b:free"). If not provided, uses the default model if set.',
|
||||
},
|
||||
messages: {
|
||||
type: 'array',
|
||||
description: 'An array of conversation messages with roles and content',
|
||||
minItems: 1,
|
||||
maxItems: 100,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
role: {
|
||||
type: 'string',
|
||||
enum: ['system', 'user', 'assistant'],
|
||||
description: 'The role of the message sender',
|
||||
},
|
||||
content: {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string',
|
||||
description: 'The text content of the message',
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
description: 'Array of content parts for multimodal messages (text and images)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['text', 'image_url'],
|
||||
description: 'The type of content (text or image)',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
description: 'The text content (for text type)',
|
||||
},
|
||||
image_url: {
|
||||
type: 'object',
|
||||
description: 'The image URL object (for image_url type)',
|
||||
properties: {
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL of the image (can be a data URL with base64)',
|
||||
},
|
||||
},
|
||||
required: ['url'],
|
||||
},
|
||||
},
|
||||
required: ['type'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
required: ['role', 'content'],
|
||||
},
|
||||
},
|
||||
temperature: {
|
||||
type: 'number',
|
||||
description: 'Sampling temperature (0-2)',
|
||||
minimum: 0,
|
||||
maximum: 2,
|
||||
},
|
||||
},
|
||||
required: ['messages'],
|
||||
},
|
||||
maxContextTokens: 200000
|
||||
},
|
||||
|
||||
// Image Analysis Tool
|
||||
{
|
||||
name: 'analyze_image',
|
||||
description: 'Analyze an image using OpenRouter vision models',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
image_path: {
|
||||
type: 'string',
|
||||
description: 'Path to the image file to analyze (must be an absolute path)',
|
||||
},
|
||||
question: {
|
||||
type: 'string',
|
||||
description: 'Question to ask about the image',
|
||||
},
|
||||
model: {
|
||||
type: 'string',
|
||||
description: 'OpenRouter model to use (e.g., "anthropic/claude-3.5-sonnet")',
|
||||
},
|
||||
},
|
||||
required: ['image_path'],
|
||||
},
|
||||
},
|
||||
|
||||
// Multi-Image Analysis Tool
|
||||
{
|
||||
name: 'multi_image_analysis',
|
||||
description: 'Analyze multiple images at once with a single prompt and receive detailed responses',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
images: {
|
||||
type: 'array',
|
||||
description: 'Array of image objects to analyze',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL or data URL of the image (can be a file:// URL to read from local filesystem)',
|
||||
},
|
||||
alt: {
|
||||
type: 'string',
|
||||
description: 'Optional alt text or description of the image',
|
||||
},
|
||||
},
|
||||
required: ['url'],
|
||||
},
|
||||
},
|
||||
prompt: {
|
||||
type: 'string',
|
||||
description: 'Prompt for analyzing the images',
|
||||
},
|
||||
markdown_response: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to format the response in Markdown (default: true)',
|
||||
default: true,
|
||||
},
|
||||
model: {
|
||||
type: 'string',
|
||||
description: 'OpenRouter model to use (defaults to claude-3.5-sonnet if not specified)',
|
||||
},
|
||||
},
|
||||
required: ['images', 'prompt'],
|
||||
},
|
||||
},
|
||||
|
||||
// Search Models Tool
|
||||
{
|
||||
name: 'search_models',
|
||||
description: 'Search and filter OpenRouter.ai models based on various criteria',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Optional search query to filter by name, description, or provider',
|
||||
},
|
||||
provider: {
|
||||
type: 'string',
|
||||
description: 'Filter by specific provider (e.g., "anthropic", "openai", "cohere")',
|
||||
},
|
||||
minContextLength: {
|
||||
type: 'number',
|
||||
description: 'Minimum context length in tokens',
|
||||
},
|
||||
maxContextLength: {
|
||||
type: 'number',
|
||||
description: 'Maximum context length in tokens',
|
||||
},
|
||||
maxPromptPrice: {
|
||||
type: 'number',
|
||||
description: 'Maximum price per 1K tokens for prompts',
|
||||
},
|
||||
maxCompletionPrice: {
|
||||
type: 'number',
|
||||
description: 'Maximum price per 1K tokens for completions',
|
||||
},
|
||||
capabilities: {
|
||||
type: 'object',
|
||||
description: 'Filter by model capabilities',
|
||||
properties: {
|
||||
functions: {
|
||||
type: 'boolean',
|
||||
description: 'Requires function calling capability',
|
||||
},
|
||||
tools: {
|
||||
type: 'boolean',
|
||||
description: 'Requires tools capability',
|
||||
},
|
||||
vision: {
|
||||
type: 'boolean',
|
||||
description: 'Requires vision capability',
|
||||
},
|
||||
json_mode: {
|
||||
type: 'boolean',
|
||||
description: 'Requires JSON mode capability',
|
||||
}
|
||||
}
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of results to return (default: 10)',
|
||||
minimum: 1,
|
||||
maximum: 50
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// Get Model Info Tool
|
||||
{
|
||||
name: 'get_model_info',
|
||||
description: 'Get detailed information about a specific model',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
model: {
|
||||
type: 'string',
|
||||
description: 'The model ID to get information for',
|
||||
},
|
||||
},
|
||||
required: ['model'],
|
||||
},
|
||||
},
|
||||
|
||||
// Validate Model Tool
|
||||
{
|
||||
name: 'validate_model',
|
||||
description: 'Check if a model ID is valid',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
model: {
|
||||
type: 'string',
|
||||
description: 'The model ID to validate',
|
||||
},
|
||||
},
|
||||
required: ['model'],
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
switch (request.params.name) {
|
||||
case 'chat_completion':
|
||||
return handleChatCompletion({
|
||||
params: {
|
||||
arguments: request.params.arguments as unknown as ChatCompletionToolRequest
|
||||
}
|
||||
}, this.openai, this.defaultModel);
|
||||
|
||||
case 'analyze_image':
|
||||
return handleAnalyzeImage({
|
||||
params: {
|
||||
arguments: request.params.arguments as unknown as AnalyzeImageToolRequest
|
||||
}
|
||||
}, this.openai, this.defaultModel);
|
||||
|
||||
case 'multi_image_analysis':
|
||||
return handleMultiImageAnalysis({
|
||||
params: {
|
||||
arguments: request.params.arguments as unknown as MultiImageAnalysisToolRequest
|
||||
}
|
||||
}, this.openai, this.defaultModel);
|
||||
|
||||
case 'search_models':
|
||||
return handleSearchModels({
|
||||
params: {
|
||||
arguments: request.params.arguments as SearchModelsToolRequest
|
||||
}
|
||||
}, this.apiClient, this.modelCache);
|
||||
|
||||
case 'get_model_info':
|
||||
return handleGetModelInfo({
|
||||
params: {
|
||||
arguments: request.params.arguments as unknown as GetModelInfoToolRequest
|
||||
}
|
||||
}, this.modelCache);
|
||||
|
||||
case 'validate_model':
|
||||
return handleValidateModel({
|
||||
params: {
|
||||
arguments: request.params.arguments as unknown as ValidateModelToolRequest
|
||||
}
|
||||
}, this.modelCache);
|
||||
|
||||
default:
|
||||
throw new McpError(
|
||||
ErrorCode.MethodNotFound,
|
||||
`Unknown tool: ${request.params.name}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user