@stabgan/openrouter-mcp-multimodal 1.4.0 → 1.8.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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +164 -147
  3. package/dist/__tests__/image-utils.test.d.ts +1 -0
  4. package/dist/__tests__/image-utils.test.js +75 -0
  5. package/dist/__tests__/integration.test.d.ts +1 -0
  6. package/dist/__tests__/integration.test.js +111 -0
  7. package/dist/__tests__/model-cache.test.d.ts +1 -0
  8. package/dist/__tests__/model-cache.test.js +96 -0
  9. package/dist/index.js +47 -46
  10. package/dist/model-cache.d.ts +18 -46
  11. package/dist/model-cache.js +28 -125
  12. package/dist/openrouter-api.d.ts +2 -22
  13. package/dist/openrouter-api.js +34 -111
  14. package/dist/tool-handlers/analyze-image.d.ts +2 -17
  15. package/dist/tool-handlers/analyze-image.js +18 -259
  16. package/dist/tool-handlers/chat-completion.d.ts +1 -0
  17. package/dist/tool-handlers/chat-completion.js +10 -143
  18. package/dist/tool-handlers/generate-image.d.ts +30 -0
  19. package/dist/tool-handlers/generate-image.js +82 -0
  20. package/dist/tool-handlers/get-model-info.d.ts +5 -5
  21. package/dist/tool-handlers/get-model-info.js +11 -41
  22. package/dist/tool-handlers/image-utils.d.ts +11 -0
  23. package/dist/tool-handlers/image-utils.js +253 -0
  24. package/dist/tool-handlers/search-models.d.ts +3 -10
  25. package/dist/tool-handlers/search-models.js +6 -37
  26. package/dist/tool-handlers/validate-model.d.ts +5 -5
  27. package/dist/tool-handlers/validate-model.js +13 -37
  28. package/dist/tool-handlers.d.ts +1 -2
  29. package/dist/tool-handlers.js +55 -234
  30. package/package.json +20 -21
  31. package/dist/index.js.map +0 -1
  32. package/dist/model-cache.js.map +0 -1
  33. package/dist/openrouter-api.js.map +0 -1
  34. package/dist/tool-handlers/analyze-image.js.map +0 -1
  35. package/dist/tool-handlers/chat-completion.js.map +0 -1
  36. package/dist/tool-handlers/get-model-info.js.map +0 -1
  37. package/dist/tool-handlers/multi-image-analysis.d.ts +0 -46
  38. package/dist/tool-handlers/multi-image-analysis.js +0 -405
  39. package/dist/tool-handlers/multi-image-analysis.js.map +0 -1
  40. package/dist/tool-handlers/search-models.js.map +0 -1
  41. package/dist/tool-handlers/validate-model.js.map +0 -1
  42. package/dist/tool-handlers.js.map +0 -1
