converse-mcp-server 4.6.0 → 4.6.2

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/docs/API.md CHANGED
@@ -474,7 +474,7 @@ A one-line summary per answer (options at 0.00 omitted), followed by the answers
474
474
  Decision · typesafe/jev-1.13-20260917 via OpenRouter · 394 input tokens · $0.000017
475
475
  - urgent (noul): 0.97
476
476
  - team (choice): billing · confidence 1.00 · billing 1.00
477
- - frustration (score): 1.24 on 0–2 · confidence 0.64 · 1 Frustrated 0.76, 2 Very angry 0.24
477
+ - frustration (score): 1.24 on 0–2 · confidence 0.64 · 1 "Frustrated" 0.76, 2 "Very angry" 0.24
478
478
 
479
479
  ```json
480
480
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "converse-mcp-server",
3
- "version": "4.6.0",
3
+ "version": "4.6.2",
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",
@@ -93,22 +93,22 @@
93
93
  ".env.example"
94
94
  ],
95
95
  "dependencies": {
96
- "@anthropic-ai/claude-agent-sdk": "^0.3.293",
97
- "@anthropic-ai/sdk": "^0.132.0",
98
- "@github/copilot-sdk": "^1.0.17",
99
- "@google/genai": "^2.27.0",
96
+ "@anthropic-ai/claude-agent-sdk": "^0.3.296",
97
+ "@anthropic-ai/sdk": "^0.133.0",
98
+ "@github/copilot-sdk": "^1.0.19",
99
+ "@google/genai": "^2.28.0",
100
100
  "@lydell/node-pty": "1.2.0-beta.15",
101
101
  "@mistralai/mistralai": "^2.7.0",
102
102
  "@modelcontextprotocol/sdk": "^1.32.1",
103
- "@openai/codex-sdk": "^0.161.0",
103
+ "@openai/codex-sdk": "^0.162.0",
104
104
  "cors": "^2.8.6",
105
- "dotenv": "^18.0.6",
106
- "express": "^5.2.1",
105
+ "dotenv": "^18.0.7",
106
+ "express": "^5.3.0",
107
107
  "lru-cache": "^11.5.3",
108
108
  "nanoid": "^6.0.2",
109
- "openai": "^7.30.0",
109
+ "openai": "^7.31.0",
110
110
  "p-limit": "^7.3.3",
111
- "vite": "^8.3.3"
111
+ "vite": "^8.3.4"
112
112
  },
113
113
  "devDependencies": {
114
114
  "@vitest/coverage-v8": "^5.0.3",
@@ -73,22 +73,26 @@ function decodedBytes(image) {
73
73
  return Math.floor((image.base64.length * 3) / 4);
74
74
  }
75
75
 
76
+ /** Clef's limits hold on every host, so a breach is checked before sending. */
77
+ function clefLimitError(message) {
78
+ return new DecisionError(message, { terminal: true, notSent: true });
79
+ }
80
+
76
81
  function checkClefImages(images, state, questions) {
77
82
  const sizes = images.map(decodedBytes);
78
83
  const tooLarge = sizes.findIndex((bytes) => bytes > CLEF_MAX_IMAGE_BYTES);
79
84
  if (tooLarge !== -1) {
80
- throw new DecisionError(`Clef accepts images up to 4 MiB each; image ${tooLarge + 1} is ${(sizes[tooLarge] / 1048576).toFixed(1)} MiB.`, { terminal: true });
85
+ throw clefLimitError(`Clef accepts images up to 4 MiB each; image ${tooLarge + 1} is ${(sizes[tooLarge] / 1048576).toFixed(1)} MiB.`);
81
86
  }
82
87
  if (sizes.reduce((a, b) => a + b, 0) > CLEF_MAX_TOTAL_IMAGE_BYTES) {
83
- throw new DecisionError('Clef accepts at most 8 MiB of images per call.', { terminal: true });
88
+ throw clefLimitError('Clef accepts at most 8 MiB of images per call.');
84
89
  }
85
90
  const chars = images.reduce((n, image) => n + dataUrl(image).length, 0) + asText(state ?? '').length + JSON.stringify(questions).length;
86
91
  const estimate = Math.ceil(chars / CLEF_CHARS_PER_ESTIMATED_TOKEN);
87
92
  if (estimate > CLEF_CONTEXT_TOKENS) {
88
- throw new DecisionError(
93
+ throw clefLimitError(
89
94
  `Cloudflare estimates this request at ~${estimate} tokens (4 base64 characters per token), over Clef's ${CLEF_CONTEXT_TOKENS}-token window. ` +
90
95
  'Downscale or recompress the images (about 180 KB in total fits), or use gpt-6-luna.',
91
- { terminal: true },
92
96
  );
93
97
  }
94
98
  }
