converse-mcp-server 4.1.1 → 4.1.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
@@ -422,6 +422,8 @@ Only jobs in a `queued` or `running` state can be cancelled; already-completed,
422
422
 
423
423
  Each provider call retries timeouts, 408, 429 and 5xx with backoff, honoring `Retry-After`. Auth failures, exhausted retries and malformed responses fall back to the next provider. A 400/422 request fault stops immediately, because every host would reject it the same way.
424
424
 
425
+ TypeSafe's API sits behind a Cloudflare firewall that rejects some request bodies containing SQL-injection or shell-command patterns (for example `-- ; DROP TABLE`, or a quoted `'...; DROP TABLE ...;'`) with a 403 before the model sees them. This applies through OpenRouter too, since it forwards to the same edge. `decide` reports it as `Blocked by typesafe.ai's Cloudflare firewall before reaching the model (Ray ID …)` and stops without retrying or falling back. Treat a block as an unanswered question, not as a decision, and report the Ray ID to TypeSafe.
426
+
425
427
  ### Example Usage
426
428
 
427
429
  ```json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "converse-mcp-server",
3
- "version": "4.1.1",
3
+ "version": "4.1.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",
@@ -84,6 +84,43 @@ export function extractErrorMessage(body, rawText) {
84
84
  return rawText?.trim() || 'No error details returned';
85
85
  }
86
86
 
87
+ const MAX_ERROR_MESSAGE_LENGTH = 500;
88
+
89
+ /**
90
+ * Summarize an HTML error page, which arrives raw from TypeSafe's edge and
91
+ * embedded in OpenRouter's JSON error message when it forwards one.
92
+ * Cloudflare's block page means the firewall rejected the request content
93
+ * before any model saw it: typically SQL-injection or shell-command patterns
94
+ * inside state or questions.
95
+ * @returns {{ message: string, firewall: boolean, rayId: string|null }|null}
96
+ * null when the text is not an HTML page
97
+ */
98
+ export function describeHtmlError(text) {
99
+ if (!/<!doctype html|<html[\s>]/i.test(text || '')) return null;
100
+ const rayId = text.match(/Cloudflare Ray ID:\s*<strong[^>]*>([0-9a-f]+)</i)?.[1] ?? null;
101
+ if (/cloudflare/i.test(text) && /you have been blocked|Attention Required/i.test(text)) {
102
+ const host = text.match(/unable to access<\/span>\s*([^<\s]+)/i)?.[1] ?? 'the upstream';
103
+ return {
104
+ firewall: true,
105
+ rayId,
106
+ message:
107
+ `Blocked by ${host}'s Cloudflare firewall before reaching the model${rayId ? ` (Ray ID ${rayId})` : ''}: ` +
108
+ 'the request content matched an attack signature, typically SQL-injection or shell-command patterns in state or questions. ' +
109
+ 'Retrying the same content will not help; report the Ray ID to the provider.',
110
+ };
111
+ }
112
+ const title = text.match(/<title>([^<]*)<\/title>/i)?.[1]?.trim();
113
+ return {
114
+ firewall: false,
115
+ rayId,
116
+ message: `Upstream returned an HTML error page${title ? `: ${title}` : ''}${rayId ? ` (Ray ID ${rayId})` : ''}`,
117
+ };
118
+ }
119
+
120
+ function truncate(text) {
121
+ return text.length > MAX_ERROR_MESSAGE_LENGTH ? `${text.slice(0, MAX_ERROR_MESSAGE_LENGTH)}…` : text;
122
+ }
123
+
87
124
  function parseRetryAfter(headers) {
88
125
  const ms = Number(headers.get('retry-after-ms'));
89
126
  if (Number.isFinite(ms) && ms > 0) return ms;
@@ -145,11 +182,20 @@ async function sendOnce({ url, headers, body, signal, timeoutMs }) {
145
182
 
146
183
  if (!response.ok) {
147
184
  const status = response.status;
148
- throw new DecisionError(`HTTP ${status}: ${extractErrorMessage(parsed, rawText)}`, {
185
+ const detail = extractErrorMessage(parsed, rawText);
186
+ const html = describeHtmlError(detail);
187
+ // Every host forwards to TypeSafe's edge, so a firewall block repeats on
188
+ // retry and on failover alike.
189
+ const firewall = html?.firewall === true;
190
+ throw new DecisionError(`HTTP ${status}: ${html ? html.message : truncate(detail)}`, {
149
191
  status,
150
- retryable: status === 408 || status === 429 || status >= 500 || status === 401 || status === 403,
151
- terminal: status === 400 || status === 422,
152
- requestId: response.headers.get('x-typesafe-request-id') || response.headers.get('x-generation-id'),
192
+ retryable: !firewall && (status === 408 || status === 429 || status >= 500 || status === 401 || status === 403),
193
+ terminal: firewall || status === 400 || status === 422,
194
+ requestId:
195
+ response.headers.get('x-typesafe-request-id') ||
196
+ response.headers.get('x-generation-id') ||
197
+ html?.rayId ||
198
+ response.headers.get('cf-ray'),
153
199
  retryAfterMs: parseRetryAfter(response.headers),
154
200
  });
155
201
  }