conduyt-mcp 4.83.0 → 4.84.0

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/README.md CHANGED
@@ -387,6 +387,8 @@ object too large for the bound is left out and named in `_omitted`. Every HTTP r
387
387
  Conduyt API 503: Contact merge in progress [Retry-After: 5s] {"code":"merge_in_progress"}
388
388
  ```
389
389
 
390
+ The contact merge route (`POST /contacts/merge`, no tool today) answers a retryable 503 with `Retry-After` and the code `merge_contacts_busy` (a contact stayed busy for the whole lock budget) or `merge_budget_exhausted` (the request's time budget ran out), nothing changed. Any tool that reaches it surfaces the refusal in this grammar and never retries it (one API job per key); the caller retries after the stated seconds.
391
+
390
392
  ## Setup
391
393
 
392
394
  ```bash
package/dist/client.d.ts CHANGED
@@ -37,6 +37,13 @@ export interface RequestOptions {
37
37
  * ALWAYS_WHOLE_KEYS; a nested object that does not fit is left out and named in `_omitted`. JSON is never cut mid-object.
38
38
  */
39
39
  export declare function renderErrorDetails(body: Record<string, unknown>): string;
40
+ /**
41
+ * The ONE Retry-After parser. RFC 9110: delay-seconds (digits; a value too long for a number saturates at 2^31-1) or an
42
+ * HTTP-date in one of the three §5.6.7 forms, parsed explicitly and always as UTC (never the host timezone): IMF-fixdate,
43
+ * RFC 850 (two-digit year: more than 50 years ahead of `nowMs` means the previous century) and asctime. A date becomes the
44
+ * remaining seconds, floored at 0. Absent, blank or anything else (ISO timestamps, other zones, "-3") is null.
45
+ */
46
+ export declare function parseRetryAfter(raw: string | null | undefined, nowMs?: number): number | null;
40
47
  export declare class ConduytClient {
41
48
  private baseUrl;
42
49
  private apiKey;
package/dist/client.js CHANGED
@@ -104,6 +104,58 @@ export function renderErrorDetails(body) {
104
104
  out._omitted = omitted;
105
105
  return JSON.stringify(out);
106
106
  }
107
+ const RA_MAX_SECONDS = 2_147_483_647; // delay-seconds is unbounded 1*DIGIT: a longer value saturates here instead of becoming null
108
+ const RA_MONTHS = { Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11 };
109
+ const RA_MON = "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec";
110
+ const RA_IMF = new RegExp(`^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\\d{2}) (${RA_MON}) (\\d{4}) (\\d{2}):(\\d{2}):(\\d{2}) GMT$`);
111
+ const RA_850 = new RegExp(`^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\\d{2})-(${RA_MON})-(\\d{2}) (\\d{2}):(\\d{2}):(\\d{2}) GMT$`);
112
+ const RA_ASC = new RegExp(`^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (${RA_MON}) ([ \\d]\\d) (\\d{2}):(\\d{2}):(\\d{2}) (\\d{4})$`);
113
+ /** A UTC timestamp for the parts, or NaN when any part is out of range (31 Feb, 25:00, ...). */
114
+ function raUtc(y, mo, d, h, mi, s) {
115
+ if (y < 1900 || h > 23 || mi > 59 || s > 60)
116
+ return NaN; // RFC 5322: years below 1900 are not dates
117
+ const t = Date.UTC(y, mo, d, h, mi, Math.min(s, 59));
118
+ const back = new Date(t);
119
+ return back.getUTCFullYear() === y && back.getUTCMonth() === mo && back.getUTCDate() === d ? t : NaN;
120
+ }
121
+ /**
122
+ * The ONE Retry-After parser. RFC 9110: delay-seconds (digits; a value too long for a number saturates at 2^31-1) or an
123
+ * HTTP-date in one of the three §5.6.7 forms, parsed explicitly and always as UTC (never the host timezone): IMF-fixdate,
124
+ * RFC 850 (two-digit year: more than 50 years ahead of `nowMs` means the previous century) and asctime. A date becomes the
125
+ * remaining seconds, floored at 0. Absent, blank or anything else (ISO timestamps, other zones, "-3") is null.
126
+ */
127
+ export function parseRetryAfter(raw, nowMs = Date.now()) {
128
+ if (raw === null || raw === undefined)
129
+ return null;
130
+ const v = raw.trim();
131
+ if (v === "")
132
+ return null;
133
+ if (/^\d+(\.\d+)?$/.test(v)) {
134
+ const n = Number(v);
135
+ return Number.isNaN(n) ? null : Math.min(n, RA_MAX_SECONDS);
136
+ }
137
+ let t = NaN;
138
+ let m;
139
+ if ((m = RA_IMF.exec(v)))
140
+ t = raUtc(Number(m[3]), RA_MONTHS[m[2]], Number(m[1]), Number(m[4]), Number(m[5]), Number(m[6]));
141
+ else if ((m = RA_850.exec(v))) {
142
+ const century = Math.floor(new Date(nowMs).getUTCFullYear() / 100) * 100;
143
+ const mo = RA_MONTHS[m[2]], d = Number(m[1]), h = Number(m[4]), mi = Number(m[5]), sec = Number(m[6]);
144
+ t = raUtc(century + Number(m[3]), mo, d, h, mi, sec);
145
+ // RFC 9110: a timestamp more than 50 years in the future (the same instant 50 calendar years on, UTC) is the previous century
146
+ // (the day is clamped to the month's last day: 29 Feb + 50 years is 28 Feb, never 1 Mar)
147
+ const n = new Date(nowMs);
148
+ const ly = n.getUTCFullYear() + 50, lm = n.getUTCMonth();
149
+ const limit = Date.UTC(ly, lm, Math.min(n.getUTCDate(), new Date(Date.UTC(ly, lm + 1, 0)).getUTCDate()), n.getUTCHours(), n.getUTCMinutes(), n.getUTCSeconds(), n.getUTCMilliseconds());
150
+ if (!Number.isNaN(t) && t > limit)
151
+ t = raUtc(century - 100 + Number(m[3]), mo, d, h, mi, sec);
152
+ }
153
+ else if ((m = RA_ASC.exec(v)))
154
+ t = raUtc(Number(m[6]), RA_MONTHS[m[1]], Number(m[2].trim()), Number(m[3]), Number(m[4]), Number(m[5]));
155
+ if (Number.isNaN(t))
156
+ return null;
157
+ return Math.max(0, Math.ceil((t - nowMs) / 1000));
158
+ }
107
159
  export class ConduytClient {
108
160
  baseUrl;
109
161
  apiKey;
@@ -240,9 +292,9 @@ export class ConduytClient {
240
292
  * until the rest of the text is valid JSON (a server message may itself contain a brace).
241
293
  */
242
294
  httpError(res, text) {
243
- const retryAfterRaw = res.headers.get("retry-after");
244
- const retryAfter = retryAfterRaw === null || retryAfterRaw.trim() === "" ? NaN : Number(retryAfterRaw);
245
- const retryAfterOk = Number.isFinite(retryAfter) && retryAfter >= 0;
295
+ const retryAfterParsed = parseRetryAfter(res.headers.get("retry-after"));
296
+ const retryAfter = retryAfterParsed ?? NaN;
297
+ const retryAfterOk = retryAfterParsed !== null;
246
298
  const raTag = retryAfterOk ? ` [Retry-After: ${retryAfter}s]` : "";
247
299
  let msg = res.statusText;
248
300
  try {
@@ -275,8 +327,8 @@ export class ConduytClient {
275
327
  });
276
328
  }
277
329
  retryDelayMs(res, attemptIndex) {
278
- const retryAfter = Number(res?.headers.get("retry-after"));
279
- if (Number.isFinite(retryAfter) && retryAfter >= 0) {
330
+ const retryAfter = parseRetryAfter(res?.headers.get("retry-after") ?? null);
331
+ if (retryAfter !== null) {
280
332
  return Math.min(retryAfter * 1000, MAX_RETRY_AFTER_MS);
281
333
  }
282
334
  return Math.min(this.retryBaseMs * 2 ** attemptIndex, MAX_RETRY_AFTER_MS);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt-mcp",
3
- "version": "4.83.0",
3
+ "version": "4.84.0",
4
4
  "mcpName": "com.conduyt/mcp",
5
5
  "description": "MCP server for Conduyt CRM — expose CRM operations as AI-accessible tools",
6
6
  "type": "module",