@stabgan/openrouter-mcp-multimodal 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +331 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/model-cache.d.ts +56 -0
- package/dist/model-cache.js +143 -0
- package/dist/model-cache.js.map +1 -0
- package/dist/openrouter-api.d.ts +26 -0
- package/dist/openrouter-api.js +119 -0
- package/dist/openrouter-api.js.map +1 -0
- package/dist/tool-handlers/analyze-image.d.ts +23 -0
- package/dist/tool-handlers/analyze-image.js +93 -0
- package/dist/tool-handlers/analyze-image.js.map +1 -0
- package/dist/tool-handlers/chat-completion.d.ts +24 -0
- package/dist/tool-handlers/chat-completion.js +111 -0
- package/dist/tool-handlers/chat-completion.js.map +1 -0
- package/dist/tool-handlers/get-model-info.d.ts +21 -0
- package/dist/tool-handlers/get-model-info.js +44 -0
- package/dist/tool-handlers/get-model-info.js.map +1 -0
- package/dist/tool-handlers/multi-image-analysis.d.ts +27 -0
- package/dist/tool-handlers/multi-image-analysis.js +136 -0
- package/dist/tool-handlers/multi-image-analysis.js.map +1 -0
- package/dist/tool-handlers/search-models.d.ts +34 -0
- package/dist/tool-handlers/search-models.js +44 -0
- package/dist/tool-handlers/search-models.js.map +1 -0
- package/dist/tool-handlers/validate-model.d.ts +21 -0
- package/dist/tool-handlers/validate-model.js +40 -0
- package/dist/tool-handlers/validate-model.js.map +1 -0
- package/dist/tool-handlers.d.ts +10 -0
- package/dist/tool-handlers.js +316 -0
- package/dist/tool-handlers.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Client for interacting with the OpenRouter API
|
|
5
|
+
*/
|
|
6
|
+
export class OpenRouterAPIClient {
|
|
7
|
+
apiKey;
|
|
8
|
+
axiosInstance;
|
|
9
|
+
retryCount = 3;
|
|
10
|
+
retryDelay = 1000; // Initial delay in ms
|
|
11
|
+
constructor(apiKey) {
|
|
12
|
+
this.apiKey = apiKey;
|
|
13
|
+
this.axiosInstance = axios.create({
|
|
14
|
+
baseURL: 'https://openrouter.ai/api/v1',
|
|
15
|
+
headers: {
|
|
16
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
'HTTP-Referer': 'https://github.com/stabgan/openrouter-mcp-multimodal',
|
|
19
|
+
'X-Title': 'OpenRouter MCP Multimodal Server'
|
|
20
|
+
},
|
|
21
|
+
timeout: 60000 // 60 seconds timeout
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get all available models from OpenRouter
|
|
26
|
+
*/
|
|
27
|
+
async getModels() {
|
|
28
|
+
try {
|
|
29
|
+
const response = await this.axiosInstance.get('/models');
|
|
30
|
+
return response.data.data;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
this.handleRequestError(error);
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Send a request to the OpenRouter API with retry functionality
|
|
39
|
+
*/
|
|
40
|
+
async request(endpoint, method, data) {
|
|
41
|
+
let lastError = null;
|
|
42
|
+
let retries = 0;
|
|
43
|
+
while (retries <= this.retryCount) {
|
|
44
|
+
try {
|
|
45
|
+
const response = await this.axiosInstance.request({
|
|
46
|
+
url: endpoint,
|
|
47
|
+
method,
|
|
48
|
+
data
|
|
49
|
+
});
|
|
50
|
+
return response.data;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
lastError = this.handleRetryableError(error, retries);
|
|
54
|
+
retries++;
|
|
55
|
+
if (retries <= this.retryCount) {
|
|
56
|
+
// Exponential backoff with jitter
|
|
57
|
+
const delay = this.retryDelay * Math.pow(2, retries - 1) * (0.5 + Math.random() * 0.5);
|
|
58
|
+
console.error(`Retrying in ${Math.round(delay)}ms (${retries}/${this.retryCount})`);
|
|
59
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// If we get here, all retries failed
|
|
64
|
+
throw lastError || new Error('Request failed after multiple retries');
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Handle retryable errors
|
|
68
|
+
*/
|
|
69
|
+
handleRetryableError(error, retryCount) {
|
|
70
|
+
if (axios.isAxiosError(error)) {
|
|
71
|
+
const axiosError = error;
|
|
72
|
+
// Rate limiting (429) or server errors (5xx)
|
|
73
|
+
if (axiosError.response?.status === 429 || (axiosError.response?.status && axiosError.response.status >= 500)) {
|
|
74
|
+
console.error(`Request error (retry ${retryCount}): ${axiosError.message}`);
|
|
75
|
+
if (axiosError.response?.status === 429) {
|
|
76
|
+
console.error('Rate limit exceeded. Retrying with backoff...');
|
|
77
|
+
}
|
|
78
|
+
return new Error(`OpenRouter API error: ${axiosError.response?.status} ${axiosError.message}`);
|
|
79
|
+
}
|
|
80
|
+
// For other status codes, don't retry
|
|
81
|
+
if (axiosError.response) {
|
|
82
|
+
const responseData = axiosError.response.data;
|
|
83
|
+
const message = responseData?.error?.message || axiosError.message;
|
|
84
|
+
throw new McpError(ErrorCode.InternalError, `OpenRouter API error: ${message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Network errors should be retried
|
|
88
|
+
console.error(`Network error (retry ${retryCount}): ${error.message}`);
|
|
89
|
+
return new Error(`Network error: ${error.message}`);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Handle request errors
|
|
93
|
+
*/
|
|
94
|
+
handleRequestError(error) {
|
|
95
|
+
console.error('Error in OpenRouter API request:', error);
|
|
96
|
+
if (axios.isAxiosError(error)) {
|
|
97
|
+
const axiosError = error;
|
|
98
|
+
if (axiosError.response) {
|
|
99
|
+
const status = axiosError.response.status;
|
|
100
|
+
const responseData = axiosError.response.data;
|
|
101
|
+
const message = responseData?.error?.message || axiosError.message;
|
|
102
|
+
if (status === 401 || status === 403) {
|
|
103
|
+
throw new McpError(ErrorCode.InvalidRequest, `Authentication error: ${message}`);
|
|
104
|
+
}
|
|
105
|
+
else if (status === 429) {
|
|
106
|
+
throw new McpError(ErrorCode.InternalError, `Rate limit exceeded: ${message}`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
throw new McpError(ErrorCode.InternalError, `OpenRouter API error (${status}): ${message}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else if (axiosError.request) {
|
|
113
|
+
throw new McpError(ErrorCode.ConnectionClosed, `Network error: ${axiosError.message}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
throw new McpError(ErrorCode.InternalError, `Unknown error: ${error.message || 'No error message'}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=openrouter-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openrouter-api.js","sourceRoot":"","sources":["../src/openrouter-api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
export interface AnalyzeImageToolRequest {
|
|
3
|
+
image_path: string;
|
|
4
|
+
question?: string;
|
|
5
|
+
model?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function handleAnalyzeImage(request: {
|
|
8
|
+
params: {
|
|
9
|
+
arguments: AnalyzeImageToolRequest;
|
|
10
|
+
};
|
|
11
|
+
}, openai: OpenAI, defaultModel?: string): Promise<{
|
|
12
|
+
content: {
|
|
13
|
+
type: string;
|
|
14
|
+
text: string;
|
|
15
|
+
}[];
|
|
16
|
+
isError?: undefined;
|
|
17
|
+
} | {
|
|
18
|
+
content: {
|
|
19
|
+
type: string;
|
|
20
|
+
text: string;
|
|
21
|
+
}[];
|
|
22
|
+
isError: boolean;
|
|
23
|
+
}>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import sharp from 'sharp';
|
|
4
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
export async function handleAnalyzeImage(request, openai, defaultModel) {
|
|
6
|
+
const args = request.params.arguments;
|
|
7
|
+
try {
|
|
8
|
+
// Validate image path
|
|
9
|
+
const imagePath = args.image_path;
|
|
10
|
+
if (!path.isAbsolute(imagePath)) {
|
|
11
|
+
throw new McpError(ErrorCode.InvalidParams, 'Image path must be absolute');
|
|
12
|
+
}
|
|
13
|
+
// Read image file
|
|
14
|
+
const imageBuffer = await fs.readFile(imagePath);
|
|
15
|
+
console.error(`Successfully read image buffer of size: ${imageBuffer.length}`);
|
|
16
|
+
// Get image metadata
|
|
17
|
+
const metadata = await sharp(imageBuffer).metadata();
|
|
18
|
+
console.error('Image metadata:', metadata);
|
|
19
|
+
// Calculate dimensions to keep base64 size reasonable
|
|
20
|
+
const MAX_DIMENSION = 800; // Larger than original example for better quality
|
|
21
|
+
const JPEG_QUALITY = 80; // Higher quality
|
|
22
|
+
let resizedBuffer = imageBuffer;
|
|
23
|
+
if (metadata.width && metadata.height) {
|
|
24
|
+
const largerDimension = Math.max(metadata.width, metadata.height);
|
|
25
|
+
if (largerDimension > MAX_DIMENSION) {
|
|
26
|
+
const resizeOptions = metadata.width > metadata.height
|
|
27
|
+
? { width: MAX_DIMENSION }
|
|
28
|
+
: { height: MAX_DIMENSION };
|
|
29
|
+
resizedBuffer = await sharp(imageBuffer)
|
|
30
|
+
.resize(resizeOptions)
|
|
31
|
+
.jpeg({ quality: JPEG_QUALITY })
|
|
32
|
+
.toBuffer();
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
resizedBuffer = await sharp(imageBuffer)
|
|
36
|
+
.jpeg({ quality: JPEG_QUALITY })
|
|
37
|
+
.toBuffer();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Convert to base64
|
|
41
|
+
const base64Image = resizedBuffer.toString('base64');
|
|
42
|
+
// Select model
|
|
43
|
+
const model = args.model || defaultModel || 'anthropic/claude-3.5-sonnet';
|
|
44
|
+
// Prepare message with image
|
|
45
|
+
const messages = [
|
|
46
|
+
{
|
|
47
|
+
role: 'user',
|
|
48
|
+
content: [
|
|
49
|
+
{
|
|
50
|
+
type: 'text',
|
|
51
|
+
text: args.question || "What's in this image?"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'image_url',
|
|
55
|
+
image_url: {
|
|
56
|
+
url: `data:image/jpeg;base64,${base64Image}`
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
console.error('Sending request to OpenRouter...');
|
|
63
|
+
// Call OpenRouter API
|
|
64
|
+
const completion = await openai.chat.completions.create({
|
|
65
|
+
model,
|
|
66
|
+
messages: messages,
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: 'text',
|
|
72
|
+
text: completion.choices[0].message.content || '',
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.error('Error analyzing image:', error);
|
|
79
|
+
if (error instanceof McpError) {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
content: [
|
|
84
|
+
{
|
|
85
|
+
type: 'text',
|
|
86
|
+
text: `Error analyzing image: ${error instanceof Error ? error.message : String(error)}`,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
isError: true,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=analyze-image.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze-image.js","sourceRoot":"","sources":["../../src/tool-handlers/analyze-image.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { ChatCompletionMessageParam } from 'openai/resources/chat/completions.js';
|
|
3
|
+
export interface ChatCompletionToolRequest {
|
|
4
|
+
model?: string;
|
|
5
|
+
messages: ChatCompletionMessageParam[];
|
|
6
|
+
temperature?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function handleChatCompletion(request: {
|
|
9
|
+
params: {
|
|
10
|
+
arguments: ChatCompletionToolRequest;
|
|
11
|
+
};
|
|
12
|
+
}, openai: OpenAI, defaultModel?: string): Promise<{
|
|
13
|
+
content: {
|
|
14
|
+
type: string;
|
|
15
|
+
text: string;
|
|
16
|
+
}[];
|
|
17
|
+
isError: boolean;
|
|
18
|
+
} | {
|
|
19
|
+
content: {
|
|
20
|
+
type: string;
|
|
21
|
+
text: string;
|
|
22
|
+
}[];
|
|
23
|
+
isError?: undefined;
|
|
24
|
+
}>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Maximum context tokens
|
|
2
|
+
const MAX_CONTEXT_TOKENS = 200000;
|
|
3
|
+
// Utility function to estimate token count (simplified)
|
|
4
|
+
function estimateTokenCount(text) {
|
|
5
|
+
// Rough approximation: 4 characters per token
|
|
6
|
+
return Math.ceil(text.length / 4);
|
|
7
|
+
}
|
|
8
|
+
// Truncate messages to fit within the context window
|
|
9
|
+
function truncateMessagesToFit(messages, maxTokens) {
|
|
10
|
+
const truncated = [];
|
|
11
|
+
let currentTokenCount = 0;
|
|
12
|
+
// Always include system message first if present
|
|
13
|
+
if (messages[0]?.role === 'system') {
|
|
14
|
+
truncated.push(messages[0]);
|
|
15
|
+
currentTokenCount += estimateTokenCount(messages[0].content);
|
|
16
|
+
}
|
|
17
|
+
// Add messages from the end, respecting the token limit
|
|
18
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
19
|
+
const message = messages[i];
|
|
20
|
+
// Skip if it's the system message we've already added
|
|
21
|
+
if (i === 0 && message.role === 'system')
|
|
22
|
+
continue;
|
|
23
|
+
// For string content, estimate tokens directly
|
|
24
|
+
if (typeof message.content === 'string') {
|
|
25
|
+
const messageTokens = estimateTokenCount(message.content);
|
|
26
|
+
if (currentTokenCount + messageTokens > maxTokens)
|
|
27
|
+
break;
|
|
28
|
+
truncated.unshift(message);
|
|
29
|
+
currentTokenCount += messageTokens;
|
|
30
|
+
}
|
|
31
|
+
// For multimodal content (array), estimate tokens for text content
|
|
32
|
+
else if (Array.isArray(message.content)) {
|
|
33
|
+
let messageTokens = 0;
|
|
34
|
+
for (const part of message.content) {
|
|
35
|
+
if (part.type === 'text' && part.text) {
|
|
36
|
+
messageTokens += estimateTokenCount(part.text);
|
|
37
|
+
}
|
|
38
|
+
else if (part.type === 'image_url') {
|
|
39
|
+
// Add a token cost estimate for images - this is a simplification
|
|
40
|
+
// Actual image token costs depend on resolution and model
|
|
41
|
+
messageTokens += 1000;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (currentTokenCount + messageTokens > maxTokens)
|
|
45
|
+
break;
|
|
46
|
+
truncated.unshift(message);
|
|
47
|
+
currentTokenCount += messageTokens;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return truncated;
|
|
51
|
+
}
|
|
52
|
+
export async function handleChatCompletion(request, openai, defaultModel) {
|
|
53
|
+
const args = request.params.arguments;
|
|
54
|
+
// Validate model selection
|
|
55
|
+
const model = args.model || defaultModel;
|
|
56
|
+
if (!model) {
|
|
57
|
+
return {
|
|
58
|
+
content: [
|
|
59
|
+
{
|
|
60
|
+
type: 'text',
|
|
61
|
+
text: 'No model specified and no default model configured in MCP settings. Please specify a model or set OPENROUTER_DEFAULT_MODEL in the MCP configuration.',
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
isError: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Validate message array
|
|
68
|
+
if (args.messages.length === 0) {
|
|
69
|
+
return {
|
|
70
|
+
content: [
|
|
71
|
+
{
|
|
72
|
+
type: 'text',
|
|
73
|
+
text: 'Messages array cannot be empty. At least one message is required.',
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
isError: true,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
// Truncate messages to fit within context window
|
|
81
|
+
const truncatedMessages = truncateMessagesToFit(args.messages, MAX_CONTEXT_TOKENS);
|
|
82
|
+
const completion = await openai.chat.completions.create({
|
|
83
|
+
model,
|
|
84
|
+
messages: truncatedMessages,
|
|
85
|
+
temperature: args.temperature ?? 1,
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: 'text',
|
|
91
|
+
text: completion.choices[0].message.content || '',
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (error instanceof Error) {
|
|
98
|
+
return {
|
|
99
|
+
content: [
|
|
100
|
+
{
|
|
101
|
+
type: 'text',
|
|
102
|
+
text: `OpenRouter API error: ${error.message}`,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
isError: true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=chat-completion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-completion.js","sourceRoot":"","sources":["../../src/tool-handlers/chat-completion.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ModelCache } from '../model-cache.js';
|
|
2
|
+
export interface GetModelInfoToolRequest {
|
|
3
|
+
model: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function handleGetModelInfo(request: {
|
|
6
|
+
params: {
|
|
7
|
+
arguments: GetModelInfoToolRequest;
|
|
8
|
+
};
|
|
9
|
+
}, modelCache: ModelCache): Promise<{
|
|
10
|
+
content: {
|
|
11
|
+
type: string;
|
|
12
|
+
text: string;
|
|
13
|
+
}[];
|
|
14
|
+
isError: boolean;
|
|
15
|
+
} | {
|
|
16
|
+
content: {
|
|
17
|
+
type: string;
|
|
18
|
+
text: string;
|
|
19
|
+
}[];
|
|
20
|
+
isError?: undefined;
|
|
21
|
+
}>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
export async function handleGetModelInfo(request, modelCache) {
|
|
3
|
+
const args = request.params.arguments;
|
|
4
|
+
try {
|
|
5
|
+
if (!modelCache.isCacheValid()) {
|
|
6
|
+
return {
|
|
7
|
+
content: [
|
|
8
|
+
{
|
|
9
|
+
type: 'text',
|
|
10
|
+
text: 'Model cache is empty or expired. Please call search_models first to populate the cache.',
|
|
11
|
+
},
|
|
12
|
+
],
|
|
13
|
+
isError: true,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const model = modelCache.getModel(args.model);
|
|
17
|
+
if (!model) {
|
|
18
|
+
throw new McpError(ErrorCode.InvalidParams, `Model '${args.model}' not found`);
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
content: [
|
|
22
|
+
{
|
|
23
|
+
type: 'text',
|
|
24
|
+
text: JSON.stringify(model, null, 2),
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error instanceof Error) {
|
|
31
|
+
return {
|
|
32
|
+
content: [
|
|
33
|
+
{
|
|
34
|
+
type: 'text',
|
|
35
|
+
text: `Error retrieving model info: ${error.message}`,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
isError: true,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=get-model-info.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-model-info.js","sourceRoot":"","sources":["../../src/tool-handlers/get-model-info.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oCAAoC,CAAC;AAOzE,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,OAA2D,EAC3D,UAAsB;IAEtB,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC;IAEtC,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,YAAY,EAAE,EAAE,CAAC;YAC/B,OAAO;gBACL,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,MAAM;wBACZ,IAAI,EAAE,yFAAyF;qBAChG;iBACF;gBACD,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;QAED,MAAM,KAAK,GAAG,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9C,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,QAAQ,CAAC,SAAS,CAAC,aAAa,EAAE,UAAU,IAAI,CAAC,KAAK,aAAa,CAAC,CAAC;QACjF,CAAC;QAED,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;iBACrC;aACF;SACF,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;YAC3B,OAAO;gBACL,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,MAAM;wBACZ,IAAI,EAAE,gCAAgC,KAAK,CAAC,OAAO,EAAE;qBACtD;iBACF;gBACD,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
export interface MultiImageAnalysisToolRequest {
|
|
3
|
+
images: Array<{
|
|
4
|
+
url: string;
|
|
5
|
+
alt?: string;
|
|
6
|
+
}>;
|
|
7
|
+
prompt: string;
|
|
8
|
+
markdown_response?: boolean;
|
|
9
|
+
model?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function handleMultiImageAnalysis(request: {
|
|
12
|
+
params: {
|
|
13
|
+
arguments: MultiImageAnalysisToolRequest;
|
|
14
|
+
};
|
|
15
|
+
}, openai: OpenAI, defaultModel?: string): Promise<{
|
|
16
|
+
content: {
|
|
17
|
+
type: string;
|
|
18
|
+
text: string;
|
|
19
|
+
}[];
|
|
20
|
+
isError?: undefined;
|
|
21
|
+
} | {
|
|
22
|
+
content: {
|
|
23
|
+
type: string;
|
|
24
|
+
text: string;
|
|
25
|
+
}[];
|
|
26
|
+
isError: boolean;
|
|
27
|
+
}>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
import sharp from 'sharp';
|
|
3
|
+
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
async function fetchImageAsBuffer(url) {
|
|
5
|
+
try {
|
|
6
|
+
// Handle data URLs
|
|
7
|
+
if (url.startsWith('data:')) {
|
|
8
|
+
const matches = url.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
|
|
9
|
+
if (!matches || matches.length !== 3) {
|
|
10
|
+
throw new Error('Invalid data URL');
|
|
11
|
+
}
|
|
12
|
+
return Buffer.from(matches[2], 'base64');
|
|
13
|
+
}
|
|
14
|
+
// Handle file URLs
|
|
15
|
+
if (url.startsWith('file://')) {
|
|
16
|
+
const filePath = url.replace('file://', '');
|
|
17
|
+
const fs = await import('fs/promises');
|
|
18
|
+
return await fs.readFile(filePath);
|
|
19
|
+
}
|
|
20
|
+
// Handle http/https URLs
|
|
21
|
+
const response = await fetch(url);
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
24
|
+
}
|
|
25
|
+
return Buffer.from(await response.arrayBuffer());
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.error(`Error fetching image from ${url}:`, error);
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function processImage(buffer) {
|
|
33
|
+
try {
|
|
34
|
+
// Get image metadata
|
|
35
|
+
const metadata = await sharp(buffer).metadata();
|
|
36
|
+
// Calculate dimensions to keep base64 size reasonable
|
|
37
|
+
const MAX_DIMENSION = 800;
|
|
38
|
+
const JPEG_QUALITY = 80;
|
|
39
|
+
if (metadata.width && metadata.height) {
|
|
40
|
+
const largerDimension = Math.max(metadata.width, metadata.height);
|
|
41
|
+
if (largerDimension > MAX_DIMENSION) {
|
|
42
|
+
const resizeOptions = metadata.width > metadata.height
|
|
43
|
+
? { width: MAX_DIMENSION }
|
|
44
|
+
: { height: MAX_DIMENSION };
|
|
45
|
+
const resizedBuffer = await sharp(buffer)
|
|
46
|
+
.resize(resizeOptions)
|
|
47
|
+
.jpeg({ quality: JPEG_QUALITY })
|
|
48
|
+
.toBuffer();
|
|
49
|
+
return resizedBuffer.toString('base64');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// If no resizing needed, just convert to JPEG
|
|
53
|
+
const jpegBuffer = await sharp(buffer)
|
|
54
|
+
.jpeg({ quality: JPEG_QUALITY })
|
|
55
|
+
.toBuffer();
|
|
56
|
+
return jpegBuffer.toString('base64');
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.error('Error processing image:', error);
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function handleMultiImageAnalysis(request, openai, defaultModel) {
|
|
64
|
+
const args = request.params.arguments;
|
|
65
|
+
try {
|
|
66
|
+
// Validate inputs
|
|
67
|
+
if (!args.images || args.images.length === 0) {
|
|
68
|
+
throw new McpError(ErrorCode.InvalidParams, 'At least one image is required');
|
|
69
|
+
}
|
|
70
|
+
if (!args.prompt) {
|
|
71
|
+
throw new McpError(ErrorCode.InvalidParams, 'A prompt is required');
|
|
72
|
+
}
|
|
73
|
+
// Prepare content array for the message
|
|
74
|
+
const content = [{
|
|
75
|
+
type: 'text',
|
|
76
|
+
text: args.prompt
|
|
77
|
+
}];
|
|
78
|
+
// Process each image
|
|
79
|
+
for (const image of args.images) {
|
|
80
|
+
try {
|
|
81
|
+
// Fetch and process the image
|
|
82
|
+
const imageBuffer = await fetchImageAsBuffer(image.url);
|
|
83
|
+
const base64Image = await processImage(imageBuffer);
|
|
84
|
+
// Add to content
|
|
85
|
+
content.push({
|
|
86
|
+
type: 'image_url',
|
|
87
|
+
image_url: {
|
|
88
|
+
url: `data:image/jpeg;base64,${base64Image}`
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.error(`Error processing image ${image.url}:`, error);
|
|
94
|
+
// Continue with other images if one fails
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// If no images were successfully processed
|
|
98
|
+
if (content.length === 1) {
|
|
99
|
+
throw new Error('Failed to process any of the provided images');
|
|
100
|
+
}
|
|
101
|
+
// Select model
|
|
102
|
+
const model = args.model || defaultModel || 'anthropic/claude-3.5-sonnet';
|
|
103
|
+
// Make the API call
|
|
104
|
+
const completion = await openai.chat.completions.create({
|
|
105
|
+
model,
|
|
106
|
+
messages: [{
|
|
107
|
+
role: 'user',
|
|
108
|
+
content
|
|
109
|
+
}]
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
content: [
|
|
113
|
+
{
|
|
114
|
+
type: 'text',
|
|
115
|
+
text: completion.choices[0].message.content || '',
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
console.error('Error in multi-image analysis:', error);
|
|
122
|
+
if (error instanceof McpError) {
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
type: 'text',
|
|
129
|
+
text: `Error analyzing images: ${error instanceof Error ? error.message : String(error)}`,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
isError: true,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=multi-image-analysis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multi-image-analysis.js","sourceRoot":"","sources":["../../src/tool-handlers/multi-image-analysis.ts"],"names":[],"mappings":"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"}
|