@debugg-ai/debugg-ai-mcp 2.4.1 → 2.6.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 +21 -1
- package/dist/handlers/index.js +1 -0
- package/dist/handlers/probePageHandler.js +314 -0
- package/dist/handlers/searchEnvironmentsHandler.js +12 -2
- package/dist/handlers/testPageChangesHandler.js +149 -70
- package/dist/handlers/triggerCrawlHandler.js +65 -21
- package/dist/services/ngrok/tunnelManager.js +46 -7
- package/dist/services/ngrok/tunnelRegistry.js +39 -5
- package/dist/services/ngrok/types.js +0 -1
- package/dist/tools/index.js +3 -0
- package/dist/tools/probePage.js +89 -0
- package/dist/types/index.js +17 -0
- package/dist/utils/errors.js +0 -1
- package/dist/utils/harSummarizer.js +193 -0
- package/dist/utils/projectAnalyzer.js +2 -2
- package/dist/utils/telemetry.js +1 -0
- package/dist/utils/transientErrors.js +82 -0
- package/dist/utils/urlParser.js +1 -1
- package/dist/utils/validation.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Probe Page Tool Definition.
|
|
3
|
+
*
|
|
4
|
+
* Lightweight no-LLM batch page probe — navigate + capture state for 1-20
|
|
5
|
+
* URLs in one backend execution. Returns screenshots, page metadata,
|
|
6
|
+
* structured console errors, and per-URL networkSummary (origin+pathname
|
|
7
|
+
* aggregation that surfaces refetch loops as a single entry).
|
|
8
|
+
*
|
|
9
|
+
* NOT an agent: no LLM in the critical path; no interaction (clicks/fills);
|
|
10
|
+
* no scenario verification. For those, use check_app_in_browser.
|
|
11
|
+
*/
|
|
12
|
+
import { ProbePageInputSchema } from '../types/index.js';
|
|
13
|
+
import { probePageHandler } from '../handlers/probePageHandler.js';
|
|
14
|
+
const DESCRIPTION = `Probe one or more URLs and return their rendered state — screenshot, page metadata (title/finalUrl/statusCode/loadTimeMs), structured console errors, and per-URL network summary (refetch loops collapse into one row by origin+pathname).
|
|
15
|
+
|
|
16
|
+
WHEN TO USE: "did I just break /settings?" / "smoke-test these 5 routes after my refactor" / "what's actually rendering at /dashboard?" — fast (<10s for 1 URL, <25s for 20), no LLM cost, no agent loop.
|
|
17
|
+
|
|
18
|
+
NOT FOR: scenario verification (sign in → click X → assert Y), interaction (clicks, form fills, scrolls), or anything requiring agent decisions. Use check_app_in_browser for those.
|
|
19
|
+
|
|
20
|
+
LOCALHOST SUPPORT: any localhost URL is auto-tunneled. Pre-flight TCP probe fails fast (<2s) if the dev server isn't listening.
|
|
21
|
+
|
|
22
|
+
BATCH MODE: pass up to 20 targets in one call to share browser session + tunnel — dramatically faster than firing parallel single-URL probes (one execution unit, not N). Per-URL waitForSelector / waitForLoadState / timeoutMs override defaults.
|
|
23
|
+
|
|
24
|
+
A single failed target's error appears in result.error without failing the whole batch — the other results stay valid.`;
|
|
25
|
+
const TARGET_PROPERTIES = {
|
|
26
|
+
url: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'URL to probe. Public URL or localhost URL (auto-tunneled).',
|
|
29
|
+
},
|
|
30
|
+
waitForSelector: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: 'Optional CSS selector to wait for after navigation completes. Useful for SPAs that mount content asynchronously.',
|
|
33
|
+
},
|
|
34
|
+
waitForLoadState: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
enum: ['load', 'domcontentloaded', 'networkidle'],
|
|
37
|
+
description: "When to consider the page 'loaded' before capturing. Default 'load'. Use 'networkidle' for SPAs to wait until the bundle finishes rendering.",
|
|
38
|
+
},
|
|
39
|
+
timeoutMs: {
|
|
40
|
+
type: 'number',
|
|
41
|
+
description: 'Per-URL navigation timeout in milliseconds (1000-30000, default 10000).',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
export function buildProbePageTool() {
|
|
45
|
+
return {
|
|
46
|
+
name: 'probe_page',
|
|
47
|
+
title: 'Probe Page',
|
|
48
|
+
description: DESCRIPTION,
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
targets: {
|
|
53
|
+
type: 'array',
|
|
54
|
+
minItems: 1,
|
|
55
|
+
maxItems: 20,
|
|
56
|
+
items: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: TARGET_PROPERTIES,
|
|
59
|
+
required: ['url'],
|
|
60
|
+
additionalProperties: false,
|
|
61
|
+
},
|
|
62
|
+
description: '1-20 URLs to probe. Each entry can carry its own per-URL wait config.',
|
|
63
|
+
},
|
|
64
|
+
includeHtml: {
|
|
65
|
+
type: 'boolean',
|
|
66
|
+
description: "If true, each result includes the page's outerHTML. Default false to keep response size sane.",
|
|
67
|
+
},
|
|
68
|
+
captureScreenshots: {
|
|
69
|
+
type: 'boolean',
|
|
70
|
+
description: 'If true (default), one PNG screenshot is returned per target. Set false for very large batches or when only the structured data matters.',
|
|
71
|
+
},
|
|
72
|
+
repoName: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
description: "GitHub repository name (e.g. 'my-org/my-repo'). Auto-detected from the current git repo — only provide this to scope the probe to a different project context.",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
required: ['targets'],
|
|
78
|
+
additionalProperties: false,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export function buildValidatedProbePageTool() {
|
|
83
|
+
const tool = buildProbePageTool();
|
|
84
|
+
return {
|
|
85
|
+
...tool,
|
|
86
|
+
inputSchema: ProbePageInputSchema,
|
|
87
|
+
handler: probePageHandler,
|
|
88
|
+
};
|
|
89
|
+
}
|
package/dist/types/index.js
CHANGED
|
@@ -152,3 +152,20 @@ export var LogLevel;
|
|
|
152
152
|
LogLevel["INFO"] = "info";
|
|
153
153
|
LogLevel["DEBUG"] = "debug";
|
|
154
154
|
})(LogLevel || (LogLevel = {}));
|
|
155
|
+
// ── probe-page ────────────────────────────────────────────────────────────
|
|
156
|
+
// Lightweight no-LLM page-probe tool. Each target gets its own wait config;
|
|
157
|
+
// targets[] is the batch — one workflow execution covers up to 20 URLs sharing
|
|
158
|
+
// browser session + tunnel. Strict schema: forbidden agent fields like
|
|
159
|
+
// `description` and `credentialId` reject (zero-LLM contract).
|
|
160
|
+
export const ProbePageTargetSchema = z.object({
|
|
161
|
+
url: z.preprocess(normalizeUrl, z.string().url('Invalid URL. Pass a full URL like "http://localhost:3000" or "https://example.com". Localhost URLs are auto-tunneled to the remote browser.')),
|
|
162
|
+
waitForSelector: z.string().optional(),
|
|
163
|
+
waitForLoadState: z.enum(['load', 'domcontentloaded', 'networkidle']).default('load'),
|
|
164
|
+
timeoutMs: z.number().int().min(1000, 'timeoutMs minimum is 1000 (1s)').max(30000, 'timeoutMs maximum is 30000 (30s) — longer probes should use check_app_in_browser').default(10000),
|
|
165
|
+
}).strict();
|
|
166
|
+
export const ProbePageInputSchema = z.object({
|
|
167
|
+
targets: z.array(ProbePageTargetSchema).min(1, 'targets must have at least one URL').max(20, 'targets capped at 20 per call — split larger sweeps across multiple calls'),
|
|
168
|
+
includeHtml: z.boolean().default(false),
|
|
169
|
+
captureScreenshots: z.boolean().default(true),
|
|
170
|
+
repoName: z.string().optional(),
|
|
171
|
+
}).strict();
|
package/dist/utils/errors.js
CHANGED
|
@@ -83,7 +83,6 @@ export function handleConfigurationError(error) {
|
|
|
83
83
|
* Handle external service errors (e.g., API calls)
|
|
84
84
|
*/
|
|
85
85
|
export function handleExternalServiceError(error, serviceName, operation) {
|
|
86
|
-
const context = `${serviceName}${operation ? `:${operation}` : ''}`;
|
|
87
86
|
if (error instanceof Error) {
|
|
88
87
|
logger.error('External service error', {
|
|
89
88
|
serviceName,
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* harSummarizer — pure HAR + console aggregation utilities.
|
|
3
|
+
*
|
|
4
|
+
* Aggregation key for networkSummary: `origin + pathname` (per system reqs).
|
|
5
|
+
* Refetch loops with varying query strings collapse into a single entry.
|
|
6
|
+
*
|
|
7
|
+
* Pure functions — no I/O, no async — so they can be reused by the future
|
|
8
|
+
* `summarize_execution` tool.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Re-aggregate backend's pre-grouped network_summary entries by
|
|
12
|
+
* `origin + pathname` (vs backend's full-URL key). Collapses refetch loops:
|
|
13
|
+
* 5 separate `/api/poll?n=0..4` entries become 1 entry with count: 5.
|
|
14
|
+
*
|
|
15
|
+
* Backend `browser.capture` (commit 154e1e69) emits network_summary already
|
|
16
|
+
* grouped by full URL with shape `{url, count, methods[], statuses{}, resource_types[]}`.
|
|
17
|
+
* That preserves the per-request granularity but defeats the original
|
|
18
|
+
* client #1 use case ("endpoint hit N times" refetch detection). MCP-side
|
|
19
|
+
* re-aggregation runs once over the small pre-grouped list — cheap.
|
|
20
|
+
*/
|
|
21
|
+
export function reaggregateByOriginPath(entries) {
|
|
22
|
+
if (!Array.isArray(entries))
|
|
23
|
+
return [];
|
|
24
|
+
const buckets = new Map();
|
|
25
|
+
for (const e of entries) {
|
|
26
|
+
try {
|
|
27
|
+
const url = e?.url;
|
|
28
|
+
if (typeof url !== 'string')
|
|
29
|
+
continue;
|
|
30
|
+
const parsed = new URL(url);
|
|
31
|
+
const key = `${parsed.origin}${parsed.pathname}`;
|
|
32
|
+
const count = typeof e.count === 'number' ? e.count : 0;
|
|
33
|
+
const statuses = e.statuses ?? {};
|
|
34
|
+
const existing = buckets.get(key);
|
|
35
|
+
if (existing) {
|
|
36
|
+
existing.count += count;
|
|
37
|
+
for (const [code, n] of Object.entries(statuses)) {
|
|
38
|
+
if (typeof n === 'number') {
|
|
39
|
+
existing.statuses[code] = (existing.statuses[code] ?? 0) + n;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const out = {
|
|
45
|
+
url: key,
|
|
46
|
+
count,
|
|
47
|
+
statuses: { ...statuses },
|
|
48
|
+
totalBytes: 0, // Backend's pre-grouped shape doesn't expose response bytes; placeholder until we wire fetched-bytes.
|
|
49
|
+
};
|
|
50
|
+
buckets.set(key, out);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// malformed URL — skip
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return [...buckets.values()].sort((a, b) => b.count - a.count);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Map backend's console_slice entry shape to MCP's ConsoleErrorEntry.
|
|
61
|
+
* Backend shape: {text, level, location: {url, line}, timestamp}
|
|
62
|
+
* MCP shape: {level, text, source?, lineNumber?, timestamp?}
|
|
63
|
+
*/
|
|
64
|
+
export function mapConsoleSlice(entries) {
|
|
65
|
+
if (!Array.isArray(entries))
|
|
66
|
+
return [];
|
|
67
|
+
const out = [];
|
|
68
|
+
for (const e of entries) {
|
|
69
|
+
if (typeof e !== 'object' || e === null)
|
|
70
|
+
continue;
|
|
71
|
+
const entry = {
|
|
72
|
+
level: typeof e.level === 'string' ? e.level : 'log',
|
|
73
|
+
text: typeof e.text === 'string' ? e.text : '',
|
|
74
|
+
};
|
|
75
|
+
const loc = e.location ?? {};
|
|
76
|
+
if (typeof loc.url === 'string' && loc.url)
|
|
77
|
+
entry.source = loc.url;
|
|
78
|
+
else if (typeof e.source === 'string' && e.source)
|
|
79
|
+
entry.source = e.source;
|
|
80
|
+
if (typeof loc.line === 'number')
|
|
81
|
+
entry.lineNumber = loc.line;
|
|
82
|
+
else if (typeof e.lineNumber === 'number')
|
|
83
|
+
entry.lineNumber = e.lineNumber;
|
|
84
|
+
// Backend timestamps are ISO strings; MCP type uses number (ms since epoch).
|
|
85
|
+
// Coerce when possible; otherwise pass through unchanged.
|
|
86
|
+
if (typeof e.timestamp === 'number') {
|
|
87
|
+
entry.timestamp = e.timestamp;
|
|
88
|
+
}
|
|
89
|
+
else if (typeof e.timestamp === 'string') {
|
|
90
|
+
const parsed = Date.parse(e.timestamp);
|
|
91
|
+
if (!isNaN(parsed))
|
|
92
|
+
entry.timestamp = parsed;
|
|
93
|
+
}
|
|
94
|
+
out.push(entry);
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Aggregate HAR `log.entries` into per-endpoint NetworkSummary[], sorted
|
|
100
|
+
* descending by request count (hottest endpoints first). Malformed entries
|
|
101
|
+
* (missing request.url or response.status) are skipped, not thrown.
|
|
102
|
+
*/
|
|
103
|
+
export function summarizeHar(harEntries) {
|
|
104
|
+
if (!Array.isArray(harEntries))
|
|
105
|
+
return [];
|
|
106
|
+
const buckets = new Map();
|
|
107
|
+
for (const entry of harEntries) {
|
|
108
|
+
try {
|
|
109
|
+
const reqUrl = entry?.request?.url;
|
|
110
|
+
const status = entry?.response?.status;
|
|
111
|
+
if (typeof reqUrl !== 'string' || typeof status !== 'number')
|
|
112
|
+
continue;
|
|
113
|
+
// Aggregation key: origin + pathname (refetch loops collapse).
|
|
114
|
+
let parsed;
|
|
115
|
+
try {
|
|
116
|
+
parsed = new URL(reqUrl);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const key = `${parsed.origin}${parsed.pathname}`;
|
|
122
|
+
const bytesRaw = entry?.response?.content?.size;
|
|
123
|
+
const bytes = typeof bytesRaw === 'number' && bytesRaw >= 0 ? bytesRaw : 0;
|
|
124
|
+
const mime = entry?.response?.content?.mimeType;
|
|
125
|
+
const mimeStr = typeof mime === 'string' && mime ? mime : '';
|
|
126
|
+
const existing = buckets.get(key);
|
|
127
|
+
if (existing) {
|
|
128
|
+
existing.count++;
|
|
129
|
+
const sk = String(status);
|
|
130
|
+
existing.statuses[sk] = (existing.statuses[sk] ?? 0) + 1;
|
|
131
|
+
existing.totalBytes += bytes;
|
|
132
|
+
if (mimeStr)
|
|
133
|
+
existing.mimeTypes.add(mimeStr);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
buckets.set(key, {
|
|
137
|
+
url: key,
|
|
138
|
+
count: 1,
|
|
139
|
+
statuses: { [String(status)]: 1 },
|
|
140
|
+
totalBytes: bytes,
|
|
141
|
+
mimeTypes: mimeStr ? new Set([mimeStr]) : new Set(),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// malformed — skip
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return [...buckets.values()]
|
|
150
|
+
.map(({ mimeTypes, url, count, statuses, totalBytes }) => {
|
|
151
|
+
const out = { url, count, statuses, totalBytes };
|
|
152
|
+
// Only attach mimeType when homogeneous — mixed types omit the field.
|
|
153
|
+
if (mimeTypes.size === 1) {
|
|
154
|
+
out.mimeType = [...mimeTypes][0];
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
})
|
|
158
|
+
.sort((a, b) => b.count - a.count);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Normalize a console-log JSON array into ConsoleErrorEntry[].
|
|
162
|
+
* Maps backend's snake_case (`line_number`, `url`) to MCP's camelCase
|
|
163
|
+
* (`lineNumber`, `source`). Drops entries that aren't plain objects.
|
|
164
|
+
*/
|
|
165
|
+
export function summarizeConsole(consoleEntries) {
|
|
166
|
+
if (!Array.isArray(consoleEntries))
|
|
167
|
+
return [];
|
|
168
|
+
const out = [];
|
|
169
|
+
for (const e of consoleEntries) {
|
|
170
|
+
if (typeof e !== 'object' || e === null)
|
|
171
|
+
continue;
|
|
172
|
+
const entry = {
|
|
173
|
+
level: typeof e.level === 'string' ? e.level : 'log',
|
|
174
|
+
text: typeof e.text === 'string' ? e.text : '',
|
|
175
|
+
};
|
|
176
|
+
// source: prefer `url` (backend convention), fall back to `source`
|
|
177
|
+
const sourceVal = typeof e.url === 'string' && e.url
|
|
178
|
+
? e.url
|
|
179
|
+
: (typeof e.source === 'string' && e.source ? e.source : undefined);
|
|
180
|
+
if (sourceVal)
|
|
181
|
+
entry.source = sourceVal;
|
|
182
|
+
// lineNumber: snake_case from backend → camelCase
|
|
183
|
+
const lineVal = typeof e.line_number === 'number'
|
|
184
|
+
? e.line_number
|
|
185
|
+
: (typeof e.lineNumber === 'number' ? e.lineNumber : undefined);
|
|
186
|
+
if (typeof lineVal === 'number')
|
|
187
|
+
entry.lineNumber = lineVal;
|
|
188
|
+
if (typeof e.timestamp === 'number')
|
|
189
|
+
entry.timestamp = e.timestamp;
|
|
190
|
+
out.push(entry);
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
@@ -45,7 +45,7 @@ export class ProjectAnalyzer {
|
|
|
45
45
|
/**
|
|
46
46
|
* Analyze codebase for context extraction
|
|
47
47
|
*/
|
|
48
|
-
async analyzeCodebase(repoPath, repoName, branchName,
|
|
48
|
+
async analyzeCodebase(repoPath, repoName, branchName, _includeChanges = true) {
|
|
49
49
|
try {
|
|
50
50
|
logger.info('Starting codebase analysis', { repoPath, repoName, branchName });
|
|
51
51
|
const analysis = await this.analyzeProject(repoPath);
|
|
@@ -384,7 +384,7 @@ export class ProjectAnalyzer {
|
|
|
384
384
|
}
|
|
385
385
|
}
|
|
386
386
|
}
|
|
387
|
-
catch
|
|
387
|
+
catch {
|
|
388
388
|
// Skip directories we can't read
|
|
389
389
|
}
|
|
390
390
|
};
|
package/dist/utils/telemetry.js
CHANGED
|
@@ -52,6 +52,7 @@ export const TelemetryEvents = {
|
|
|
52
52
|
TOOL_EXECUTED: 'tool.executed',
|
|
53
53
|
TOOL_FAILED: 'tool.failed',
|
|
54
54
|
WORKFLOW_EXECUTED: 'workflow.executed',
|
|
55
|
+
WORKFLOW_TRANSIENT_RETRY: 'workflow.transient_retry',
|
|
55
56
|
TUNNEL_PROVISIONED: 'tunnel.provisioned',
|
|
56
57
|
TUNNEL_PROVISION_RETRY: 'tunnel.provision_retry',
|
|
57
58
|
TUNNEL_STOPPED: 'tunnel.stopped',
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect well-known transient failure signatures in completed workflow
|
|
3
|
+
* executions. When `isTransientWorkflowError` returns true, the MCP handler
|
|
4
|
+
* auto-retries the workflow (cost: one extra quota unit) — saving the caller
|
|
5
|
+
* from the 'pure infrastructure noise' failure mode the original client
|
|
6
|
+
* called out in their feedback (Pydantic JSON parse errors, etc.).
|
|
7
|
+
*
|
|
8
|
+
* Be CONSERVATIVE: only patterns documented as transient. False positives
|
|
9
|
+
* waste quota; false negatives leave existing behavior, which is fine — the
|
|
10
|
+
* caller still gets a clear error and can decide what to do.
|
|
11
|
+
*
|
|
12
|
+
* Bead `kbxy`. Patterns are extracted (not inlined) so they're easy to audit
|
|
13
|
+
* + extend as new transient signatures get observed in production.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Patterns that match transient backend failures worth retrying. Each entry
|
|
17
|
+
* is a regex tested against `errorMessage` AND `state.error`. Matching ANY
|
|
18
|
+
* pattern in EITHER field flags the execution as transient.
|
|
19
|
+
*
|
|
20
|
+
* To add a new pattern: confirm by sampling production telemetry that the
|
|
21
|
+
* signature recovers on retry (a one-shot reproduce-then-retry test is
|
|
22
|
+
* sufficient evidence). Document the source in the comment.
|
|
23
|
+
*/
|
|
24
|
+
const TRANSIENT_PATTERNS = [
|
|
25
|
+
// The original client complaint. Backend agent's brain.step occasionally
|
|
26
|
+
// returns malformed JSON for the structured output — Pydantic chokes on
|
|
27
|
+
// EOF / partial JSON. A fresh agent invocation reliably recovers.
|
|
28
|
+
{ pattern: /Invalid JSON.*EOF while parsing/i, reason: 'pydantic-eof' },
|
|
29
|
+
{ pattern: /Failed to parse AgentOutput/i, reason: 'agent-output-parse' },
|
|
30
|
+
// Backend-side infrastructure flakes (nginx 502 from upstream + timeouts).
|
|
31
|
+
// Both observed in production during 2026-04-26 + 2026-04-27 deploys —
|
|
32
|
+
// recovery on next request is the rule, not the exception.
|
|
33
|
+
{ pattern: /502 Bad Gateway/i, reason: 'nginx-502' },
|
|
34
|
+
{ pattern: /upstream connect timeout/i, reason: 'upstream-timeout' },
|
|
35
|
+
// Network-layer transient — TCP reset between MCP↔backend or backend↔model.
|
|
36
|
+
{ pattern: /ECONNRESET|connection reset by peer/i, reason: 'econnreset' },
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* @returns true if the execution's error fields contain a known transient
|
|
40
|
+
* signature, indicating a retry has a reasonable chance of succeeding.
|
|
41
|
+
*/
|
|
42
|
+
export function isTransientWorkflowError(execution) {
|
|
43
|
+
if (!execution)
|
|
44
|
+
return false;
|
|
45
|
+
const candidates = [];
|
|
46
|
+
if (typeof execution.errorMessage === 'string' && execution.errorMessage) {
|
|
47
|
+
candidates.push(execution.errorMessage);
|
|
48
|
+
}
|
|
49
|
+
if (typeof execution.state?.error === 'string' && execution.state.error) {
|
|
50
|
+
candidates.push(execution.state.error);
|
|
51
|
+
}
|
|
52
|
+
if (candidates.length === 0)
|
|
53
|
+
return false;
|
|
54
|
+
for (const text of candidates) {
|
|
55
|
+
for (const { pattern } of TRANSIENT_PATTERNS) {
|
|
56
|
+
if (pattern.test(text))
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* @returns the reason tag for the matched transient pattern (for telemetry),
|
|
64
|
+
* or undefined if no pattern matched. Useful when you want to attach a
|
|
65
|
+
* classifier to a `workflow.transient_retry` event.
|
|
66
|
+
*/
|
|
67
|
+
export function transientReasonTag(execution) {
|
|
68
|
+
if (!execution)
|
|
69
|
+
return undefined;
|
|
70
|
+
const fields = [];
|
|
71
|
+
if (typeof execution.errorMessage === 'string' && execution.errorMessage)
|
|
72
|
+
fields.push(execution.errorMessage);
|
|
73
|
+
if (typeof execution.state?.error === 'string' && execution.state.error)
|
|
74
|
+
fields.push(execution.state.error);
|
|
75
|
+
for (const text of fields) {
|
|
76
|
+
for (const { pattern, reason } of TRANSIENT_PATTERNS) {
|
|
77
|
+
if (pattern.test(text))
|
|
78
|
+
return reason;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
package/dist/utils/urlParser.js
CHANGED
package/dist/utils/validation.js
CHANGED
|
@@ -81,7 +81,7 @@ export function validatePort(port) {
|
|
|
81
81
|
try {
|
|
82
82
|
return commonSchemas.port.parse(port);
|
|
83
83
|
}
|
|
84
|
-
catch
|
|
84
|
+
catch {
|
|
85
85
|
throw new MCPError(MCPErrorCode.VALIDATION_ERROR, `Invalid port number: ${port}. Port must be between 1 and 65535.`, { port });
|
|
86
86
|
}
|
|
87
87
|
}
|