@stabgan/openrouter-mcp-multimodal 4.5.3 → 4.6.1

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.
@@ -14,7 +14,12 @@ import { handleAnalyzeVideo } from './tool-handlers/analyze-video.js';
14
14
  import { handleGenerateVideo, handleGetVideoStatus, handleGenerateVideoFromImage, } from './tool-handlers/generate-video.js';
15
15
  import { handleRerankDocuments } from './tool-handlers/rerank.js';
16
16
  import { handleHealthCheck } from './tool-handlers/health-check.js';
17
+ import { handleGenerateImageDedicated } from './tool-handlers/generate-image-dedicated.js';
18
+ import { handleTextToSpeech } from './tool-handlers/text-to-speech.js';
19
+ import { handleSpeechToText } from './tool-handlers/speech-to-text.js';
20
+ import { handleStartChatCompletion, handleGetChatCompletionStatus, } from './tool-handlers/async-chat.js';
17
21
  import { TOOL_DESCRIPTIONS } from './tool-descriptions.js';
22
+ import { TOOL_ICONS } from './tool-icons.js';
18
23
  function wrapToolArgs(a) {
19
24
  return { params: { arguments: a ?? {} } };
20
25
  }
@@ -88,8 +93,9 @@ export class ToolHandlers {
88
93
  model: {
89
94
  type: 'string',
90
95
  description: 'Model ID (optional, uses default). Append `:nitro` for the fastest variant, ' +
91
- '`:floor` for the cheapest, or `:exacto` for the best tool-calling accuracy. ' +
92
- 'Example: `openai/gpt-4o:nitro`.',
96
+ '`:floor` for the cheapest, `:free` for the free tier, `:online` for web search, ' +
97
+ 'or `:exacto` for the best tool-calling accuracy. ' +
98
+ 'Example: `openai/gpt-4o:nitro`. Or pass `online: true` for programmatic web search control.',
93
99
  },
94
100
  messages: {
95
101
  type: 'array',
@@ -166,6 +172,68 @@ export class ToolHandlers {
166
172
  required: ['messages'],
167
173
  },
168
174
  },
175
+ {
176
+ name: 'start_chat_completion',
177
+ description: TOOL_DESCRIPTIONS.start_chat_completion,
178
+ annotations: {
179
+ title: 'Start async chat completion',
180
+ readOnlyHint: false,
181
+ destructiveHint: false,
182
+ idempotentHint: false,
183
+ openWorldHint: true,
184
+ },
185
+ inputSchema: {
186
+ type: 'object',
187
+ properties: {
188
+ model: { type: 'string', description: 'Model ID (same as chat_completion).' },
189
+ messages: {
190
+ type: 'array',
191
+ minItems: 1,
192
+ items: {
193
+ type: 'object',
194
+ properties: {
195
+ role: { type: 'string', enum: ['system', 'user', 'assistant'] },
196
+ content: {
197
+ oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'object' } }],
198
+ },
199
+ },
200
+ required: ['role', 'content'],
201
+ },
202
+ },
203
+ temperature: { type: 'number', minimum: 0, maximum: 2 },
204
+ max_tokens: { type: 'number', minimum: 1 },
205
+ provider: { type: 'object' },
206
+ include_reasoning: { type: 'boolean' },
207
+ online: { type: 'boolean' },
208
+ web_max_results: { type: 'number', minimum: 1 },
209
+ cache: { type: 'boolean' },
210
+ cache_ttl: { type: 'string' },
211
+ cache_clear: { type: 'boolean' },
212
+ },
213
+ required: ['messages'],
214
+ },
215
+ },
216
+ {
217
+ name: 'get_chat_completion_status',
218
+ description: TOOL_DESCRIPTIONS.get_chat_completion_status,
219
+ annotations: {
220
+ title: 'Get async chat completion status',
221
+ readOnlyHint: true,
222
+ destructiveHint: false,
223
+ idempotentHint: true,
224
+ openWorldHint: false,
225
+ },
226
+ inputSchema: {
227
+ type: 'object',
228
+ properties: {
229
+ job_id: {
230
+ type: 'string',
231
+ description: 'The job_id returned by start_chat_completion.',
232
+ },
233
+ },
234
+ required: ['job_id'],
235
+ },
236
+ },
169
237
  {
170
238
  name: 'analyze_image',
171
239
  description: TOOL_DESCRIPTIONS.analyze_image,
@@ -394,6 +462,69 @@ export class ToolHandlers {
394
462
  required: ['prompt'],
395
463
  },
396
464
  },
