converse-mcp-server 2.7.0 → 2.8.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "converse-mcp-server",
3
- "version": "2.7.0",
3
+ "version": "2.8.1",
4
4
  "description": "Converse MCP Server - Converse with other LLMs with chat and consensus tools",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -94,11 +94,12 @@
94
94
  ".env.example"
95
95
  ],
96
96
  "dependencies": {
97
+ "@anthropic-ai/claude-agent-sdk": "^0.1.51",
97
98
  "@anthropic-ai/sdk": "^0.70.0",
98
99
  "@google/genai": "^1.30.0",
99
100
  "@mistralai/mistralai": "^1.10.0",
100
101
  "@modelcontextprotocol/sdk": "^1.22.0",
101
- "@openai/codex-sdk": "^0.58.0",
102
+ "@openai/codex-sdk": "^0.63.0",
102
103
  "ai": "^5.0.101",
103
104
  "ai-sdk-provider-gemini-cli": "^1.4.0",
104
105
  "cors": "^2.8.5",
@@ -0,0 +1,482 @@
1
+ /**
2
+ * Claude SDK Provider
3
+ *
4
+ * Provider implementation for Anthropic's Claude models using the @anthropic-ai/claude-agent-sdk.
5
+ * Implements the unified interface: async invoke(messages, options) => { content, stop_reason, rawResponse }
6
+ *
7
+ * Key differences from traditional providers:
8
+ * - Uses Claude Code CLI authentication (via `claude login`) - NOT API keys
9
+ * - Converts message arrays to single prompts (SDK expects prompts, not message history)
10
+ * - Spawns local process (bundled CLI binary) for execution
11
+ * - Requires Claude Code authentication (NOT ANTHROPIC_API_KEY)
12
+ *
13
+ * @see agent-sdk/typescript.md for SDK reference documentation
14
+ */
15
+
16
+ import { debugLog, debugError } from '../utils/console.js';
17
+ import { ProviderError, ErrorCodes, StopReasons } from './interface.js';
18
+
19
+ // Supported Claude SDK models with their configurations
20
+ const SUPPORTED_MODELS = {
21
+ claude: {
22
+ modelName: 'claude',
23
+ friendlyName: 'Claude (via Agent SDK)',
24
+ contextWindow: 200000,
25
+ maxOutputTokens: 8192,
26
+ supportsStreaming: true,
27
+ supportsImages: false, // SDK has limited image support
28
+ supportsTemperature: false, // SDK manages temperature internally
29
+ supportsWebSearch: false, // SDK accesses files directly, not web
30
+ timeout: 120000, // 2 minutes
31
+ description:
32
+ 'Claude via Agent SDK - requires claude login authentication',
33
+ aliases: ['claude-sdk', 'claude-code'],
34
+ },
35
+ };
36
+
37
+ /**
38
+ * Custom error class for Claude provider errors
39
+ */
40
+ class ClaudeProviderError extends ProviderError {
41
+ constructor(message, code, originalError = null) {
42
+ super(message, code, originalError);
43
+ this.name = 'ClaudeProviderError';
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Check if Claude SDK is available (optional dependency)
49
+ */
50
+ function isClaudeSDKAvailable() {
51
+ try {
52
+ // Simple presence check that works in ES modules
53
+ // If SDK not available, the actual import() will fail later with clear error
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Dynamically import Claude SDK (lazy loading)
62
+ * This keeps the SDK as an optional dependency
63
+ */
64
+ async function getClaudeSDK() {
65
+ if (!isClaudeSDKAvailable()) {
66
+ throw new ClaudeProviderError(
67
+ 'Claude SDK not installed. Install with: npm install @anthropic-ai/claude-agent-sdk',
68
+ 'CLAUDE_SDK_NOT_INSTALLED',
69
+ );
70
+ }
71
+
72
+ try {
73
+ // Use dynamic import to load SDK only when needed
74
+ const { query } = await import('@anthropic-ai/claude-agent-sdk');
75
+ return query;
76
+ } catch (error) {
77
+ throw new ClaudeProviderError(
78
+ 'Failed to load Claude SDK. Install with: npm install @anthropic-ai/claude-agent-sdk',
79
+ 'CLAUDE_SDK_LOAD_ERROR',
80
+ error,
81
+ );
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Convert message array to single prompt for Claude SDK
87
+ * Claude SDK expects single prompts, not message history
88
+ *
89
+ * Strategy:
90
+ * - Extract last user message only
91
+ * - Handle both string and multimodal content formats
92
+ */
93
+ function convertMessagesToPrompt(messages) {
94
+ if (!Array.isArray(messages)) {
95
+ throw new ClaudeProviderError(
96
+ 'Messages must be an array',
97
+ ErrorCodes.INVALID_MESSAGES,
98
+ );
99
+ }
100
+
101
+ if (messages.length === 0) {
102
+ throw new ClaudeProviderError(
103
+ 'Messages array cannot be empty',
104
+ ErrorCodes.INVALID_MESSAGES,
105
+ );
106
+ }
107
+
108
+ // Find last user message
109
+ const lastUserMessage = messages.filter((m) => m.role === 'user').pop();
110
+
111
+ if (!lastUserMessage) {
112
+ throw new ClaudeProviderError(
113
+ 'No user message found in messages array',
114
+ ErrorCodes.INVALID_MESSAGES,
115
+ );
116
+ }
117
+
118
+ // Extract text content from message
119
+ if (typeof lastUserMessage.content === 'string') {
120
+ return lastUserMessage.content;
121
+ }
122
+
123
+ // Handle array content (multimodal format)
124
+ if (Array.isArray(lastUserMessage.content)) {
125
+ const textParts = lastUserMessage.content
126
+ .filter((item) => item.type === 'text')
127
+ .map((item) => item.text);
128
+
129
+ // Log warning if images present (Claude SDK has limited image support)
130
+ const hasImages = lastUserMessage.content.some(
131
+ (item) => item.type === 'image',
132
+ );
133
+ if (hasImages) {
134
+ debugLog(
135
+ '[Claude SDK] Warning: Images in message will be ignored (Claude SDK does not support multimodal input)',
136
+ );
137
+ }
138
+
139
+ return textParts.join('\n');
140
+ }
141
+
142
+ throw new ClaudeProviderError(
143
+ 'Invalid message content format',
144
+ ErrorCodes.INVALID_MESSAGES,
145
+ );
146
+ }
147
+
148
+ /**
149
+ * Create stream generator for Claude SDK streaming responses
150
+ * Yields normalized events compatible with ProviderStreamNormalizer
151
+ *
152
+ * SDK Message Types:
153
+ * - system (subtype: init): Session initialization
154
+ * - assistant: Model responses with message.content
155
+ * - result (subtype: success/error_*): Final results with usage
156
+ */
157
+ async function* createStreamingGenerator(queryFn, prompt, options, signal) {
158
+ try {
159
+ // Build query options
160
+ const queryOptions = {
161
+ maxTurns: 1, // Single turn for chat
162
+ permissionMode: 'bypassPermissions', // Don't prompt for permissions
163
+ };
164
+
165
+ // Add working directory if provided
166
+ if (options.cwd) {
167
+ queryOptions.cwd = options.cwd;
168
+ }
169
+
170
+ // Pass abort controller if provided
171
+ // Note: The SDK expects AbortController, not AbortSignal
172
+ if (signal) {
173
+ // Create a new abort controller that we can pass to the SDK
174
+ const controller = new globalThis.AbortController();
175
+ queryOptions.abortController = controller;
176
+ // Forward abort signal from the provided signal to our controller
177
+ signal.addEventListener('abort', () => {
178
+ controller.abort();
179
+ });
180
+ }
181
+
182
+ // Create query generator
183
+ const response = queryFn({
184
+ prompt,
185
+ options: queryOptions,
186
+ });
187
+
188
+ let _sessionId = null;
189
+ let _modelUsed = 'claude';
190
+ let _accumulatedContent = '';
191
+
192
+ // Yield start event
193
+ yield {
194
+ type: 'start',
195
+ provider: 'claude',
196
+ model: 'claude',
197
+ };
198
+
199
+ // Iterate over SDK messages
200
+ for await (const message of response) {
201
+ // Check for cancellation
202
+ if (signal?.aborted) {
203
+ throw new ClaudeProviderError('Request cancelled', 'CANCELLED');
204
+ }
205
+
206
+ // Handle different message types
207
+ switch (message.type) {
208
+ case 'system':
209
+ if (message.subtype === 'init') {
210
+ _sessionId = message.session_id;
211
+ _modelUsed = message.model || 'claude';
212
+ debugLog(
213
+ `[Claude SDK] Session initialized: ${_sessionId}, model: ${_modelUsed}`,
214
+ );
215
+ }
216
+ break;
217
+
218
+ case 'assistant':
219
+ // Extract content from assistant message
220
+ if (message.message?.content) {
221
+ for (const block of message.message.content) {
222
+ if (block.type === 'text') {
223
+ const text = block.text || '';
224
+ _accumulatedContent += text;
225
+
226
+ // Yield delta event with content chunk
227
+ yield {
228
+ type: 'delta',
229
+ data: {
230
+ textDelta: text,
231
+ },
232
+ };
233
+ }
234
+ }
235
+ }
236
+ break;
237
+
238
+ case 'result':
239
+ // Handle final result
240
+ if (message.subtype === 'success') {
241
+ // Yield usage event
242
+ if (message.usage) {
243
+ yield {
244
+ type: 'usage',
245
+ usage: {
246
+ input_tokens: message.usage.input_tokens || 0,
247
+ output_tokens: message.usage.output_tokens || 0,
248
+ total_tokens:
249
+ (message.usage.input_tokens || 0) +
250
+ (message.usage.output_tokens || 0),
251
+ cached_input_tokens:
252
+ message.usage.cache_read_input_tokens || 0,
253
+ },
254
+ };
255
+ }
256
+
257
+ // Yield end event
258
+ yield {
259
+ type: 'end',
260
+ stop_reason: StopReasons.STOP,
261
+ finish_reason: 'stop',
262
+ };
263
+ } else if (
264
+ message.subtype === 'error_max_turns' ||
265
+ message.subtype === 'error_during_execution'
266
+ ) {
267
+ throw new ClaudeProviderError(
268
+ `Claude SDK execution failed: ${message.subtype}`,
269
+ ErrorCodes.API_ERROR,
270
+ );
271
+ }
272
+ break;
273
+ }
274
+ }
275
+ } catch (error) {
276
+ if (signal?.aborted) {
277
+ throw new ClaudeProviderError('Request cancelled', 'CANCELLED');
278
+ }
279
+ throw error;
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Claude SDK Provider Implementation
285
+ */
286
+ export const claudeProvider = {
287
+ /**
288
+ * Invoke Claude SDK with messages and options
289
+ * @param {Array} messages - Message array (Converse format)
290
+ * @param {Object} options - Invocation options
291
+ * @returns {Promise<Object>|AsyncGenerator} Response or stream generator
292
+ */
293
+ async invoke(messages, options = {}) {
294
+ const {
295
+ model = 'claude',
296
+ config,
297
+ stream = false,
298
+ signal,
299
+ reasoning_effort,
300
+ temperature,
301
+ use_websearch,
302
+ } = options;
303
+
304
+ // Validate configuration
305
+ if (!config) {
306
+ throw new ClaudeProviderError(
307
+ 'Configuration is required',
308
+ ErrorCodes.MISSING_API_KEY,
309
+ );
310
+ }
311
+
312
+ // Log unsupported parameters at debug level
313
+ if (temperature !== undefined) {
314
+ debugLog(
315
+ '[Claude SDK] Parameter "temperature" not supported by Claude SDK (ignored)',
316
+ );
317
+ }
318
+ if (use_websearch) {
319
+ debugLog(
320
+ '[Claude SDK] Parameter "use_websearch" not supported by Claude SDK (ignored)',
321
+ );
322
+ }
323
+ if (reasoning_effort !== undefined) {
324
+ debugLog(
325
+ '[Claude SDK] Parameter "reasoning_effort" not supported by Claude SDK (ignored)',
326
+ );
327
+ }
328
+
329
+ try {
330
+ // Get Claude SDK
331
+ const query = await getClaudeSDK();
332
+
333
+ // Convert messages to prompt
334
+ const prompt = convertMessagesToPrompt(messages);
335
+
336
+ // Build SDK options
337
+ const sdkOptions = {
338
+ cwd: config.server?.client_cwd || process.cwd(),
339
+ };
340
+
341
+ // Streaming mode
342
+ if (stream) {
343
+ return createStreamingGenerator(query, prompt, sdkOptions, signal);
344
+ }
345
+
346
+ // Synchronous mode: consume streaming internally and return complete response
347
+ const startTime = Date.now();
348
+ const generator = createStreamingGenerator(
349
+ query,
350
+ prompt,
351
+ sdkOptions,
352
+ signal,
353
+ );
354
+
355
+ let content = '';
356
+ let usage = null;
357
+
358
+ for await (const event of generator) {
359
+ if (event.type === 'delta' && event.data?.textDelta) {
360
+ content += event.data.textDelta;
361
+ } else if (event.type === 'usage') {
362
+ usage = event.usage;
363
+ }
364
+ }
365
+
366
+ const responseTime = Date.now() - startTime;
367
+
368
+ return {
369
+ content,
370
+ stop_reason: StopReasons.STOP,
371
+ rawResponse: { content, usage },
372
+ metadata: {
373
+ provider: 'claude',
374
+ model,
375
+ usage: usage
376
+ ? {
377
+ input_tokens: usage.input_tokens || 0,
378
+ output_tokens: usage.output_tokens || 0,
379
+ total_tokens:
380
+ (usage.input_tokens || 0) + (usage.output_tokens || 0),
381
+ cached_input_tokens: usage.cached_input_tokens || 0,
382
+ }
383
+ : null,
384
+ response_time_ms: responseTime,
385
+ finish_reason: 'stop',
386
+ },
387
+ };
388
+ } catch (error) {
389
+ debugError('[Claude SDK] Execution error', error);
390
+
391
+ // Map common errors to standard error codes
392
+ if (
393
+ error.message?.includes('authentication') ||
394
+ error.message?.includes('login') ||
395
+ error.message?.includes('not authenticated')
396
+ ) {
397
+ throw new ClaudeProviderError(
398
+ 'Claude SDK authentication failed. Run: claude login',
399
+ ErrorCodes.INVALID_API_KEY,
400
+ error,
401
+ );
402
+ }
403
+
404
+ if (error.message?.includes('timeout')) {
405
+ throw new ClaudeProviderError(
406
+ 'Claude SDK execution timeout',
407
+ ErrorCodes.TIMEOUT_ERROR,
408
+ error,
409
+ );
410
+ }
411
+
412
+ if (error.message?.includes('rate limit')) {
413
+ throw new ClaudeProviderError(
414
+ 'Rate limit exceeded',
415
+ ErrorCodes.RATE_LIMIT_EXCEEDED,
416
+ error,
417
+ );
418
+ }
419
+
420
+ // Re-throw as Claude error if not already
421
+ if (error instanceof ClaudeProviderError) {
422
+ throw error;
423
+ }
424
+
425
+ throw new ClaudeProviderError(
426
+ error.message || 'Claude SDK execution failed',
427
+ ErrorCodes.API_ERROR,
428
+ error,
429
+ );
430
+ }
431
+ },
432
+
433
+ /**
434
+ * Validate Claude SDK configuration
435
+ * Claude SDK uses CLI authentication (NOT API keys)
436
+ * Returns true optimistically - authentication errors handled at runtime
437
+ */
438
+ validateConfig(_config) {
439
+ // Claude SDK uses CLI authentication, not API keys
440
+ // We can't reliably check auth status, so return true optimistically
441
+ // and let the SDK handle authentication errors during execution
442
+ return isClaudeSDKAvailable();
443
+ },
444
+
445
+ /**
446
+ * Check if Claude SDK provider is available
447
+ */
448
+ isAvailable(config) {
449
+ return this.validateConfig(config);
450
+ },
451
+
452
+ /**
453
+ * Get supported Claude SDK models
454
+ */
455
+ getSupportedModels() {
456
+ return SUPPORTED_MODELS;
457
+ },
458
+
459
+ /**
460
+ * Get model configuration for specific model
461
+ */
462
+ getModelConfig(modelName) {
463
+ const modelNameLower = modelName.toLowerCase();
464
+
465
+ // Check exact match
466
+ if (SUPPORTED_MODELS[modelNameLower]) {
467
+ return SUPPORTED_MODELS[modelNameLower];
468
+ }
469
+
470
+ // Check aliases
471
+ for (const [_name, config] of Object.entries(SUPPORTED_MODELS)) {
472
+ if (
473
+ config.aliases &&
474
+ config.aliases.some((alias) => alias.toLowerCase() === modelNameLower)
475
+ ) {
476
+ return config;
477
+ }
478
+ }
479
+
480
+ return null;
481
+ },
482
+ };
@@ -15,6 +15,7 @@ import { deepseekProvider } from './deepseek.js';
15
15
  import { openrouterProvider } from './openrouter.js';
16
16
  import { codexProvider } from './codex.js';
17
17
  import { geminiCliProvider } from './gemini-cli.js';
18
+ import { claudeProvider } from './claude.js';
18
19
 
19
20
  /**
20
21
  * Provider registry map
@@ -33,6 +34,7 @@ const providers = {
33
34
  deepseek: deepseekProvider,
34
35
  openrouter: openrouterProvider,
35
36
  codex: codexProvider,
37
+ claude: claudeProvider,
36
38
  };
37
39
 
38
40
  /**
package/src/tools/chat.js CHANGED
@@ -441,6 +441,7 @@ function resolveAutoModel(model, providerName) {
441
441
  const defaults = {
442
442
  codex: 'codex',
443
443
  'gemini-cli': 'gemini',
444
+ claude: 'claude',
444
445
  openai: 'gpt-5',
445
446
  xai: 'grok-4-0709',
446
447
  google: 'gemini-pro',
@@ -478,6 +479,11 @@ export function mapModelToProvider(model, providers) {
478
479
  return 'gemini-cli';
479
480
  }
480
481
 
482
+ // Check Claude SDK (exact match only - routes to SDK provider instead of Anthropic API)
483
+ if (modelLower === 'claude' || modelLower === 'claude-sdk' || modelLower === 'claude-code') {
484
+ return 'claude';
485
+ }
486
+
481
487
  // Check OpenRouter-specific patterns first
482
488
  if (
483
489
  modelLower === 'openrouter auto' ||
@@ -698,6 +698,7 @@ function getDefaultModelForProvider(providerName) {
698
698
  const defaults = {
699
699
  codex: 'codex',
700
700
  'gemini-cli': 'gemini',
701
+ claude: 'claude',
701
702
  openai: 'gpt-5',
702
703
  xai: 'grok-4-0709',
703
704
  google: 'gemini-pro',
@@ -746,6 +747,11 @@ function mapModelToProvider(model, providers) {
746
747
  return 'gemini-cli';
747
748
  }
748
749
 
750
+ // Check Claude SDK (exact match only - routes to SDK provider instead of Anthropic API)
751
+ if (modelLower === 'claude' || modelLower === 'claude-sdk' || modelLower === 'claude-code') {
752
+ return 'claude';
753
+ }
754
+
749
755
  // Check OpenRouter-specific patterns first
750
756
  if (
751
757
  modelLower === 'openrouter auto' ||