@ateam-ai/mcp 0.4.36 → 0.4.38

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ateam-ai/mcp",
3
- "version": "0.4.36",
3
+ "version": "0.4.38",
4
4
  "mcpName": "io.github.ariekogan/ateam-mcp",
5
5
  "description": "A-Team MCP Server — build, validate, and deploy multi-agent solutions from any AI environment",
6
6
  "type": "module",
package/src/api.js CHANGED
@@ -520,7 +520,12 @@ async function request(method, path, body, sessionId, opts = {}) {
520
520
 
521
521
  if (!res.ok) {
522
522
  const text = await res.text().catch(() => "");
523
- throw new Error(formatError(method, path, res.status, text, baseUrl));
523
+ // Attach the HTTP status so callers can distinguish a genuine 404
524
+ // (resource absent) from a transient/5xx failure. ateam_patch relies
525
+ // on this to NOT scaffold-clobber an existing skill on a read error.
526
+ const e = new Error(formatError(method, path, res.status, text, baseUrl));
527
+ e.status = res.status;
528
+ throw e;
524
529
  }
525
530
 
526
531
  return res.json();
package/src/tools.js CHANGED
@@ -1026,6 +1026,7 @@ export const tools = [
1026
1026
  " • github:true + files:[] — GitHub state at `ref` as BASE, your files overlay on top (incoming wins).\n" +
1027
1027
  " • files:[] (no github) — default MERGE with GitHub state at `ref`. Refuses if no GitHub base exists (no silent nuke).\n" +
1028
1028
  " • files:[] + replace:true — full replace. Wipes connector dir + writes only the provided files. Use deliberately.\n\n" +
1029
+ "Multi-file connectors (server.js + dashboard HTML + RN bundle + package/manifest): pass each file with content_base64 (a single-line, escape-safe base64 string) instead of content — so you don't hand-escape ~90KB of HTML/JS/JSON inside one tool call. This is the CANONICAL agent path for a full connector; do NOT hand-roll `curl` against the raw endpoint (that skips connector registration / PAT provisioning).\n\n" +
1029
1030
  "Common traps this design prevents:\n" +
1030
1031
  " • Pre-fix bug (2026-06-06): sending just ui-dist HTML wiped server.js + node_modules — connector broke until a full re-upload. Now: those files merge with the GitHub base.\n" +
1031
1032
  " • Pre-fix bug: github:true silently read from `main` even when patches were on `dev`. Now: defaults to dev; pass ref:'main' to opt into the legacy path.",
@@ -1054,11 +1055,12 @@ export const tools = [
1054
1055
  type: "object",
1055
1056
  properties: {
1056
1057
  path: { type: "string", description: "Relative file path (e.g. 'server.js', 'ui-dist/panel/index.html')" },
1057
- content: { type: "string", description: "File content" },
1058
+ content: { type: "string", description: "File content as an inline string. Prefer content_base64 when the content has complex escaping (HTML/JS/JSON)." },
1059
+ content_base64: { type: "string", description: "File content as a single-line base64 string — escape-safe. PREFERRED for a multi-file connector so large HTML/JS/bundles don't need hand-escaping in the tool call. Provide exactly ONE of content / content_base64 per file." },
1058
1060
  },
1059
- required: ["path", "content"],
1061
+ required: ["path"],
1060
1062
  },
1061
- description: "Files to upload. By default merges with the GitHub state at `ref`. Set replace:true to wipe the connector dir and write only these files.",
1063
+ description: "Files to upload — each needs 'path' plus ONE of content (inline string) or content_base64 (escape-safe base64; preferred for multi-file connectors). By default merges with the GitHub state at `ref`. Set replace:true to wipe the connector dir and write only these files.",
1062
1064
  },
1063
1065
  replace: {
1064
1066
  type: "boolean",
@@ -3261,11 +3263,51 @@ const handlers = {
3261
3263
  current = JSON.parse(readResult.content);
3262
3264
  }
3263
3265
  } catch (err) {
3264
- // If it's a skill that doesn't exist yet, create a default scaffold.
3265
- // This lets agents use ateam_patch to both CREATE and UPDATE skills
3266
- // no separate "create" step needed.
3266
+ // OPEN-31 guard: only scaffold-create when the skill is GENUINELY ABSENT.
3267
+ // The old code scaffolded on ANY read error, so a transient github/read
3268
+ // failure (fetch failed, 5xx, parse error) silently OVERWROTE a full
3269
+ // deployed skill with a bare scaffold on the next write — total data loss.
3270
+ //
3271
+ // A genuine "doesn't exist" is a 404 (or the local empty-definition throw).
3272
+ // Anything else = the store is unreachable/broken → FAIL LOUD, never write.
3273
+ const notFound = err.status === 404 || /not found \(empty definition\)/i.test(err.message || "");
3274
+ if (target === "skill" && skill_id && !notFound) {
3275
+ return {
3276
+ ok: false, phase: "read",
3277
+ error: `Refusing to patch "${skill_id}": could not read its current definition from ${isLocal ? "the Builder store" : "GitHub"} (${err.message}). This is NOT a "skill doesn't exist" error (that would be a 404) — scaffolding now could DESTROY the existing definition. Retry once the store is reachable, or check ateam_get_solution(solution_id, skill_id).`,
3278
+ phases,
3279
+ };
3280
+ }
3281
+ // Even on a real 404, the skill may exist in the OTHER source (deployed to
3282
+ // the Builder store but not pushed to GitHub, or vice versa). Scaffolding
3283
+ // then would destroy/diverge that real def — cross-check before creating.
3284
+ if (target === "skill" && skill_id) {
3285
+ let otherDef = null;
3286
+ try {
3287
+ const other = isLocal
3288
+ ? JSON.parse((await get(`/deploy/solutions/${solution_id}/github/read?path=${encodeURIComponent(filePath)}`, sid)).content)
3289
+ : await get(`/deploy/solutions/${solution_id}/skills/${encodeURIComponent(skill_id)}`, sid);
3290
+ otherDef = other?.skill || other?.definition || other;
3291
+ } catch { otherDef = null; /* absent in the other source too → truly new */ }
3292
+ const otherIsReal = otherDef && typeof otherDef === "object" && (
3293
+ (Array.isArray(otherDef.tools) && otherDef.tools.length > 0) ||
3294
+ (Array.isArray(otherDef.connectors) && otherDef.connectors.length > 0) ||
3295
+ otherDef.voice_native || otherDef.ui_plugins ||
3296
+ (otherDef.role && otherDef.role.persona)
3297
+ );
3298
+ if (otherIsReal) {
3299
+ return {
3300
+ ok: false, phase: "read",
3301
+ error: `Refusing to patch "${skill_id}": it was not found in ${isLocal ? "the Builder store" : "GitHub"}, but a full definition EXISTS in ${isLocal ? "GitHub" : "the Builder store"}. Scaffold-creating here would destroy/diverge it. Sync the two first (ateam_redeploy / ateam_verify_consistency), then retry — do NOT patch-create over an existing skill.`,
3302
+ phases,
3303
+ };
3304
+ }
3305
+ }
3306
+ // If it's a skill that GENUINELY doesn't exist (404 in the primary source
3307
+ // AND absent from the other), create a default scaffold. This lets agents
3308
+ // use ateam_patch to both CREATE and UPDATE skills — no separate step.
3267
3309
  if (target === "skill" && skill_id) {
3268
- console.log(`[ateam_patch] Skill "${skill_id}" not found on GitHub — creating new skill scaffold`);
3310
+ console.log(`[ateam_patch] Skill "${skill_id}" genuinely absent (404, both sources) — creating new skill scaffold`);
3269
3311
  isNewSkill = true;
3270
3312
  current = {
3271
3313
  id: skill_id,
@@ -4423,12 +4465,40 @@ const handlers = {
4423
4465
  },
4424
4466
 
4425
4467
  ateam_upload_connector: async ({ solution_id, connector_id, github, files, ref, replace }, sid) => {
4468
+ // OPEN-32: accept content_base64 per file (single-line, escape-safe) and
4469
+ // decode it to plain content here, so an agent can upload a multi-file
4470
+ // connector without hand-escaping ~90KB of HTML/JS/JSON in one tool call —
4471
+ // and via THIS registered path (not a raw curl that skips PAT provisioning).
4472
+ let normFiles = files;
4473
+ if (Array.isArray(files)) {
4474
+ normFiles = files.map((f, i) => {
4475
+ if (!f || typeof f !== "object" || !f.path) {
4476
+ throw new Error(`ateam_upload_connector: files[${i}] must be an object with a 'path'.`);
4477
+ }
4478
+ const hasContent = typeof f.content === "string";
4479
+ const hasB64 = typeof f.content_base64 === "string";
4480
+ if (hasContent && hasB64) {
4481
+ throw new Error(`ateam_upload_connector: files[${i}] ("${f.path}") has BOTH content and content_base64 — provide exactly one.`);
4482
+ }
4483
+ if (!hasContent && !hasB64) {
4484
+ throw new Error(`ateam_upload_connector: files[${i}] ("${f.path}") needs 'content' (inline string) or 'content_base64' (escape-safe base64).`);
4485
+ }
4486
+ if (hasB64) {
4487
+ const decoded = Buffer.from(f.content_base64, "base64").toString("utf8");
4488
+ if (!decoded && f.content_base64.trim()) {
4489
+ throw new Error(`ateam_upload_connector: files[${i}] ("${f.path}") content_base64 did not decode to any content — check the encoding.`);
4490
+ }
4491
+ return { path: f.path, content: decoded };
4492
+ }
4493
+ return { path: f.path, content: f.content };
4494
+ });
4495
+ }
4426
4496
  // Async-first: this runs npm install + build in Core (up to ~7min) and is a
4427
4497
  // prime Cloudflare-524 culprit. Kick async → poll /deploy/jobs; fall back to
4428
4498
  // sync for older backends that don't honor async. Mirrors ateam_github_pull.
4429
4499
  const body = {
4430
4500
  github,
4431
- files,
4501
+ files: normFiles,
4432
4502
  ...(ref ? { ref } : {}),
4433
4503
  ...(replace === true ? { replace: true } : {}),
4434
4504
  };