@@ -104,14 +108,11 @@ export const cloudflareFormat = {
104
108
  request(model, state, questions, images = []) {
105
109
  const ids = Object.keys(questions);
106
110
  if (ids.length > CLEF_MAX_QUESTIONS) {
107
- throw new DecisionError(`Clef accepts at most ${CLEF_MAX_QUESTIONS} questions per call (got ${ids.length}).`, { terminal: true });
111
+ throw clefLimitError(`Clef accepts at most ${CLEF_MAX_QUESTIONS} questions per call (got ${ids.length}).`);
108
112
  }
109
113
  const bad = ids.filter((id) => !CLEF_QUESTION_ID.test(id));
110
114
  if (bad.length) {
111
- throw new DecisionError(
112
- `Clef question names may use only letters, digits, "_", ".", and "-" (max 100 characters): ${bad.join(', ')}.`,
113
- { terminal: true },
114
- );
115
+ throw clefLimitError(`Clef question names may use only letters, digits, "_", ".", and "-" (max 100 characters): ${bad.join(', ')}.`);
115
116
  }
116
117
  const body = { model, state: state ?? '', questions: toSystemOneQuestions(questions) };
117
118
  if (images.length === 0) return body;
@@ -21,15 +21,18 @@ const MAX_RETRY_AFTER_MS = 30_000;
21
21
  /**
22
22
  * Error from a decision call. `retryable` marks failures worth repeating or
23
23
  * failing over (network, timeout, 408/429/5xx, auth); `terminal` marks request
24
- * faults that every host of the same model would reject the same way.
24
+ * faults that every host of the same model would reject the same way;
25
+ * `notSent` marks a request refused by a local limit check before any network
26
+ * call, so it is not reported as a failed request.
25
27
  */
26
28
  export class DecisionError extends Error {
27
- constructor(message, { status = null, retryable = false, terminal = false, requestId = null, retryAfterMs = null } = {}) {
29
+ constructor(message, { status = null, retryable = false, terminal = false, notSent = false, requestId = null, retryAfterMs = null } = {}) {
28
30
  super(message);
29
31
  this.name = 'DecisionError';
30
32
  this.status = status;
31
33
  this.retryable = retryable;
32
34
  this.terminal = terminal;
35
+ this.notSent = notSent;
33
36
  this.requestId = requestId;
34
37
  this.retryAfterMs = retryAfterMs;
35
38
  }
@@ -250,7 +250,8 @@ function summarizeAnswer(key, answer) {
250
250
  if (type === 'score') {
251
251
  const levels = Object.keys(answer.legend || answer.probabilities || {});
252
252
  const range = levels.length ? ` on 0–${levels.length - 1}` : '';
253
- const label = (k) => (answer.legend?.[k] ? `${k} ${answer.legend[k]}` : k);
253
+ // Quoted so a numeric label ("4") stays distinct from its level index (3).
254
+ const label = (k) => (answer.legend?.[k] ? `${k} "${answer.legend[k]}"` : k);
254
255
  return `- ${key} (score): ${fixed(answer.score)}${range} · confidence ${fixed(answer.confidence)} · ${byProbability(answer.probabilities, label)}`;
255
256
  }
256
257
  if (type === 'refusal') {
@@ -269,7 +270,7 @@ function formatResult(response, candidate, failures) {
269
270
 
270
271
  const lines = [header];
271
272
  for (const failure of failures) {
272
- lines.push(`(${failure.provider} failed, fell back: ${failure.message})`);
273
+ lines.push(`(${failure.provider} ${failure.notSent ? 'skipped' : 'failed'}, fell back: ${failure.message})`);
273
274
  }
274
275
  lines.push(...Object.entries(response.answers).map(([key, answer]) => summarizeAnswer(key, answer)));
275
276
 
@@ -352,13 +353,14 @@ export async function decideTool(args, dependencies) {
352
353
  } catch (error) {
353
354
  if (signal?.aborted) return createToolError('Decision request cancelled.');
354
355
  logger.error('Decision request failed', { provider: candidate.providerName, model: candidate.model, error: error.message });
355
- failures.push({ provider: candidate.providerName, message: error.message });
356
+ failures.push({ provider: candidate.providerName, message: error.message, notSent: Boolean(error.notSent) });
356
357
  if (error.terminal) rejectedFamilies.add(candidate.family);
357
358
  }
358
359
  }
359
360
 
360
- const detail = failures.map((f) => `${f.provider}: ${f.message}`).join('; ');
361
- return createToolError(`Decision request failed (${detail})`);
361
+ const noneSent = failures.every((f) => f.notSent);
362
+ const detail = failures.map((f) => `${f.provider}${f.notSent && !noneSent ? ', not sent' : ''}: ${f.message}`).join('; ');
363
+ return createToolError(`Decision request ${noneSent ? 'not sent' : 'failed'} (${detail})`);
362
364
  }
363
365
 
364
366
  decideTool.description =