@@ -1,269 +1,28 @@
1
- import path from 'path';
2
- import { promises as fs } from 'fs';
3
- import fetch from 'node-fetch';
4
- import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
5
- import { findSuitableFreeModel } from './multi-image-analysis.js';
6
- // Default model for image analysis
7
- const DEFAULT_FREE_MODEL = 'qwen/qwen2.5-vl-32b-instruct:free';
8
- let sharp;
9
- try {
10
- sharp = require('sharp');
11
- }
12
- catch (e) {
13
- console.error('Warning: sharp module not available, using fallback image processing');
14
- // Mock implementation that just passes through the base64 data
15
- sharp = (buffer) => ({
16
- metadata: async () => ({ width: 800, height: 600 }),
17
- resize: () => ({
18
- jpeg: () => ({
19
- toBuffer: async () => buffer
20
- })
21
- }),
22
- jpeg: () => ({
23
- toBuffer: async () => buffer
24
- })
25
- });
26
- }
27
- async function fetchImageAsBuffer(url) {
28
- try {
29
- // Handle data URLs
30
- if (url.startsWith('data:')) {
31
- const matches = url.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
32
- if (!matches || matches.length !== 3) {
33
- throw new Error('Invalid data URL');
34
- }
35
- return Buffer.from(matches[2], 'base64');
36
- }
37
- // Handle file URLs
38
- if (url.startsWith('file://')) {
39
- const filePath = url.replace('file://', '');
40
- return await fs.readFile(filePath);
41
- }
42
- // Handle http/https URLs
43
- if (url.startsWith('http://') || url.startsWith('https://')) {
44
- const response = await fetch(url);
45
- if (!response.ok) {
46
- throw new Error(`HTTP error! status: ${response.status}`);
47
- }
48
- return Buffer.from(await response.arrayBuffer());
49
- }
50
- // Handle regular file paths
51
- return await fs.readFile(url);
52
- }
53
- catch (error) {
54
- console.error(`Error fetching image from ${url}:`, error);
55
- throw error;
56
- }
57
- }
58
- /**
59
- * Processes an image with minimal processing when sharp isn't available
60
- */
61
- async function processImageFallback(buffer) {
62
- try {
63
- // Just return the buffer as base64 without processing
64
- return buffer.toString('base64');
65
- }
66
- catch (error) {
67
- console.error('Error in fallback image processing:', error);
68
- throw error;
69
- }
70
- }
71
- async function processImage(buffer) {
72
- try {
73
- if (typeof sharp !== 'function') {
74
- console.warn('Using fallback image processing (sharp not available)');
75
- return processImageFallback(buffer);
76
- }
77
- // Get image metadata
78
- let metadata;
79
- try {
80
- metadata = await sharp(buffer).metadata();
81
- }
82
- catch (error) {
83
- console.warn('Error getting image metadata, using fallback:', error);
84
- return processImageFallback(buffer);
85
- }
86
- // Calculate dimensions to keep base64 size reasonable
87
- const MAX_DIMENSION = 800;
88
- const JPEG_QUALITY = 80;
89
- if (metadata.width && metadata.height) {
90
- const largerDimension = Math.max(metadata.width, metadata.height);
91
- if (largerDimension > MAX_DIMENSION) {
92
- const resizeOptions = metadata.width > metadata.height
93
- ? { width: MAX_DIMENSION }
94
- : { height: MAX_DIMENSION };
95
- const resizedBuffer = await sharp(buffer)
96
- .resize(resizeOptions)
97
- .jpeg({ quality: JPEG_QUALITY })
98
- .toBuffer();
99
- return resizedBuffer.toString('base64');
100
- }
101
- }
102
- // If no resizing needed, just convert to JPEG
103
- const jpegBuffer = await sharp(buffer)
104
- .jpeg({ quality: JPEG_QUALITY })
105
- .toBuffer();
106
- return jpegBuffer.toString('base64');
107
- }
108
- catch (error) {
109
- console.error('Error processing image, using fallback:', error);
110
- return processImageFallback(buffer);
111
- }
112
- }
113
- /**
114
- * Processes an image from a path or base64 string to a proper base64 format for APIs
115
- */
116
- async function prepareImage(imagePath) {
117
- try {
118
- // Check if already a base64 data URL
119
- if (imagePath.startsWith('data:')) {
120
- const matches = imagePath.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
121
- if (!matches || matches.length !== 3) {
122
- throw new McpError(ErrorCode.InvalidParams, 'Invalid base64 data URL format');
123
- }
124
- return { base64: matches[2], mimeType: matches[1] };
125
- }
126
- // Check if image is a URL
127
- if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
128
- try {
129
- const buffer = await fetchImageAsBuffer(imagePath);
130
- const processed = await processImage(buffer);
131
- return { base64: processed, mimeType: 'image/jpeg' }; // We convert everything to JPEG
132
- }
133
- catch (error) {
134
- throw new McpError(ErrorCode.InvalidParams, `Failed to fetch image from URL: ${error.message}`);
135
- }
136
- }
137
- // Handle file paths
138
- let absolutePath = imagePath;
139
- // Ensure the image path is absolute if it's a file path
140
- if (!imagePath.startsWith('data:') && !path.isAbsolute(imagePath)) {
141
- throw new McpError(ErrorCode.InvalidParams, 'Image path must be absolute');
142
- }
143
- try {
144
- // Check if the file exists
145
- await fs.access(absolutePath);
146
- }
147
- catch (error) {
148
- throw new McpError(ErrorCode.InvalidParams, `File not found: ${absolutePath}`);
149
- }
150
- // Read the file as a buffer
151
- const buffer = await fs.readFile(absolutePath);
152
- // Determine MIME type from file extension
153
- const extension = path.extname(absolutePath).toLowerCase();
154
- let mimeType;
155
- switch (extension) {
156
- case '.png':
157
- mimeType = 'image/png';
158
- break;
159
- case '.jpg':
160
- case '.jpeg':
161
- mimeType = 'image/jpeg';
162
- break;
163
- case '.webp':
164
- mimeType = 'image/webp';
165
- break;
166
- case '.gif':
167
- mimeType = 'image/gif';
168
- break;
169
- case '.bmp':
170
- mimeType = 'image/bmp';
171
- break;
172
- default:
173
- mimeType = 'application/octet-stream';
174
- }
175
- // Process and optimize the image
176
- const processed = await processImage(buffer);
177
- return { base64: processed, mimeType };
178
- }
179
- catch (error) {
180
- console.error('Error preparing image:', error);
181
- throw error;
182
- }
183
- }
184
- /**
185
- * Handler for analyzing a single image
186
- */
1
+ import { prepareImageUrl } from './image-utils.js';
2
+ const DEFAULT_MODEL = 'nvidia/nemotron-nano-12b-v2-vl:free';
187
3
  export async function handleAnalyzeImage(request, openai, defaultModel) {
188
- const args = request.params.arguments;
4
+ const { image_path, question, model } = request.params.arguments;
5
+ if (!image_path) {
6
+ return { content: [{ type: 'text', text: 'image_path is required.' }], isError: true };
7
+ }
189
8
  try {
190
- // Validate inputs
191
- if (!args.image_path) {
192
- throw new McpError(ErrorCode.InvalidParams, 'An image path, URL, or base64 data is required');
193
- }
194
- const question = args.question || "What's in this image?";
195
- console.error(`Processing image: ${args.image_path.substring(0, 100)}${args.image_path.length > 100 ? '...' : ''}`);
196
- // Convert the image to base64
197
- const { base64, mimeType } = await prepareImage(args.image_path);
198
- // Create the content array for the OpenAI API
199
- const content = [
200
- {
201
- type: 'text',
202
- text: question
203
- },
204
- {
205
- type: 'image_url',
206
- image_url: {
207
- url: `data:${mimeType};base64,${base64}`
208
- }
209
- }
210
- ];
211
- // Select model with priority:
212
- // 1. User-specified model
213
- // 2. Default model from environment
214
- // 3. Default free vision model (qwen/qwen2.5-vl-32b-instruct:free)
215
- let model = args.model || defaultModel || DEFAULT_FREE_MODEL;
216
- // If a model is specified but not our default free model, verify it exists
217
- if (model !== DEFAULT_FREE_MODEL) {
218
- try {
219
- await openai.models.retrieve(model);
220
- }
221
- catch (error) {
222
- console.error(`Specified model ${model} not found, falling back to auto-selection`);
223
- model = await findSuitableFreeModel(openai);
224
- }
225
- }
226
- console.error(`Making API call with model: ${model}`);
227
- // Make the API call
9
+ const imageUrl = await prepareImageUrl(image_path);
228
10
  const completion = await openai.chat.completions.create({
229
- model,
230
- messages: [{
231
- role: 'user',
232
- content
233
- }]
234
- });
235
- // Return the analysis result
236
- return {
237
- content: [
11
+ model: model || defaultModel || DEFAULT_MODEL,
12
+ messages: [
238
13
  {
239
- type: 'text',
240
- text: completion.choices[0].message.content || '',
14
+ role: 'user',
15
+ content: [
16
+ { type: 'text', text: question || "What's in this image?" },
17
+ { type: 'image_url', image_url: { url: imageUrl } },
18
+ ],
241
19
  },
242
20
  ],
243
- metadata: {
244
- model: completion.model,
245
- usage: completion.usage
246
- }
247
- };
21
+ });
22
+ return { content: [{ type: 'text', text: completion.choices[0].message.content || '' }] };
248
23
  }
