@ateam-ai/mcp 0.4.75 → 0.4.77

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/tools.js +92 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ateam-ai/mcp",
3
- "version": "0.4.75",
3
+ "version": "0.4.77",
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/tools.js CHANGED
@@ -2241,6 +2241,24 @@ export const tools = [
2241
2241
  required: ["solution_id"],
2242
2242
  },
2243
2243
  },
2244
+ {
2245
+ name: "ateam_github_reconcile",
2246
+ core: true,
2247
+ description:
2248
+ "JOIN a diverged dev and main. Use when ateam_github_promote returns PROMOTE_NEEDS_HUMAN or PROMOTE_PRECONDITION_FAILED — i.e. the automatic main→dev back-merge could not resolve itself.\n\n" +
2249
+ "TRY sync_from_main FIRST; this tool calls it internally (a plain merge keeps both sides with no judgement call) and only escalates when git genuinely conflicts.\n\n" +
2250
+ "On conflict it writes a TWO-PARENT merge commit so the histories actually join. That matters: copying one branch's tree onto the other makes the contents match while leaving NO merge base, so the very next promote conflicts again — equal content is not a reconciled history.\n\n" +
2251
+ "Conflicting files are resolved per file, NEWEST WINS, and every decision is reported. Recency is a heuristic, not intent: read the decisions. On a real tenant main held the newer solution.json while dev held the newer widget, so a blanket choice would have reverted one of them.\n\n" +
2252
+ "WHO NEEDS THIS: any tenant whose deploys predate the dev-routing fix carries main-only commits the platform itself wrote, and hits this on its first promote afterwards. Pass dry_run:true to see the decisions before writing anything.",
2253
+ inputSchema: {
2254
+ type: "object",
2255
+ properties: {
2256
+ solution_id: { type: "string", description: "The solution ID" },
2257
+ dry_run: { type: "boolean", description: "Report the per-file decisions without writing the merge commit." },
2258
+ },
2259
+ required: ["solution_id"],
2260
+ },
2261
+ },
2244
2262
  {
2245
2263
  name: "ateam_github_sync_from_main",
2246
2264
  core: true,
@@ -5329,10 +5347,78 @@ const handlers = {
5329
5347
  gaps.push(`solution health unavailable: ${e.message}`);
5330
5348
  }
5331
5349
 
5350
+ // 4. SMOKE CALL — actually invoke a tool the SOLUTION depends on.
5351
+ //
5352
+ // Everything above is tools/list-level: `connected`, `tools > 0`, "renders",
5353
+ // "deployed". All of it stayed green on a clinic connector that answered
5354
+ // tools/list correctly and returned 401 on EVERY storage call, because its
5355
+ // generated client put a PAT in the shared-secret header. The build shipped,
5356
+ // reported healthy, and failed the first time a user touched it. The only
5357
+ // check that could have caught it is calling something.
5358
+ //
5359
+ // Tool names come from the SKILLS, not the connector: the connector
5360
+ // endpoints expose a count and a description but no name (verified live —
5361
+ // /connectors/<id>/tools returns 7 objects that are all {description:""}),
5362
+ // and the tools a skill declares are the ones that actually have to work.
5363
+ // Testing those is a better question than testing an arbitrary one.
5364
+ out.smoke = [];
5365
+ try {
5366
+ const wanted = new Map(); // toolName -> connectorId (first skill that declares it)
5367
+ for (const sk of Array.isArray(out.skills) ? out.skills : []) {
5368
+ if (!sk?.id) continue;
5369
+ const def = await get(`/deploy/solutions/${solution_id}/skills/${sk.id}`, sid).catch(() => null);
5370
+ for (const t of (def?.skill?.tools || def?.tools || [])) {
5371
+ const name = typeof t === "string" ? t : t?.name;
5372
+ if (!name || wanted.has(name)) continue;
5373
+ const conn = (typeof t === "object" && (t?.source?.connector || t?.source?.id)) || null;
5374
+ wanted.set(name, conn);
5375
+ }
5376
+ }
5377
+
5378
+ // READ-SHAPED ONLY. A smoke test must never book an appointment or delete a
5379
+ // row to prove a connector is alive, so a write-shaped name is skipped and
5380
+ // SAID to be skipped rather than quietly passed over.
5381
+ const readish = [...wanted.keys()].filter((n) => /(^|[._])(list|get|today|available|health|status|ping|info|search)([._]|$)/i.test(n));
5382
+ const perConnector = new Map();
5383
+ for (const name of readish) {
5384
+ const conn = wanted.get(name) || (Array.isArray(out.connectors) && out.connectors[0]?.id) || null;
5385
+ if (!conn || perConnector.has(conn)) continue;
5386
+ perConnector.set(conn, name);
5387
+ }
5388
+
5389
+ for (const c of Array.isArray(out.connectors) ? out.connectors : []) {
5390
+ // DELIBERATELY NOT GATED ON c.connected. Connectors are LAZY — one idles
5391
+ // back to sleep between step 1 and here, and the /call endpoint wakes it
5392
+ // on demand. Skipping a sleeping connector would make this check silently
5393
+ // untestable exactly when it is most needed, and "asleep" is not an
5394
+ // answer to "do its calls work?". The call itself is the verdict.
5395
+ const pick = perConnector.get(c.id);
5396
+ if (!pick) {
5397
+ out.smoke.push({ connector: c.id, called: null, note: "no read-shaped tool declared by any skill — NOT smoke-tested" });
5398
+ gaps.push(`connector '${c.id}' was not smoke-tested (no read-shaped tool declared by a skill); tools/list working does NOT prove its calls succeed`);
5399
+ continue;
5400
+ }
5401
+ try {
5402
+ const r = await post(`/deploy/solutions/${solution_id}/connectors/${c.id}/call`, { tool: pick, args: {} }, sid);
5403
+ const failed = r?.ok === false;
5404
+ out.smoke.push({ connector: c.id, called: pick, ok: !failed, ...(failed && { error: String(r?.error || "").slice(0, 200) }) });
5405
+ if (failed) {
5406
+ gaps.push(`connector '${c.id}' lists ${c.tools} tool(s) but CALLING ${pick} failed: ${String(r?.error || "").slice(0, 160)} — tools/list works and real calls do not`);
5407
+ }
5408
+ } catch (e) {
5409
+ out.smoke.push({ connector: c.id, called: pick, ok: false, error: e.message.slice(0, 200) });
5410
+ gaps.push(`connector '${c.id}' smoke call ${pick} errored: ${e.message.slice(0, 160)}`);
5411
+ }
5412
+ }
5413
+ } catch (e) {
5414
+ out.smoke = { error: e.message };
5415
+ gaps.push(`smoke check could not run: ${e.message}`);
5416
+ }
5417
+
5332
5418
  out.gaps = gaps;
5333
5419
  out.ok = gaps.length === 0;
5334
5420
  out._status = out.ok
5335
- ? "✅ Verified live — connectors connected, widgets render, skills deployed."
5421
+ ? "✅ Verified live — connectors connected AND answering real calls, widgets render, skills deployed."
5336
5422
  : `⚠️ ${gaps.length} gap(s): ${gaps.slice(0, 5).join("; ")}${gaps.length > 5 ? " …" : ""}`;
5337
5423
  return out;
5338
5424
  },
@@ -5412,6 +5498,11 @@ const handlers = {
5412
5498
  ateam_github_promote: async ({ solution_id, label, dry_run, skip_tag }, sid) =>
5413
5499
  post(`/deploy/solutions/${solution_id}/promote`, { label, dry_run, skip_tag }, sid),
5414
5500
 
5501
+ ateam_github_reconcile: async ({ solution_id, dry_run }, sid) => {
5502
+ if (!solution_id) throw new Error("solution_id required");
5503
+ return await post(`/deploy/solutions/${solution_id}/reconcile`, { dry_run: dry_run === true }, sid);
5504
+ },
5505
+
5415
5506
  ateam_github_sync_from_main: async ({ solution_id, dry_run }, sid) =>
5416
5507
  post(`/deploy/solutions/${solution_id}/sync-from-main`, { dry_run }, sid),
5417
5508