465
+ {
466
+ name: 'generate_image_dedicated',
467
+ description: TOOL_DESCRIPTIONS.generate_image_dedicated,
468
+ annotations: {
469
+ title: 'Generate image (dedicated API)',
470
+ readOnlyHint: false,
471
+ destructiveHint: false,
472
+ idempotentHint: false,
473
+ openWorldHint: true,
474
+ },
475
+ inputSchema: {
476
+ type: 'object',
477
+ properties: {
478
+ prompt: {
479
+ type: 'string',
480
+ description: 'Text prompt describing the image to generate.',
481
+ },
482
+ model: {
483
+ type: 'string',
484
+ description: 'Image model ID. Default: google/gemini-2.5-flash-image. Browse available at https://openrouter.ai/collections/image-models',
485
+ },
486
+ resolution: {
487
+ type: 'string',
488
+ enum: ['512', '0.5K', '1K', '2K', '4K'],
489
+ description: 'Normalized resolution tier. Provider maps to closest supported size.',
490
+ },
491
+ aspect_ratio: {
492
+ type: 'string',
493
+ description: 'Aspect ratio (e.g. "1:1", "16:9", "9:16", "4:3", "21:9").',
494
+ },
495
+ quality: {
496
+ type: 'string',
497
+ enum: ['auto', 'low', 'medium', 'high'],
498
+ description: 'Image quality level.',
499
+ },
500
+ output_format: {
501
+ type: 'string',
502
+ enum: ['png', 'jpeg', 'webp', 'svg'],
503
+ description: 'Output image format.',
504
+ },
505
+ n: {
506
+ type: 'number',
507
+ minimum: 1,
508
+ maximum: 10,
509
+ description: 'Number of images to generate (model-dependent, default 1).',
510
+ },
511
+ input_references: {
512
+ type: 'array',
513
+ items: { type: 'string' },
514
+ description: 'Reference images for image-to-image workflows. Each entry: local path, http(s) URL, or data URL.',
515
+ },
516
+ save_path: { type: 'string', description: 'Save generated image to this path.' },
517
+ provider: {
518
+ type: 'object',
519
+ description: 'Provider routing overrides (order, sort, allow_fallbacks, etc.).',
520
+ },
521
+ cache: { type: 'boolean' },
522
+ cache_ttl: { type: 'string' },
523
+ cache_clear: { type: 'boolean' },
524
+ },
525
+ required: ['prompt'],
526
+ },
527
+ },
397
528
  {
398
529
  name: 'generate_audio',
399
530
  description: TOOL_DESCRIPTIONS.generate_audio,
@@ -416,6 +547,97 @@ export class ToolHandlers {
416
547
  required: ['prompt'],
417
548
  },
418
549
  },
550
+ {
551
+ name: 'text_to_speech',
552
+ description: TOOL_DESCRIPTIONS.text_to_speech,
553
+ annotations: {
554
+ title: 'Text to speech (dedicated API)',
555
+ readOnlyHint: false,
556
+ destructiveHint: false,
557
+ idempotentHint: false,
558
+ openWorldHint: true,
559
+ },
560
+ inputSchema: {
561
+ type: 'object',
562
+ properties: {
563
+ input: {
564
+ type: 'string',
565
+ description: 'Text to convert to speech.',
566
+ },
567
+ model: {
568
+ type: 'string',
569
+ description: 'TTS model. Default: openai/gpt-4o-mini-tts-2025-12-15. Also: google/gemini-flash-tts, mistral/voxtral-mini-tts.',
570
+ },
571
+ voice: {
572
+ type: 'string',
573
+ description: 'Voice ID (model-specific). Default: alloy. OpenAI voices: alloy, echo, fable, onyx, nova, shimmer.',
574
+ },
575
+ response_format: {
576
+ type: 'string',
577
+ enum: ['mp3', 'opus', 'aac', 'flac', 'wav', 'pcm'],
578
+ description: 'Output audio format. Default: mp3.',
579
+ },
580
+ speed: {
581
+ type: 'number',
582
+ minimum: 0.25,
583
+ maximum: 4.0,
584
+ description: 'Speed of speech (0.25 to 4.0). Default: 1.0.',
585
+ },
586
+ instructions: {
587
+ type: 'string',
588
+ description: 'Tone/style instructions (e.g. "speak in a warm, friendly tone"). OpenAI models only.',
589
+ },
590
+ save_path: { type: 'string', description: 'Save audio to this path.' },
591
+ cache: { type: 'boolean' },
592
+ cache_ttl: { type: 'string' },
593
+ cache_clear: { type: 'boolean' },
594
+ },
595
+ required: ['input'],
596
+ },
597
+ },
598
+ {
599
+ name: 'speech_to_text',
600
+ description: TOOL_DESCRIPTIONS.speech_to_text,
601
+ annotations: {
602
+ title: 'Speech to text (dedicated API)',
603
+ readOnlyHint: true,
604
+ destructiveHint: false,
605
+ idempotentHint: false,
606
+ openWorldHint: true,
607
+ },
608
+ inputSchema: {
609
+ type: 'object',
610
+ properties: {
611
+ audio_path: {
612
+ type: 'string',
613
+ description: 'Audio file: local path (sandboxed), http(s) URL, or base64 data URL. Formats: mp3, wav, flac, ogg, webm, mp4.',
614
+ },
615
+ model: {
616
+ type: 'string',
617
+ description: 'STT model. Default: openai/whisper-1. Also: openai/gpt-4o-transcribe, openai/gpt-4o-mini-transcribe.',
618
+ },
619
+ language: {
620
+ type: 'string',
621
+ description: 'ISO-639-1 language code (e.g. "en", "es", "fr"). Improves accuracy.',
622
+ },
623
+ response_format: {
624
+ type: 'string',
625
+ enum: ['json', 'text', 'srt', 'verbose_json', 'vtt'],
626
+ description: 'Output format for transcription. Default: json.',
627
+ },
628
+ temperature: {
629
+ type: 'number',
630
+ minimum: 0,
631
+ maximum: 1,
632
+ description: 'Sampling temperature for transcription (0-1).',
633
+ },
634
+ cache: { type: 'boolean' },
635
+ cache_ttl: { type: 'string' },
636
+ cache_clear: { type: 'boolean' },
637
+ },
638
+ required: ['audio_path'],
639
+ },
640
+ },
419
641
  {
420
642
  name: 'generate_video',
421
643
  description: TOOL_DESCRIPTIONS.generate_video,
@@ -566,7 +788,10 @@ export class ToolHandlers {
566
788
  ],
567
789
  },
