converse-mcp-server 1.0.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.
@@ -0,0 +1,441 @@
1
+ /**
2
+ * Google (Gemini) Provider
3
+ *
4
+ * Provider implementation for Google Gemini models using the official @google/genai SDK v1.11+.
5
+ * Implements the unified interface: async invoke(messages, options) => { content, stop_reason, rawResponse }
6
+ */
7
+
8
+ import { GoogleGenAI } from '@google/genai';
9
+ import { debugLog, debugError } from '../utils/console.js';
10
+
11
+ // Define supported Gemini models with their capabilities
12
+ const SUPPORTED_MODELS = {
13
+ 'gemini-2.0-flash': {
14
+ modelName: 'gemini-2.0-flash',
15
+ friendlyName: 'Gemini (Flash 2.0)',
16
+ contextWindow: 1048576, // 1M tokens
17
+ maxOutputTokens: 65536,
18
+ supportsStreaming: true,
19
+ supportsImages: true,
20
+ supportsTemperature: true,
21
+ supportsThinking: true,
22
+ maxThinkingTokens: 24576,
23
+ timeout: 300000,
24
+ description: 'Gemini 2.0 Flash (1M context) - Latest fast model with experimental thinking, supports audio/video input',
25
+ aliases: ['flash-2.0', 'flash2']
26
+ },
27
+ 'gemini-2.0-flash-lite': {
28
+ modelName: 'gemini-2.0-flash-lite',
29
+ friendlyName: 'Gemini (Flash Lite 2.0)',
30
+ contextWindow: 1048576, // 1M tokens
31
+ maxOutputTokens: 65536,
32
+ supportsStreaming: true,
33
+ supportsImages: false,
34
+ supportsTemperature: true,
35
+ supportsThinking: false,
36
+ maxThinkingTokens: 0,
37
+ timeout: 300000,
38
+ description: 'Gemini 2.0 Flash Lite (1M context) - Lightweight fast model, text-only',
39
+ aliases: ['flashlite', 'flash-lite']
40
+ },
41
+ 'gemini-2.5-flash': {
42
+ modelName: 'gemini-2.5-flash',
43
+ friendlyName: 'Gemini (Flash 2.5)',
44
+ contextWindow: 1048576, // 1M tokens
45
+ maxOutputTokens: 65536,
46
+ supportsStreaming: true,
47
+ supportsImages: true,
48
+ supportsTemperature: true,
49
+ supportsThinking: true,
50
+ maxThinkingTokens: 24576,
51
+ timeout: 300000,
52
+ description: 'Ultra-fast (1M context) - Quick analysis, simple queries, rapid iterations',
53
+ aliases: ['flash', 'flash2.5', 'gemini-flash', 'gemini-flash-2.5']
54
+ },
55
+ 'gemini-2.5-pro': {
56
+ modelName: 'gemini-2.5-pro',
57
+ friendlyName: 'Gemini (Pro 2.5)',
58
+ contextWindow: 1048576, // 1M tokens
59
+ maxOutputTokens: 65536,
60
+ supportsStreaming: true,
61
+ supportsImages: true,
62
+ supportsTemperature: true,
63
+ supportsThinking: true,
64
+ maxThinkingTokens: 32768,
65
+ timeout: 300000,
66
+ description: 'Deep reasoning + thinking mode (1M context) - Complex problems, architecture, deep analysis',
67
+ aliases: ['pro', 'gemini pro', 'gemini-pro', 'gemini']
68
+ }
69
+ };
70
+
71
+ // Thinking mode budget percentages
72
+ const THINKING_BUDGETS = {
73
+ minimal: 0.005, // 0.5% of max - minimal thinking for fast responses
74
+ low: 0.08, // 8% of max - light reasoning tasks
75
+ medium: 0.33, // 33% of max - balanced reasoning (default)
76
+ high: 0.67, // 67% of max - complex analysis
77
+ max: 1.0 // 100% of max - full thinking budget
78
+ };
79
+
80
+ /**
81
+ * Custom error class for Google provider errors
82
+ */
83
+ class GoogleProviderError extends Error {
84
+ constructor(message, code, originalError = null) {
85
+ super(message);
86
+ this.name = 'GoogleProviderError';
87
+ this.code = code;
88
+ this.originalError = originalError;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Resolve model name to canonical form, including aliases
94
+ */
95
+ function resolveModelName(modelName) {
96
+ const modelNameLower = modelName.toLowerCase();
97
+
98
+ // Check exact matches first
99
+ for (const [supportedModel] of Object.entries(SUPPORTED_MODELS)) {
100
+ if (supportedModel.toLowerCase() === modelNameLower) {
101
+ return supportedModel;
102
+ }
103
+ }
104
+
105
+ // Check aliases
106
+ for (const [supportedModel, config] of Object.entries(SUPPORTED_MODELS)) {
107
+ if (config.aliases) {
108
+ for (const alias of config.aliases) {
109
+ if (alias.toLowerCase() === modelNameLower) {
110
+ return supportedModel;
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ // Return as-is if not found (let Google API handle unknown models)
117
+ return modelName;
118
+ }
119
+
120
+ /**
121
+ * Validate Google API key format
122
+ */
123
+ function validateApiKey(apiKey) {
124
+ if (!apiKey || typeof apiKey !== 'string') {
125
+ return false;
126
+ }
127
+
128
+ // Google API keys are typically long strings, usually starting with specific patterns
129
+ // They are generally 39+ characters long
130
+ return apiKey.length >= 20;
131
+ }
132
+
133
+ /**
134
+ * Convert messages to Google Gemini format
135
+ */
136
+ function convertMessagesToGemini(messages) {
137
+ if (!Array.isArray(messages)) {
138
+ throw new GoogleProviderError('Messages must be an array', 'INVALID_MESSAGES');
139
+ }
140
+
141
+ const contents = [];
142
+ let systemPrompt = null;
143
+
144
+ for (const [index, msg] of messages.entries()) {
145
+ if (!msg || typeof msg !== 'object') {
146
+ throw new GoogleProviderError(`Message at index ${index} must be an object`, 'INVALID_MESSAGE');
147
+ }
148
+
149
+ const { role, content } = msg;
150
+
151
+ if (!role || !['system', 'user', 'assistant'].includes(role)) {
152
+ throw new GoogleProviderError(`Invalid role "${role}" at message index ${index}`, 'INVALID_ROLE');
153
+ }
154
+
155
+ if (!content) {
156
+ throw new GoogleProviderError(`Message content is required at index ${index}`, 'MISSING_CONTENT');
157
+ }
158
+
159
+ if (role === 'system') {
160
+ // Google Gemini handles system prompts differently - they are typically prepended to the first user message
161
+ systemPrompt = content;
162
+ } else if (role === 'user') {
163
+ // Combine system prompt with user message if present
164
+ const userContent = systemPrompt ? `${systemPrompt}\n\n${content}` : content;
165
+ contents.push({
166
+ role: 'user',
167
+ parts: [{ text: userContent }]
168
+ });
169
+ systemPrompt = null; // Only use system prompt once
170
+ } else if (role === 'assistant') {
171
+ contents.push({
172
+ role: 'model', // Google uses 'model' instead of 'assistant'
173
+ parts: [{ text: content }]
174
+ });
175
+ }
176
+ }
177
+
178
+ return contents;
179
+ }
180
+
181
+ /**
182
+ * Calculate thinking budget for models that support it
183
+ */
184
+ function calculateThinkingBudget(modelConfig, reasoningEffort) {
185
+ if (!modelConfig.supportsThinking || !modelConfig.maxThinkingTokens) {
186
+ return 0;
187
+ }
188
+
189
+ const budget = THINKING_BUDGETS[reasoningEffort] || THINKING_BUDGETS.medium;
190
+ return Math.floor(modelConfig.maxThinkingTokens * budget);
191
+ }
192
+
193
+ /**
194
+ * Check if error is retryable
195
+ */
196
+ function isErrorRetryable(error) {
197
+ const errorStr = String(error).toLowerCase();
198
+
199
+ // Non-retryable errors
200
+ const nonRetryableIndicators = [
201
+ 'quota exceeded',
202
+ 'quota_exceeded',
203
+ 'resource exhausted',
204
+ 'resource_exhausted',
205
+ 'context length',
206
+ 'token limit',
207
+ 'request too large',
208
+ 'invalid request',
209
+ 'invalid_request',
210
+ 'read timeout',
211
+ 'timeout error',
212
+ '408',
213
+ 'deadline exceeded'
214
+ ];
215
+
216
+ if (nonRetryableIndicators.some(indicator => errorStr.includes(indicator))) {
217
+ return false;
218
+ }
219
+
220
+ // Retryable errors
221
+ const retryableIndicators = [
222
+ 'connection',
223
+ 'network',
224
+ 'temporary',
225
+ 'unavailable',
226
+ 'retry',
227
+ 'internal error',
228
+ '429',
229
+ '500',
230
+ '502',
231
+ '503',
232
+ '504',
233
+ 'ssl',
234
+ 'handshake'
235
+ ];
236
+
237
+ return retryableIndicators.some(indicator => errorStr.includes(indicator));
238
+ }
239
+
240
+ /**
241
+ * Retry with progressive delays
242
+ */
243
+ async function retryWithBackoff(fn, maxRetries = 4) {
244
+ const retryDelays = [1000, 3000, 5000, 8000]; // Progressive delays in ms
245
+ let lastError;
246
+
247
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
248
+ try {
249
+ return await fn();
250
+ } catch (error) {
251
+ lastError = error;
252
+
253
+ // If this is the last attempt or not retryable, give up
254
+ if (attempt === maxRetries - 1 || !isErrorRetryable(error)) {
255
+ break;
256
+ }
257
+
258
+ // Wait before retrying
259
+ const delay = retryDelays[attempt];
260
+ debugLog(`[Google] Retrying after ${delay}ms (attempt ${attempt + 1}/${maxRetries}):`, error.message);
261
+ await new Promise(resolve => setTimeout(resolve, delay));
262
+ }
263
+ }
264
+
265
+ throw lastError;
266
+ }
267
+
268
+ /**
269
+ * Main Google provider implementation
270
+ */
271
+ export const googleProvider = {
272
+ /**
273
+ * Unified provider interface: invoke messages with options
274
+ * @param {Array} messages - Array of message objects with role and content
275
+ * @param {Object} options - Configuration options
276
+ * @returns {Object} - { content, stop_reason, rawResponse }
277
+ */
278
+ async invoke(messages, options = {}) {
279
+ const {
280
+ model = 'gemini-2.5-flash',
281
+ temperature = 0.7,
282
+ maxTokens = null,
283
+ stream: _unused_stream = false, // Acknowledged but not used yet
284
+ reasoningEffort = 'medium',
285
+ config,
286
+ ...otherOptions
287
+ } = options;
288
+
289
+ // Validate API key
290
+ if (!config?.apiKeys?.google) {
291
+ throw new GoogleProviderError('Google API key not configured', 'MISSING_API_KEY');
292
+ }
293
+
294
+ if (!validateApiKey(config.apiKeys.google)) {
295
+ throw new GoogleProviderError('Invalid Google API key format', 'INVALID_API_KEY');
296
+ }
297
+
298
+ // Initialize Google AI client
299
+ const genAI = new GoogleGenAI({apiKey: config.apiKeys.google});
300
+
301
+ // Resolve model name
302
+ const resolvedModel = resolveModelName(model);
303
+ const modelConfig = SUPPORTED_MODELS[resolvedModel] || {};
304
+
305
+ // Convert messages to Google format
306
+ const geminiContents = convertMessagesToGemini(messages);
307
+
308
+ // Note: No need to get model instance, we use genAI.models.generateContent directly
309
+
310
+ // Build generation config
311
+ const generationConfig = {};
312
+
313
+ // Add temperature if model supports it
314
+ if (modelConfig.supportsTemperature !== false && temperature !== undefined) {
315
+ generationConfig.temperature = Math.max(0, Math.min(2, temperature));
316
+ }
317
+
318
+ // Add max tokens if specified
319
+ if (maxTokens) {
320
+ generationConfig.maxOutputTokens = Math.min(maxTokens, modelConfig.maxOutputTokens || 65536);
321
+ }
322
+
323
+ // Add thinking configuration for models that support it
324
+ if (modelConfig.supportsThinking && reasoningEffort) {
325
+ const thinkingBudget = calculateThinkingBudget(modelConfig, reasoningEffort);
326
+ if (thinkingBudget > 0) {
327
+ generationConfig.thinkingConfig = { thinkingBudget };
328
+ }
329
+ }
330
+
331
+ try {
332
+ debugLog(`[Google] Calling ${resolvedModel} with ${messages.length} messages`);
333
+
334
+ const startTime = Date.now();
335
+
336
+ // Make the API call with retry logic
337
+ const response = await retryWithBackoff(async () => {
338
+ return await genAI.models.generateContent({
339
+ model: resolvedModel,
340
+ contents: geminiContents,
341
+ config: generationConfig
342
+ });
343
+ });
344
+
345
+ const responseTime = Date.now() - startTime;
346
+ debugLog(`[Google] Response received in ${responseTime}ms`);
347
+
348
+ // Extract response data using the new SDK format
349
+ const content = response.text;
350
+ if (!content) {
351
+ throw new GoogleProviderError('No text content received from Google', 'NO_RESPONSE_CONTENT');
352
+ }
353
+
354
+ // Extract usage information from the new SDK format
355
+ const usage = {
356
+ input_tokens: response.usageMetadata?.promptTokenCount || 0,
357
+ output_tokens: response.usageMetadata?.candidatesTokenCount || 0,
358
+ total_tokens: response.usageMetadata?.totalTokenCount || 0
359
+ };
360
+
361
+ // Extract finish reason from candidates
362
+ const finishReason = response.candidates?.[0]?.finishReason || 'STOP';
363
+
364
+ // Return unified response format
365
+ return {
366
+ content,
367
+ stop_reason: finishReason,
368
+ rawResponse: response,
369
+ metadata: {
370
+ model: resolvedModel,
371
+ usage,
372
+ response_time_ms: responseTime,
373
+ finish_reason: finishReason,
374
+ reasoning_effort: modelConfig.supportsThinking ? reasoningEffort : null,
375
+ provider: 'google'
376
+ }
377
+ };
378
+
379
+ } catch (error) {
380
+ debugError('[Google] Error during API call:', error);
381
+
382
+ // Handle specific Google errors
383
+ if (error.message?.includes('quota') || error.message?.includes('QUOTA_EXCEEDED')) {
384
+ throw new GoogleProviderError('Google API quota exceeded', 'QUOTA_EXCEEDED', error);
385
+ } else if (error.message?.includes('API_KEY_INVALID') || error.message?.includes('invalid api key')) {
386
+ throw new GoogleProviderError('Invalid Google API key', 'INVALID_API_KEY', error);
387
+ } else if (error.message?.includes('MODEL_NOT_FOUND')) {
388
+ throw new GoogleProviderError(`Model ${resolvedModel} not found`, 'MODEL_NOT_FOUND', error);
389
+ } else if (error.message?.includes('CONTEXT_LENGTH_EXCEEDED')) {
390
+ throw new GoogleProviderError('Context length exceeded for model', 'CONTEXT_LENGTH_EXCEEDED', error);
391
+ } else if (error.message?.includes('SAFETY')) {
392
+ throw new GoogleProviderError('Content blocked by safety filters', 'SAFETY_ERROR', error);
393
+ } else if (error.message?.includes('RATE_LIMIT_EXCEEDED')) {
394
+ throw new GoogleProviderError('Google rate limit exceeded', 'RATE_LIMIT_EXCEEDED', error);
395
+ }
396
+
397
+ // Generic error handling
398
+ throw new GoogleProviderError(
399
+ `Google API error: ${error.message || 'Unknown error'}`,
400
+ 'API_ERROR',
401
+ error
402
+ );
403
+ }
404
+ },
405
+
406
+ /**
407
+ * Validate configuration for Google provider
408
+ * @param {Object} config - Configuration object
409
+ * @returns {boolean} - True if configuration is valid
410
+ */
411
+ validateConfig(config) {
412
+ return !!(config?.apiKeys?.google && validateApiKey(config.apiKeys.google));
413
+ },
414
+
415
+ /**
416
+ * Check if provider is available with current configuration
417
+ * @param {Object} config - Configuration object
418
+ * @returns {boolean} - True if provider is available
419
+ */
420
+ isAvailable(config) {
421
+ return this.validateConfig(config);
422
+ },
423
+
424
+ /**
425
+ * Get supported models
426
+ * @returns {Object} - Map of supported models and their configurations
427
+ */
428
+ getSupportedModels() {
429
+ return SUPPORTED_MODELS;
430
+ },
431
+
432
+ /**
433
+ * Get model configuration
434
+ * @param {string} modelName - Model name
435
+ * @returns {Object|null} - Model configuration or null if not found
436
+ */
437
+ getModelConfig(modelName) {
438
+ const resolved = resolveModelName(modelName);
439
+ return SUPPORTED_MODELS[resolved] || null;
440
+ }
441
+ };
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Provider Registry
3
+ *
4
+ * Central registry for all AI providers following functional architecture.
5
+ * Each provider implements: async invoke(messages, options) => { content, stop_reason, rawResponse }
6
+ */
7
+
8
+ // Import individual providers (will be implemented in subsequent tasks)
9
+ import { openaiProvider } from './openai.js';
10
+ import { xaiProvider } from './xai.js';
11
+ import { googleProvider } from './google.js';
12
+
13
+ /**
14
+ * Provider registry map
15
+ * Each provider must implement the unified interface:
16
+ * - invoke(messages, options): Main invocation method
17
+ * - validateConfig(config): Configuration validation
18
+ * - isAvailable(config): Availability check
19
+ */
20
+ const providers = {
21
+ // Will be populated by individual provider modules
22
+ openai: openaiProvider,
23
+ xai: xaiProvider,
24
+ google: googleProvider,
25
+ };
26
+
27
+ /**
28
+ * Get all available providers
29
+ * @returns {object} Map of provider name to provider implementation
30
+ */
31
+ export function getProviders() {
32
+ return providers;
33
+ }
34
+
35
+ /**
36
+ * Get a specific provider by name
37
+ * @param {string} name - Provider name
38
+ * @returns {object|null} Provider implementation or null if not found
39
+ */
40
+ export function getProvider(name) {
41
+ return providers[name] || null;
42
+ }
43
+
44
+ /**
45
+ * Register a new provider
46
+ * @param {string} name - Provider name
47
+ * @param {object} provider - Provider implementation
48
+ */
49
+ export function registerProvider(name, provider) {
50
+ // Validate provider interface
51
+ if (!provider.invoke || typeof provider.invoke !== 'function') {
52
+ throw new Error(`Provider ${name} must implement invoke() method`);
53
+ }
54
+
55
+ providers[name] = provider;
56
+ }
57
+
58
+ /**
59
+ * Get list of available provider names
60
+ * @param {object} config - Configuration object
61
+ * @returns {string[]} Array of available provider names
62
+ */
63
+ export function getAvailableProviders(config) {
64
+ return Object.keys(providers).filter(name => {
65
+ const provider = providers[name];
66
+ return provider.isAvailable && provider.isAvailable(config);
67
+ });
68
+ }
69
+
70
+ /**
71
+ * Unified provider interface validation
72
+ * @param {object} provider - Provider to validate
73
+ * @returns {boolean} True if provider implements required interface
74
+ */
75
+ export function validateProviderInterface(provider) {
76
+ const requiredMethods = ['invoke'];
77
+ // const optionalMethods = ['validateConfig', 'isAvailable'];
78
+
79
+ // Check required methods
80
+ for (const method of requiredMethods) {
81
+ if (!provider[method] || typeof provider[method] !== 'function') {
82
+ return false;
83
+ }
84
+ }
85
+
86
+ return true;
87
+ }