@ateam-ai/mcp 0.3.34 → 0.3.36
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 +1 -1
- package/src/tools.js +133 -17
package/package.json
CHANGED
package/src/tools.js
CHANGED
|
@@ -16,6 +16,66 @@ import {
|
|
|
16
16
|
} from "./api.js";
|
|
17
17
|
import { renderAgentDocHeader, mergeAgentDoc, AGENT_DOC_SENTINEL } from "./agentDoc.js";
|
|
18
18
|
|
|
19
|
+
// ─── Async deploy helper ────────────────────────────────────────────
|
|
20
|
+
//
|
|
21
|
+
// All long-running deploy endpoints (build_and_run, redeploy, github_pull)
|
|
22
|
+
// support async mode: POST returns {job_id, poll_url} in <1s, the work runs
|
|
23
|
+
// in the background, and the client polls /deploy/jobs/:jobId until status
|
|
24
|
+
// is "done" or "failed". This bypasses the upstream Cloudflare 100s timeout
|
|
25
|
+
// that used to kill bulk redeploys with 524.
|
|
26
|
+
//
|
|
27
|
+
// pollDeployJob is the client side of that contract: it polls the job and
|
|
28
|
+
// returns the final job entry (which is the same shape as the original
|
|
29
|
+
// sync response would have been, plus job metadata). MCP tool wrappers use
|
|
30
|
+
// this so the agent gets a normal response from a long-running tool call —
|
|
31
|
+
// no async API leaks out to agent prompts.
|
|
32
|
+
async function pollDeployJob(jobId, sid, { label = 'deploy', maxMs = 15 * 60_000, intervalMs = 2000 } = {}) {
|
|
33
|
+
const start = Date.now();
|
|
34
|
+
let lastStatus = null;
|
|
35
|
+
while (Date.now() - start < maxMs) {
|
|
36
|
+
await new Promise(r => setTimeout(r, intervalMs));
|
|
37
|
+
try {
|
|
38
|
+
const job = await get(`/deploy/jobs/${jobId}`, sid);
|
|
39
|
+
lastStatus = job?.status;
|
|
40
|
+
if (job?.status === 'done' || job?.status === 'failed') {
|
|
41
|
+
return job; // job entry has the full result merged in
|
|
42
|
+
}
|
|
43
|
+
} catch (err) {
|
|
44
|
+
// Transient — keep polling. Log at debug level if requested.
|
|
45
|
+
if (process.env.MCP_DEBUG_POLLS) console.warn(`[pollDeployJob:${label}] poll error (will retry): ${err.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
error: `${label} polling timed out after ${Math.round(maxMs / 60_000)}min`,
|
|
51
|
+
last_status: lastStatus,
|
|
52
|
+
job_id: jobId,
|
|
53
|
+
hint: 'The job may still be running on the server. Call get(`/deploy/jobs/<job_id>`) directly to check.',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Dotted-field resolver ─────────────────────────────────────────
|
|
58
|
+
//
|
|
59
|
+
// Given an object and a dotted field name, walk down the path creating
|
|
60
|
+
// missing intermediate objects, and return { parent, leaf } so the caller
|
|
61
|
+
// can mutate parent[leaf] directly. Used by ateam_patch's _push / _delete /
|
|
62
|
+
// _update mutators so they correctly traverse "intents.supported" instead
|
|
63
|
+
// of creating a top-level key with a literal dot in its name. (That bug
|
|
64
|
+
// silently corrupted skill.json with three different copies of the same
|
|
65
|
+
// field — see the bug report from the parallel agent.)
|
|
66
|
+
function _resolveDottedField(obj, dottedPath) {
|
|
67
|
+
const parts = dottedPath.split('.');
|
|
68
|
+
let parent = obj;
|
|
69
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
70
|
+
const k = parts[i];
|
|
71
|
+
if (!parent[k] || typeof parent[k] !== 'object' || Array.isArray(parent[k])) {
|
|
72
|
+
parent[k] = {};
|
|
73
|
+
}
|
|
74
|
+
parent = parent[k];
|
|
75
|
+
}
|
|
76
|
+
return { parent, leaf: parts[parts.length - 1] };
|
|
77
|
+
}
|
|
78
|
+
|
|
19
79
|
// ─── Tool definitions ───────────────────────────────────────────────
|
|
20
80
|
|
|
21
81
|
export const tools = [
|
|
@@ -1859,24 +1919,40 @@ const handlers = {
|
|
|
1859
1919
|
const existing = typeof patched.role.persona === "string" ? patched.role.persona : "";
|
|
1860
1920
|
const sep = (!existing || /\s$/.test(existing)) ? "" : "\n\n";
|
|
1861
1921
|
patched.role.persona = existing + sep + value;
|
|
1862
|
-
} else if (key.endsWith("_push")
|
|
1863
|
-
// Array push: tools_push,
|
|
1922
|
+
} else if (key.endsWith("_push")) {
|
|
1923
|
+
// Array push: tools_push, intents.supported_push, etc.
|
|
1924
|
+
// BUG FIX: previously did `patched[field] = ...` which created a
|
|
1925
|
+
// top-level key with a literal dot (e.g. patched["intents.supported"])
|
|
1926
|
+
// instead of pushing into patched.intents.supported. Traverse the
|
|
1927
|
+
// dotted path correctly. Also enforce array-only values — was silently
|
|
1928
|
+
// falling through to the dot-notation branch when given a single
|
|
1929
|
+
// object, leaving a stray "<field>_push" sibling key behind.
|
|
1930
|
+
if (!Array.isArray(value)) {
|
|
1931
|
+
return { ok: false, phase: "patch", error: `${key} requires an array value (got ${typeof value}). Wrap the item in [] — e.g. {"${key}": [{...}]}.` };
|
|
1932
|
+
}
|
|
1864
1933
|
const field = key.replace(/_push$/, "");
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1934
|
+
const { parent, leaf } = _resolveDottedField(patched, field);
|
|
1935
|
+
parent[leaf] = [...(Array.isArray(parent[leaf]) ? parent[leaf] : []), ...value];
|
|
1936
|
+
} else if (key.endsWith("_delete")) {
|
|
1937
|
+
if (!Array.isArray(value)) {
|
|
1938
|
+
return { ok: false, phase: "patch", error: `${key} requires an array of names/ids (got ${typeof value}). Pass {"${key}": ["name1", "name2"]}.` };
|
|
1939
|
+
}
|
|
1868
1940
|
const field = key.replace(/_delete$/, "");
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1941
|
+
const { parent, leaf } = _resolveDottedField(patched, field);
|
|
1942
|
+
parent[leaf] = (Array.isArray(parent[leaf]) ? parent[leaf] : []).filter(item => !value.includes(item?.name || item?.id || item));
|
|
1943
|
+
} else if (key.endsWith("_update")) {
|
|
1944
|
+
if (!Array.isArray(value)) {
|
|
1945
|
+
return { ok: false, phase: "patch", error: `${key} requires an array of update objects (got ${typeof value}). Pass {"${key}": [{name: "x", description: "..."}]}.` };
|
|
1946
|
+
}
|
|
1872
1947
|
const field = key.replace(/_update$/, "");
|
|
1873
|
-
const
|
|
1948
|
+
const { parent, leaf } = _resolveDottedField(patched, field);
|
|
1949
|
+
const arr = Array.isArray(parent[leaf]) ? parent[leaf] : [];
|
|
1874
1950
|
for (const upd of value) {
|
|
1875
1951
|
const idx = arr.findIndex(item => (item.name || item.id) === (upd.name || upd.id));
|
|
1876
1952
|
if (idx >= 0) arr[idx] = { ...arr[idx], ...upd };
|
|
1877
1953
|
else arr.push(upd);
|
|
1878
1954
|
}
|
|
1879
|
-
|
|
1955
|
+
parent[leaf] = arr;
|
|
1880
1956
|
} else if (key.includes(".")) {
|
|
1881
1957
|
// Dot notation: "role.persona", "intents.thresholds.accept"
|
|
1882
1958
|
const parts = key.split(".");
|
|
@@ -2227,8 +2303,20 @@ const handlers = {
|
|
|
2227
2303
|
ateam_github_push: async ({ solution_id, message }, sid) =>
|
|
2228
2304
|
post(`/deploy/solutions/${solution_id}/github/push`, { push_to_github: true, message }, sid, { timeoutMs: 60_000 }),
|
|
2229
2305
|
|
|
2230
|
-
ateam_github_pull: async ({ solution_id }, sid) =>
|
|
2231
|
-
|
|
2306
|
+
ateam_github_pull: async ({ solution_id }, sid) => {
|
|
2307
|
+
// Async-first: github_pull is the #1 Cloudflare-524 culprit on large
|
|
2308
|
+
// solutions. Kick the job off, then poll. Falls back to sync if the
|
|
2309
|
+
// backend doesn't support async (older deployments).
|
|
2310
|
+
let kicked;
|
|
2311
|
+
try {
|
|
2312
|
+
kicked = await post(`/deploy/solutions/${solution_id}/github/pull`, { async: true }, sid, { timeoutMs: 30_000 });
|
|
2313
|
+
} catch (err) {
|
|
2314
|
+
// Sync fallback (older backend without async support)
|
|
2315
|
+
return await post(`/deploy/solutions/${solution_id}/github/pull`, {}, sid, { timeoutMs: 300_000, retries: 2 });
|
|
2316
|
+
}
|
|
2317
|
+
if (!kicked?.async || !kicked.job_id) return kicked; // backend didn't honor async — return as-is
|
|
2318
|
+
return await pollDeployJob(kicked.job_id, sid, { label: 'github-pull', maxMs: 15 * 60_000, intervalMs: 2000 });
|
|
2319
|
+
},
|
|
2232
2320
|
|
|
2233
2321
|
ateam_github_status: async ({ solution_id }, sid) =>
|
|
2234
2322
|
get(`/deploy/solutions/${solution_id}/github/status`, sid),
|
|
@@ -2277,23 +2365,51 @@ const handlers = {
|
|
|
2277
2365
|
const endpoint = skill_id
|
|
2278
2366
|
? `/deploy/solutions/${solution_id}/skills/${skill_id}/redeploy`
|
|
2279
2367
|
: `/deploy/solutions/${solution_id}/redeploy`;
|
|
2368
|
+
|
|
2369
|
+
// Async-first: bulk redeploys used to 524 on >5-skill solutions because
|
|
2370
|
+
// the upstream Cloudflare timeout is ~100s. Kick the job and poll. If
|
|
2371
|
+
// the backend doesn't support async (older deployment), fall back to
|
|
2372
|
+
// the legacy sync path with longer retry. If both fail, surface a
|
|
2373
|
+
// useful error/hint to the agent.
|
|
2280
2374
|
let result;
|
|
2375
|
+
let lastErr = null;
|
|
2281
2376
|
try {
|
|
2282
|
-
|
|
2377
|
+
const kicked = await post(endpoint, { async: true }, sid, { timeoutMs: 30_000 });
|
|
2378
|
+
if (kicked?.async && kicked.job_id) {
|
|
2379
|
+
result = await pollDeployJob(kicked.job_id, sid, {
|
|
2380
|
+
label: skill_id ? `redeploy-skill ${skill_id}` : 'redeploy-bulk',
|
|
2381
|
+
maxMs: 15 * 60_000,
|
|
2382
|
+
intervalMs: 2000,
|
|
2383
|
+
});
|
|
2384
|
+
} else {
|
|
2385
|
+
result = kicked; // backend didn't honor async — already-finished sync result
|
|
2386
|
+
}
|
|
2283
2387
|
} catch (err) {
|
|
2284
|
-
|
|
2285
|
-
|
|
2388
|
+
lastErr = err;
|
|
2389
|
+
// Sync fallback for backends without async support
|
|
2390
|
+
try {
|
|
2391
|
+
result = await post(endpoint, {}, sid, { timeoutMs: 300_000, retries: 2 });
|
|
2392
|
+
lastErr = null;
|
|
2393
|
+
} catch (syncErr) {
|
|
2394
|
+
lastErr = syncErr;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
if (!result && lastErr) {
|
|
2399
|
+
const notFound = /not found|404|ENOENT/i.test(lastErr.message);
|
|
2400
|
+
const isTimeout = /524|502|503|timeout|ETIMEDOUT/i.test(lastErr.message);
|
|
2286
2401
|
return {
|
|
2287
2402
|
ok: false,
|
|
2288
|
-
error:
|
|
2403
|
+
error: lastErr.message,
|
|
2289
2404
|
...(notFound && {
|
|
2290
2405
|
hint: "Skill not found in Builder storage. Edit the skill on GitHub with ateam_github_patch(solution_id, path: 'skills/<skill-id>/skill.json', search: '...', replace: '...'), then use ateam_build_and_run(solution_id, github: true) or ask the platform operator to deploy the single skill.",
|
|
2291
2406
|
}),
|
|
2292
2407
|
...(isTimeout && {
|
|
2293
|
-
hint: "Redeploy timed out
|
|
2408
|
+
hint: "Redeploy timed out even after async polling (15min). Use ateam_redeploy(solution_id, skill_id: '<specific-skill>') to redeploy one skill at a time.",
|
|
2294
2409
|
}),
|
|
2295
2410
|
};
|
|
2296
2411
|
}
|
|
2412
|
+
if (!result) result = { ok: false, error: 'Redeploy returned no result' };
|
|
2297
2413
|
// Pull through the underlying error/message instead of fabricating "0/0/0
|
|
2298
2414
|
// success-shaped" output. Old wrapper hid backend errors (e.g. validator
|
|
2299
2415
|
// failures from sentinel files in user repos) and reported `total: 0` with
|