568
790
  },
569
- ],
791
+ ].map((tool) => ({
792
+ ...tool,
793
+ icons: TOOL_ICONS[tool.name],
794
+ })),
570
795
  }));
571
796
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
572
797
  const { name, arguments: args } = request.params;
@@ -581,6 +806,10 @@ export class ToolHandlers {
581
806
  switch (name) {
582
807
  case 'chat_completion':
583
808
  return handleChatCompletion(wrapToolArgs(args), this.openai, this.defaultModel);
809
+ case 'start_chat_completion':
810
+ return handleStartChatCompletion(wrapToolArgs(args), this.openai, this.defaultModel);
811
+ case 'get_chat_completion_status':
812
+ return handleGetChatCompletionStatus(wrapToolArgs(args));
584
813
  case 'analyze_image':
585
814
  return handleAnalyzeImage(wrapToolArgs(args), this.openai, this.defaultModel);
586
815
  case 'analyze_audio':
@@ -595,8 +824,14 @@ export class ToolHandlers {
595
824
  return handleValidateModel(wrapToolArgs(args), this.modelCache, this.apiClient);
596
825
  case 'generate_image':
597
826
  return handleGenerateImage(wrapToolArgs(args), this.openai);
827
+ case 'generate_image_dedicated':
828
+ return handleGenerateImageDedicated(wrapToolArgs(args), this.apiClient);
598
829
  case 'generate_audio':
599
830
  return handleGenerateAudio(wrapToolArgs(args), this.openai);
831
+ case 'text_to_speech':
832
+ return handleTextToSpeech(wrapToolArgs(args), this.apiClient);
833
+ case 'speech_to_text':
834
+ return handleSpeechToText(wrapToolArgs(args), this.apiClient);
600
835
  case 'generate_video':
601
836
  return handleGenerateVideo(wrapToolArgs(args), this.apiClient, buildProgressHook(this.server, extractProgressToken(request)));
602
837
  case 'generate_video_from_image':
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Tool icons for MCP 2025-11-25+ clients that render icons in the tool list.
3
+ * Each icon is a minimal SVG data URI — no external hosting needed.
4
+ *
5
+ * Clients that don't support icons simply ignore the `icons` field.
6
+ */
7
+ export type ToolIcon = Array<{
8
+ src: string;
9
+ mimeType: string;
10
+ sizes: string[];
11
+ }>;
12
+ /** Map of tool name → icons array for use in tool definitions. */
13
+ export declare const TOOL_ICONS: Record<string, ToolIcon>;
14
+ /** Server icon for use in the Server initialization metadata. */
15
+ export declare const SERVER_ICON: ToolIcon;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Tool icons for MCP 2025-11-25+ clients that render icons in the tool list.
3
+ * Each icon is a minimal SVG data URI — no external hosting needed.
4
+ *
5
+ * Clients that don't support icons simply ignore the `icons` field.
6
+ */
7
+ /** Encode an SVG string as a data URI. */
8
+ function svgDataUri(svg) {
9
+ return `data:image/svg+xml,${encodeURIComponent(svg.trim())}`;
10
+ }
11
+ // Minimal 24x24 SVG icons using simple shapes
12
+ const ICONS = {
13
+ chat: svgDataUri(`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#6366f1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`),
14
+ chatAsync: svgDataUri(`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#8b5cf6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><circle cx="17" cy="7" r="3" fill="#8b5cf6"/></svg>`),
15
+ image: svgDataUri(`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg>`),
16
+ audio: svgDataUri(`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>`),
17
+ mic: svgDataUri(`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>`),
18
+ video: svgDataUri(`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ec4899" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 8-6 4 6 4V8Z"/><rect x="2" y="6" width="14" height="12" rx="2"/></svg>`),
19
+ search: svgDataUri(`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#06b6d4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>`),
20
+ info: svgDataUri(`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#06b6d4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>`),
21
+ check: svgDataUri(`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/></svg>`),
22
+ rerank: svgDataUri(`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#a855f7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg>`),
23
+ health: svgDataUri(`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>`),
24
+ speaker: svgDataUri(`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#f97316" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/></svg>`),
25
+ poll: svgDataUri(`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#64748b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></svg>`),
26
+ };
27
+ function icon(src) {
28
+ return [{ src, mimeType: 'image/svg+xml', sizes: ['24x24'] }];
29
+ }
30
+ /** Map of tool name → icons array for use in tool definitions. */
31
+ export const TOOL_ICONS = {
32
+ chat_completion: icon(ICONS.chat),
33
+ start_chat_completion: icon(ICONS.chatAsync),
34
+ get_chat_completion_status: icon(ICONS.poll),
35
+ analyze_image: icon(ICONS.image),
36
+ analyze_audio: icon(ICONS.audio),
37
+ analyze_video: icon(ICONS.video),
38
+ search_models: icon(ICONS.search),
39
+ get_model_info: icon(ICONS.info),
40
+ validate_model: icon(ICONS.check),
41
+ generate_image: icon(ICONS.image),
42
+ generate_image_dedicated: icon(ICONS.image),
43
+ generate_audio: icon(ICONS.audio),
44
+ text_to_speech: icon(ICONS.speaker),
45
+ speech_to_text: icon(ICONS.mic),
46
+ generate_video: icon(ICONS.video),
47
+ generate_video_from_image: icon(ICONS.video),
48
+ get_video_status: icon(ICONS.poll),
49
+ rerank_documents: icon(ICONS.rerank),
50
+ health_check: icon(ICONS.health),
51
+ };
52
+ /** Server icon for use in the Server initialization metadata. */
53
+ export const SERVER_ICON = [
54
+ {
55
+ src: svgDataUri(`<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="none"><rect width="48" height="48" rx="12" fill="#1a1a2e"/><path d="M14 24h20M24 14v20" stroke="#6366f1" stroke-width="3" stroke-linecap="round"/><circle cx="24" cy="24" r="8" stroke="#22c55e" stroke-width="2"/><circle cx="24" cy="24" r="3" fill="#6366f1"/></svg>`),
56
+ mimeType: 'image/svg+xml',
57
+ sizes: ['48x48'],
58
+ },
59
+ ];
package/dist/version.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * Bumped in lockstep with package.json / server.json / smithery.yaml /
7
7
  * scripts/build-manifest.mjs during release prep.
8
8
  */
9
- export declare const SERVER_VERSION = "4.5.3";
9
+ export declare const SERVER_VERSION = "4.6.1";
10
10
  /**
11
11
  * MCP protocol version our SDK speaks. Hardcoded to match the version
12
12
  * bundled with `@modelcontextprotocol/sdk`; update when upgrading the
package/dist/version.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * Bumped in lockstep with package.json / server.json / smithery.yaml /
7
7
  * scripts/build-manifest.mjs during release prep.
8
8
  */
9
- export const SERVER_VERSION = '4.5.3';
9
+ export const SERVER_VERSION = '4.6.1';
10
10
  /**
11
11
  * MCP protocol version our SDK speaks. Hardcoded to match the version
12
12
  * bundled with `@modelcontextprotocol/sdk`; update when upgrading the
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@stabgan/openrouter-mcp-multimodal",
3
- "version": "4.5.3",
3
+ "version": "4.6.1",
4
4
  "mcpName": "io.github.stabgan/openrouter-multimodal",
5
- "description": "MCP server for OpenRouter with text chat, image analysis + generation, audio analysis + generation, video analysis, and video generation (Veo 3.1 / Sora 2 Pro / Seedance / Wan)",
5
+ "description": "MCP server for OpenRouter — chat with 300+ LLMs, analyze images/audio/video, generate images (dedicated API), TTS/STT, video generation (Veo 3.1 / Seedance / Wan), async completions, response caching",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "bin": {