249
24
  catch (error) {
250
- console.error('Error in image analysis:', error);
251
- if (error instanceof McpError) {
252
- throw error;
253
- }
254
- return {
255
- content: [
256
- {
257
- type: 'text',
258
- text: `Error analyzing image: ${error instanceof Error ? error.message : String(error)}`,
259
- },
260
- ],
261
- isError: true,
262
- metadata: {
263
- error_type: error instanceof Error ? error.constructor.name : 'Unknown',
264
- error_message: error instanceof Error ? error.message : String(error)
265
- }
266
- };
25
+ const msg = error instanceof Error ? error.message : String(error);
26
+ return { content: [{ type: 'text', text: `Error: ${msg}` }], isError: true };
267
27
  }
268
28
  }
269
- //# sourceMappingURL=analyze-image.js.map
@@ -4,6 +4,7 @@ export interface ChatCompletionToolRequest {
4
4
  model?: string;
5
5
  messages: ChatCompletionMessageParam[];
6
6
  temperature?: number;
7
+ max_tokens?: number;
7
8
  }
8
9
  export declare function handleChatCompletion(request: {
9
10
  params: {
@@ -1,152 +1,19 @@
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
- // Find a suitable free model with the largest context window
53
- async function findSuitableFreeModel(openai) {
54
- try {
55
- // Query available models with 'free' in their name
56
- const modelsResponse = await openai.models.list();
57
- if (!modelsResponse || !modelsResponse.data || modelsResponse.data.length === 0) {
58
- return 'deepseek/deepseek-chat-v3-0324:free'; // Fallback to a known model
59
- }
60
- // Filter models with 'free' in ID
61
- const freeModels = modelsResponse.data
62
- .filter(model => model.id.includes('free'))
63
- .map(model => {
64
- // Try to extract context length from the model object
65
- let contextLength = 0;
66
- try {
67
- const modelAny = model; // Cast to any to access non-standard properties
68
- if (typeof modelAny.context_length === 'number') {
69
- contextLength = modelAny.context_length;
70
- }
71
- else if (modelAny.context_window) {
72
- contextLength = parseInt(modelAny.context_window, 10);
73
- }
74
- }
75
- catch (e) {
76
- console.error(`Error parsing context length for model ${model.id}:`, e);
77
- }
78
- return {
79
- id: model.id,
80
- contextLength: contextLength || 0
81
- };
82
- });
83
- if (freeModels.length === 0) {
84
- return 'deepseek/deepseek-chat-v3-0324:free'; // Fallback if no free models found
85
- }
86
- // Sort by context length and pick the one with the largest context window
87
- freeModels.sort((a, b) => b.contextLength - a.contextLength);
88
- console.error(`Selected free model: ${freeModels[0].id} with context length: ${freeModels[0].contextLength}`);
89
- return freeModels[0].id;
90
- }
91
- catch (error) {
92
- console.error('Error finding suitable free model:', error);
93
- return 'deepseek/deepseek-chat-v3-0324:free'; // Fallback to a known model
94
- }
95
- }
96
1
  export async function handleChatCompletion(request, openai, defaultModel) {
97
- const args = request.params.arguments;
98
- // Validate message array
99
- if (args.messages.length === 0) {
100
- return {
101
- content: [
102
- {
103
- type: 'text',
104
- text: 'Messages array cannot be empty. At least one message is required.',
105
- },
106
- ],
107
- isError: true,
108
- };
2
+ const { messages, model, temperature, max_tokens } = request.params.arguments;
3
+ if (!messages?.length) {
4
+ return { content: [{ type: 'text', text: 'Messages array cannot be empty.' }], isError: true };
109
5
  }
110
6
  try {
111
- // Select model with priority:
112
- // 1. User-specified model
113
- // 2. Default model from environment
114
- // 3. Free model with the largest context window (selected automatically)
115
- let model = args.model || defaultModel;
116
- if (!model) {
117
- model = await findSuitableFreeModel(openai);
118
- console.error(`Using auto-selected model: ${model}`);
119
- }
120
- // Truncate messages to fit within context window
121
- const truncatedMessages = truncateMessagesToFit(args.messages, MAX_CONTEXT_TOKENS);
122
- console.error(`Making API call with model: ${model}`);
123
7
  const completion = await openai.chat.completions.create({
124
- model,
125
- messages: truncatedMessages,
126
- temperature: args.temperature ?? 1,
8
+ model: model || defaultModel || 'nvidia/nemotron-nano-12b-v2-vl:free',
9
+ messages,
10
+ temperature: temperature ?? 1,
11
+ ...(max_tokens && { max_tokens }),
127
12
  });
128
- return {
129
- content: [
130
- {
131
- type: 'text',
132
- text: completion.choices[0].message.content || '',
133
- },
134
- ],
135
- };
13
+ return { content: [{ type: 'text', text: completion.choices[0].message.content || '' }] };
136
14
  }
137
15
  catch (error) {
138
- if (error instanceof Error) {
139
- return {
140
- content: [
141
- {
142
- type: 'text',
143
- text: `OpenRouter API error: ${error.message}`,
144
- },
145
- ],
146
- isError: true,
147
- };
148
- }
149
- throw error;
16
+ const msg = error instanceof Error ? error.message : String(error);
17
+ return { content: [{ type: 'text', text: `API error: ${msg}` }], isError: true };
150
18
  }
151
19
  }
152
- //# sourceMappingURL=chat-completion.js.map
@@ -0,0 +1,30 @@
1
+ import OpenAI from 'openai';
2
+ export interface GenerateImageToolRequest {
3
+ prompt: string;
4
+ model?: string;
5
+ save_path?: string;
6
+ }
7
+ export declare function handleGenerateImage(request: {
8
+ params: {
9
+ arguments: GenerateImageToolRequest;
10
+ };
11
+ }, openai: OpenAI): Promise<{
12
+ content: {
13
+ type: string;
14
+ text: string;
15
+ }[];
16
+ isError: boolean;
17
+ } | {
18
+ content: ({
19
+ type: string;
20
+ text: string;
21
+ mimeType?: undefined;
22
+ data?: undefined;
23
+ } | {
24
+ type: string;
25
+ mimeType: string;
26
+ data: string;
27
+ text?: undefined;
28
+ })[];
29
+ isError?: undefined;
30
+ }>;
@@ -0,0 +1,82 @@
1
+ import { promises as fs } from 'fs';
2
+ import { dirname } from 'path';
3
+ const DEFAULT_MODEL = 'google/gemini-2.5-flash-image';
4
+ export async function handleGenerateImage(request, openai) {
5
+ const { prompt, model, save_path } = request.params.arguments;
6
+ if (!prompt?.trim()) {
7
+ return { content: [{ type: 'text', text: 'Prompt is required.' }], isError: true };
8
+ }
9
+ try {
10
+ const completion = await openai.chat.completions.create({
11
+ model: model || DEFAULT_MODEL,
12
+ messages: [{ role: 'user', content: `Generate an image: ${prompt}` }],
13
+ });
14
+ const message = completion.choices[0]?.message;
15
+ if (!message) {
16
+ return { content: [{ type: 'text', text: 'No response from model.' }], isError: true };
17
+ }
18
+ const base64 = extractBase64(message);
19
+ if (base64) {
20
+ if (save_path) {
21
+ const dir = dirname(save_path);
22
+ await fs.mkdir(dir, { recursive: true });
23
+ await fs.writeFile(save_path, Buffer.from(base64.data, 'base64'));
24
+ return {
25
+ content: [
26
+ { type: 'text', text: `Image saved to: ${save_path}` },
27
+ { type: 'image', mimeType: base64.mime, data: base64.data },
28
+ ],
29
+ };
30
+ }
31
+ return { content: [{ type: 'image', mimeType: base64.mime, data: base64.data }] };
32
+ }
33
+ const content = message.content;
34
+ const text = typeof content === 'string' ? content : JSON.stringify(content);
35
+ return { content: [{ type: 'text', text }] };
36
+ }
37
+ catch (error) {
38
+ const msg = error instanceof Error ? error.message : String(error);
39
+ return { content: [{ type: 'text', text: `Error: ${msg}` }], isError: true };
40
+ }
41
+ }
42
+ function extractBase64(message) {
43
+ const images = message.images;
44
+ if (Array.isArray(images) && images.length) {
45
+ for (const img of images) {
46
+ const imageUrl = img.image_url;
47
+ const result = parseDataUrl(imageUrl?.url || img.url);
48
+ if (result)
49
+ return result;
50
+ }
51
+ }
52
+ if (Array.isArray(message.content)) {
53
+ for (const part of message.content) {
54
+ const iu = part.image_url;
55
+ const url = iu?.url || part.url;
56
+ if (url) {
57
+ const r = parseDataUrl(url);
58
+ if (r)
59
+ return r;
60
+ }
61
+ const inline = part.inline_data;
62
+ if (inline?.data) {
63
+ return { data: inline.data, mime: inline.mime_type || 'image/png' };
64
+ }
65
+ if (part.type === 'image' && typeof part.data === 'string') {
66
+ return { data: part.data, mime: part.mime_type || 'image/png' };
67
+ }
68
+ }
69
+ }
70
+ if (typeof message.content === 'string') {
71
+ const match = message.content.match(/data:image\/([^;]+);base64,([A-Za-z0-9+/=]+)/);
72
+ if (match)
73
+ return { data: match[2], mime: `image/${match[1]}` };
74
+ }
75
+ return null;
76
+ }
77
+ function parseDataUrl(url) {
78
+ if (!url?.startsWith('data:'))
79
+ return null;
80
+ const match = url.match(/^data:([^;]+);base64,(.+)$/);
81
+ return match ? { data: match[2], mime: match[1] } : null;
82
+ }
@@ -1,12 +1,12 @@
1
1
  import { ModelCache } from '../model-cache.js';
2
- export interface GetModelInfoToolRequest {
3
- model: string;
4
- }
2
+ import { OpenRouterAPIClient } from '../openrouter-api.js';
5
3
  export declare function handleGetModelInfo(request: {
6
4
  params: {
7
- arguments: GetModelInfoToolRequest;
5
+ arguments: {
6
+ model: string;
7
+ };
8
8
  };
9
- }, modelCache: ModelCache): Promise<{
9
+ }, modelCache: ModelCache, apiClient?: OpenRouterAPIClient): Promise<{
10
10
  content: {
11
11
  type: string;
12
12
  text: string;
@@ -1,44 +1,14 @@
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
- };
1
+ export async function handleGetModelInfo(request, modelCache, apiClient) {
2
+ const { model } = request.params.arguments;
3
+ if (!modelCache.isValid() && apiClient) {
4
+ modelCache.setModels(await apiClient.getModels());
28
5
  }
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;
6
+ if (!modelCache.isValid()) {
7
+ return { content: [{ type: 'text', text: 'No model data available.' }], isError: true };
42
8
  }
9
+ const info = modelCache.get(model);
10
+ if (!info) {
11
+ return { content: [{ type: 'text', text: `Model '${model}' not found.` }], isError: true };
12
+ }
13
+ return { content: [{ type: 'text', text: JSON.stringify(info, null, 2) }] };
43
14
  }
44
- //# sourceMappingURL=get-model-info.js.map
@@ -0,0 +1,11 @@
1
+ export declare function getMaxImageDimension(): number;
2
+ export declare function getImageJpegQuality(): number;
3
+ export declare function getMimeType(filePath: string): string;
4
+ /** Blocks RFC1918, loopback, link-local, CGNAT, metadata (e.g. 169.254.169.254). */
5
+ export declare function isBlockedIPv4(ip: string): boolean;
6
+ /** Resolve hostname and ensure the resolved address is not private/link-local. */
7
+ export declare function assertUrlSafeForFetch(urlString: string): Promise<URL>;
8
+ export declare function fetchHttpImage(urlString: string): Promise<Buffer>;
9
+ export declare function fetchImage(source: string): Promise<Buffer>;
10
+ export declare function optimizeImage(buffer: Buffer): Promise<string>;
11
+ export declare function prepareImageUrl(source: string): Promise<string>;