@ateam-ai/mcp 0.3.57 → 0.4.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/tools.js +218 -28
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ateam-ai/mcp",
3
- "version": "0.3.57",
3
+ "version": "0.4.0",
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
@@ -83,6 +83,89 @@ function _resolveDottedField(obj, dottedPath) {
83
83
  return { parent, leaf: parts[parts.length - 1] };
84
84
  }
85
85
 
86
+ // ─── Protected array fields (v0.4.0 sibling-loss guard) ─────────────
87
+ //
88
+ // Historically, `ateam_patch(target:"solution", updates:{ linked_skills:["only-one"] })`
89
+ // silently REPLACED the whole linked_skills array — wiping every other
90
+ // skill wired into the solution. Same footgun on ui_plugins, handoffs,
91
+ // grants, connectors, etc. The Solution Builder skill hit this in the
92
+ // wild and wiped a tenant's whole solution.
93
+ //
94
+ // From v0.4.0, ateam_patch REFUSES a bare array-replace on any of these
95
+ // fields unless the caller explicitly opts in via one of:
96
+ // updates: { _replace: true, linked_skills: [...] } // object-level
97
+ // updates: { linked_skills: [...], linked_skills_replace: true } // field-level
98
+ // To ADD or REMOVE without opt-in, use the _push / _delete / _update
99
+ // suffix pattern that has always been the correct form.
100
+ const SOLUTION_ARRAY_FIELDS = new Set([
101
+ 'linked_skills',
102
+ 'ui_plugins',
103
+ 'platform_connectors',
104
+ 'handoffs',
105
+ 'grants',
106
+ 'triggers',
107
+ 'notification_routes',
108
+ 'channels',
109
+ 'actor_types',
110
+ 'admin_roles',
111
+ 'plugins',
112
+ 'security_contracts',
113
+ 'connectors',
114
+ 'skills',
115
+ ]);
116
+
117
+ const SKILL_ARRAY_FIELDS = new Set([
118
+ 'tools',
119
+ 'connectors',
120
+ 'handoffs',
121
+ 'scenarios',
122
+ 'triggers',
123
+ 'notification_routes',
124
+ 'plugins',
125
+ 'bootstrap_tools',
126
+ ]);
127
+
128
+ // Returns { ok:false, ... } if the write would REPLACE a protected array
129
+ // without an explicit opt-in; returns null if the write is safe to proceed.
130
+ function _guardArrayReplace({ target, key, value, current, updates }) {
131
+ const knownFields = target === 'skill' ? SKILL_ARRAY_FIELDS : SOLUTION_ARRAY_FIELDS;
132
+ if (!knownFields.has(key)) return null;
133
+ if (!Array.isArray(value)) return null;
134
+ const currentArr = Array.isArray(current) ? current : [];
135
+ if (currentArr.length === 0) return null; // nothing to lose
136
+ if (updates && updates._replace === true) return null;
137
+ if (updates && updates[key + '_replace'] === true) return null;
138
+
139
+ // Compute what would be dropped so the error message is actionable.
140
+ const keyOf = (item) => (item && typeof item === 'object') ? (item.id ?? item.name ?? JSON.stringify(item)) : item;
141
+ const newKeys = new Set(value.map(keyOf));
142
+ const dropped = currentArr.map(keyOf).filter(k => !newKeys.has(k));
143
+ const kept = currentArr.map(keyOf).filter(k => newKeys.has(k));
144
+ const wouldAdd = value.map(keyOf).filter(k => !currentArr.map(keyOf).includes(k));
145
+
146
+ return {
147
+ ok: false,
148
+ phase: 'patch',
149
+ error:
150
+ `⚠️ REFUSED: bare-array replace on ${target}.${key} would drop ${dropped.length} sibling item(s): ` +
151
+ `[${dropped.slice(0, 8).join(', ')}${dropped.length > 8 ? ', ...' : ''}]. ` +
152
+ `This footgun wiped a whole solution in the wild — v0.4.0 refuses it by default. ` +
153
+ `\n\nWhat to do instead:` +
154
+ `\n • To ADD items: updates: { "${key}_push": ${JSON.stringify(wouldAdd)} }` +
155
+ (dropped.length ? `\n • To REMOVE items: updates: { "${key}_delete": ${JSON.stringify(dropped)} }` : '') +
156
+ `\n • To FULLY REPLACE (rare): updates: { "${key}": [...], "${key}_replace": true }` +
157
+ `\n • To replace many arrays in one call: updates: { _replace: true, "${key}": [...] }`,
158
+ dropped_ids: dropped,
159
+ kept_ids: kept,
160
+ would_add: wouldAdd,
161
+ safe_alternatives: {
162
+ push: { [`${key}_push`]: wouldAdd },
163
+ ...(dropped.length && { delete: { [`${key}_delete`]: dropped } }),
164
+ force_replace: { [key]: value, [`${key}_replace`]: true },
165
+ },
166
+ };
167
+ }
168
+
86
169
  // ─── Tool definitions ───────────────────────────────────────────────
87
170
 
88
171
  export const tools = [
@@ -428,24 +511,27 @@ export const tools = [
428
511
  core: true,
429
512
  description:
430
513
  "Surgically update ANY field in a skill or solution definition, redeploy, and optionally re-test — all in one step.\n\n" +
431
- "SUPPORTED OPERATIONS:\n" +
514
+ "⚠️ MERGE-BY-DEFAULT (v0.4.0) — Arrays are protected from silent replace. Bare array writes on solution.linked_skills / ui_plugins / platform_connectors / handoffs / grants / triggers (etc.) and skill.tools / connectors / handoffs / scenarios are REFUSED to prevent sibling loss. Add or remove items with the _push / _delete / _update suffixes; opt into a full-array replace only when you really mean it.\n\n" +
515
+ "OPERATIONS (safe by construction):\n" +
432
516
  "1. Scalar (dot notation): { \"problem.statement\": \"new value\", \"role.persona\": \"You are...\" }\n" +
433
517
  "2. Deep nested: { \"intents.thresholds.accept\": 0.9, \"policy.escalation.enabled\": true }\n" +
434
- "3. Array push: { \"tools_push\": [{ name: \"new_tool\", description: \"...\" }] }\n" +
435
- "4. Array delete: { \"tools_delete\": [\"tool_name\"] }\n" +
436
- "5. Array update: { \"tools_update\": [{ name: \"existing_tool\", description: \"updated\" }] }\n" +
437
- "6. Replace whole section: { \"role\": { persona: \"...\", goals: [...] } }\n\n" +
438
- "EXAMPLES:\n" +
439
- "- Change persona (full replace): updates: { \"role.persona\": \"You are a friendly assistant\" }\n" +
440
- "- Append to persona (don't replace): updates: { \"persona_append\": \"\\n\\nALWAYS respond in 2 sentences.\" }\n" +
518
+ "3. Array APPEND: { \"tools_push\": [{ name: \"new_tool\", description: \"...\" }] }\n" +
519
+ "4. Array REMOVE: { \"tools_delete\": [\"tool_name\"] }\n" +
520
+ "5. Array MODIFY-ONE: { \"tools_update\": [{ name: \"existing_tool\", description: \"updated\" }] }\n" +
521
+ "6. Full-array REPLACE (opt-in): { \"linked_skills\": [...], \"linked_skills_replace\": true } — or { _replace: true, ... } to opt every array in this call.\n\n" +
522
+ "SOLUTION-LEVEL EXAMPLES (target='solution'):\n" +
523
+ "- ADD a skill to the solution: updates: { \"linked_skills_push\": [\"my-new-skill\"] } NOT { linked_skills: [\"my-new-skill\"] } (that would REFUSE — it drops your other skills)\n" +
524
+ "- REMOVE a skill: updates: { \"linked_skills_delete\": [\"old-skill\"] }\n" +
525
+ "- ADD a UI plugin: updates: { \"ui_plugins_push\": [{ id: \"mcp:conn:panel\", ... }] }\n" +
526
+ "- ADD a handoff: updates: { \"handoffs_push\": [{ id: \"h1\", ... }] }\n\n" +
527
+ "SKILL-LEVEL EXAMPLES (target='skill' + skill_id):\n" +
528
+ "- Change persona: updates: { \"role.persona\": \"You are a friendly assistant\" }\n" +
529
+ "- Append to persona: updates: { \"persona_append\": \"\\n\\nALWAYS respond in 2 sentences.\" }\n" +
441
530
  "- Add a guardrail: updates: { \"policy.guardrails.never_push\": [\"Never share passwords\"] }\n" +
442
- "- Update problem: updates: { \"problem.statement\": \"...\", \"problem.goals\": [\"goal1\"] }\n" +
443
531
  "- Add a tool: updates: { \"tools_push\": [{ name: \"conn.tool\", description: \"...\", inputs: [...], output: {...} }] }\n" +
444
532
  "- Change intent: updates: { \"intents.supported_update\": [{ id: \"i1\", description: \"new desc\" }] }\n" +
445
- "- Force redeploy: updates: { \"_force_redeploy\": true }\n" +
446
- "- CREATE a new skill: target='skill', skill_id='my-new-skill', updates: { \"problem.statement\": \"...\", \"role.persona\": \"...\" }\n" +
447
- " If the skill doesn't exist yet, a default scaffold is created and the updates are applied on top. The skill is automatically added to the solution topology.\n\n" +
448
- "Use target='skill' + skill_id for skill fields. Use target='solution' for solution-level fields (linked_skills, platform_connectors, ui_plugins).",
533
+ "- CREATE a new skill: target='skill', skill_id='my-new-skill', updates: { \"problem.statement\": \"...\", \"role.persona\": \"...\" } — auto-scaffolded and added to solution topology.\n\n" +
534
+ "PREVIEW BEFORE WRITING: pass dry_run:true to see the diff (arrays_merged, arrays_replaced, dropped_ids, added_ids) without applying. Use this before any destructive-looking edit.",
449
535
  inputSchema: {
450
536
  type: "object",
451
537
  properties: {
@@ -473,6 +559,10 @@ export const tools = [
473
559
  type: "string",
474
560
  description: "Optional: re-test the skill after patching. Requires skill_id.",
475
561
  },
562
+ dry_run: {
563
+ type: "boolean",
564
+ description: "If true, apply the patch in memory and return the diff (arrays_merged, arrays_replaced, dropped_ids, added_ids, would_write_bytes) WITHOUT writing to GitHub or redeploying. Preview a change before committing to it.",
565
+ },
476
566
  },
477
567
  required: ["solution_id", "target", "updates"],
478
568
  },
@@ -516,7 +606,9 @@ export const tools = [
516
606
  name: "ateam_delete_solution",
517
607
  core: true,
518
608
  description:
519
- "Delete a deployed solution and all its skills from A-Team. Use with caution this removes the solution from both the Skill Builder and A-Team Core. Useful for cleaning up test solutions or starting fresh.",
609
+ "⚠️ IRREVERSIBLE kills Mongo state, running MCP processes, and Builder FS for the whole solution and every skill. " +
610
+ "REQUIRES `confirm:true` AND `confirm_solution_id` echoing the solution id you're destroying (defeats typos and hallucinated ids). " +
611
+ "RECOVERY: the GitHub repo is untouched; `ateam_github_pull` rebuilds the solution from `main`. Prefer that over re-deploying from memory.",
520
612
  inputSchema: {
521
613
  type: "object",
522
614
  properties: {
@@ -524,15 +616,24 @@ export const tools = [
524
616
  type: "string",
525
617
  description: "The solution ID to delete",
526
618
  },
619
+ confirm: {
620
+ type: "boolean",
621
+ description: "REQUIRED. Must be exactly true. A missing/false value refuses the call with a recovery hint.",
622
+ },
623
+ confirm_solution_id: {
624
+ type: "string",
625
+ description: "REQUIRED. Must exactly equal `solution_id`. This defeats typos and hallucinated ids — you can't wipe a solution you couldn't spell.",
626
+ },
527
627
  },
528
- required: ["solution_id"],
628
+ required: ["solution_id", "confirm", "confirm_solution_id"],
529
629
  },
530
630
  },
531
631
  {
532
632
  name: "ateam_delete_skill",
533
633
  core: true,
534
634
  description:
535
- "Delete a single skill from a deployed solution. Removes the skill from A-Team Core (kills the running MCP process, unregisters from skill registry, deletes from Mongo), removes the skill from solution.skills[] and solution.linked_skills, and deletes the skill's files from Builder FS. Use this to drop a skill without tearing down the whole solution.",
635
+ "⚠️ IRREVERSIBLE in Core + Builder FS kills the running MCP process, unregisters from skill registry, deletes the Mongo record, drops from solution.skills[] and solution.linked_skills, and removes the skill's files from Builder FS. " +
636
+ "REQUIRES `confirm:true`. RECOVERY: the skill still lives in GitHub — `ateam_github_pull` rebuilds the whole solution (no per-skill restore path).",
536
637
  inputSchema: {
537
638
  type: "object",
538
639
  properties: {
@@ -544,15 +645,22 @@ export const tools = [
544
645
  type: "string",
545
646
  description: "The skill ID to remove (e.g. 'linkedin-agent')",
546
647
  },
648
+ confirm: {
649
+ type: "boolean",
650
+ description: "REQUIRED. Must be exactly true. A missing/false value refuses the call with a recovery hint.",
651
+ },
547
652
  },
548
- required: ["solution_id", "skill_id"],
653
+ required: ["solution_id", "skill_id", "confirm"],
549
654
  },
550
655
  },
551
656
  {
552
657
  name: "ateam_delete_connector",
553
658
  core: true,
554
659
  description:
555
- "Remove a connector from a deployed solution. Stops and deletes it from A-Team Core, removes references from the solution definition (grants, platform_connectors) and skill definitions (connectors array), and cleans up mcp-store files.",
660
+ "⚠️ CASCADING any skill whose engine.bootstrap_tools or tools[] name a tool from this connector will FAIL its next execution. " +
661
+ "Stops and deletes the connector from A-Team Core; drops references from the solution definition (grants, platform_connectors, ui_plugins ids starting `mcp:<connector-id>:*`) and skill definitions (connectors array); cleans up mcp-store files. " +
662
+ "GitHub source is preserved — a follow-up `ateam_build_and_run(github:true)` can resurrect. " +
663
+ "REQUIRES `confirm:true`.",
556
664
  inputSchema: {
557
665
  type: "object",
558
666
  properties: {
@@ -564,8 +672,12 @@ export const tools = [
564
672
  type: "string",
565
673
  description: "The connector ID to remove (e.g. 'device-mock-mcp')",
566
674
  },
675
+ confirm: {
676
+ type: "boolean",
677
+ description: "REQUIRED. Must be exactly true. A missing/false value refuses the call with a recovery hint.",
678
+ },
567
679
  },
568
- required: ["solution_id", "connector_id"],
680
+ required: ["solution_id", "connector_id", "confirm"],
569
681
  },
570
682
  },
571
683
 
@@ -2535,9 +2647,10 @@ const handlers = {
2535
2647
  // Updates → Redeploys → Optionally tests
2536
2648
  // One call replaces: ateam_update + ateam_redeploy
2537
2649
 
2538
- ateam_patch: async ({ solution_id, target, skill_id, updates, test_message }, sid) => {
2650
+ ateam_patch: async ({ solution_id, target, skill_id, updates, test_message, dry_run }, sid) => {
2539
2651
  const phases = [];
2540
2652
  let isNewSkill = false;
2653
+ const _diff = { arrays_merged: [], arrays_replaced: [], scalars_changed: [], sections_replaced: [] };
2541
2654
 
2542
2655
  // GitHub-first patch: read from GitHub → apply patch → write back → redeploy
2543
2656
  // This ensures GitHub stays the single source of truth.
@@ -2569,7 +2682,7 @@ const handlers = {
2569
2682
  role: { name: "", persona: "", goals: [], limitations: [], communication_style: { tone: "professional", verbosity: "concise" } },
2570
2683
  intents: { supported: [], thresholds: { accept: 0.8, clarify: 0.5, reject: 0.5 }, out_of_domain: { action: "redirect", message: "" } },
2571
2684
  tools: [],
2572
- policy: { guardrails: { never: [], always: [] }, approvals: [], workflows: [], escalation: { enabled: false, conditions: [], target: "" } },
2685
+ policy: { access: { requires_roles: [] }, guardrails: { never: [], always: [] }, approvals: [], workflows: [], escalation: { enabled: false, conditions: [], target: "" } },
2573
2686
  engine: { rv2: { max_iterations: 10, iteration_timeout_ms: 120000, allow_parallel_tools: false, on_max_iterations: "ask_user" }, hlr: { enabled: true, critic: { enabled: true, check_interval: 3, strictness: "medium" }, reflection: { enabled: true, depth: "shallow" }, replanning: { enabled: true, max_replans: 3 } }, autonomy: { level: "supervised" }, finalization_gate: { enabled: true, max_retries: 2 } },
2574
2687
  access_policy: { rules: [{ tools: ["*"], effect: "allow" }] },
2575
2688
  grant_mappings: [],
@@ -2656,9 +2769,30 @@ const handlers = {
2656
2769
  else arr.push(upd);
2657
2770
  }
2658
2771
  parent[leaf] = arr;
2772
+ } else if (key === "_replace" || key.endsWith("_replace")) {
2773
+ // Escape-hatch flags handled by the guard — skip them here so they
2774
+ // don't get written into the patched object as literal fields.
2775
+ continue;
2659
2776
  } else if (key.includes(".")) {
2660
2777
  // Dot notation: "role.persona", "intents.thresholds.accept"
2661
2778
  const parts = key.split(".");
2779
+ // Sibling-loss guard: if the leaf resolves to an existing non-empty
2780
+ // array and the incoming value is also an array, refuse the replace
2781
+ // unless the caller opted in. (Dot-notation is how many agents
2782
+ // accidentally hit this — e.g. updates:{ "linked_skills": ["one"] }
2783
+ // on target='solution'.)
2784
+ const leafKey = parts[parts.length - 1];
2785
+ let cursor = patched;
2786
+ for (let i = 0; i < parts.length - 1; i++) {
2787
+ if (!cursor || typeof cursor[parts[i]] !== 'object') { cursor = null; break; }
2788
+ cursor = cursor[parts[i]];
2789
+ }
2790
+ const currentLeaf = cursor && Object.prototype.hasOwnProperty.call(cursor, leafKey) ? cursor[leafKey] : undefined;
2791
+ const guardErr = _guardArrayReplace({ target, key: leafKey, value, current: currentLeaf, updates });
2792
+ if (guardErr) return guardErr;
2793
+ if (Array.isArray(value) && Array.isArray(currentLeaf)) _diff.arrays_replaced.push(key);
2794
+ else if (typeof value === 'object' && value !== null && !Array.isArray(value)) _diff.sections_replaced.push(key);
2795
+ else _diff.scalars_changed.push(key);
2662
2796
  let obj = patched;
2663
2797
  for (let i = 0; i < parts.length - 1; i++) {
2664
2798
  if (!obj[parts[i]] || typeof obj[parts[i]] !== "object") obj[parts[i]] = {};
@@ -2666,7 +2800,14 @@ const handlers = {
2666
2800
  }
2667
2801
  obj[parts[parts.length - 1]] = value;
2668
2802
  } else {
2669
- // Direct field replacement
2803
+ // Direct top-level field replacement. Sibling-loss guard: if this
2804
+ // names a known array field and would drop items, refuse unless the
2805
+ // caller passed _replace:true (object-level) or <field>_replace:true.
2806
+ const guardErr = _guardArrayReplace({ target, key, value, current: patched[key], updates });
2807
+ if (guardErr) return guardErr;
2808
+ if (Array.isArray(value) && Array.isArray(patched[key])) _diff.arrays_replaced.push(key);
2809
+ else if (typeof value === 'object' && value !== null && !Array.isArray(value)) _diff.sections_replaced.push(key);
2810
+ else _diff.scalars_changed.push(key);
2670
2811
  patched[key] = value;
2671
2812
  }
2672
2813
  }
@@ -2675,6 +2816,23 @@ const handlers = {
2675
2816
  return { ok: false, phase: "patch", error: `Failed to apply patch: ${err.message}` };
2676
2817
  }
2677
2818
 
2819
+ // Dry-run: return diff + would-be after-state without writing to GitHub
2820
+ // or redeploying. Lets an agent preview any destructive-looking edit.
2821
+ if (dry_run) {
2822
+ return {
2823
+ ok: true,
2824
+ dry_run: true,
2825
+ target,
2826
+ solution_id,
2827
+ skill_id,
2828
+ phases,
2829
+ _diff,
2830
+ after_state: patched,
2831
+ would_write_bytes: JSON.stringify(patched, null, 2).length,
2832
+ hint: "No changes applied. Remove dry_run:true to commit + redeploy.",
2833
+ };
2834
+ }
2835
+
2678
2836
  // Phase 3: Write patched version back to GitHub
2679
2837
  try {
2680
2838
  const patchKeys = Object.keys(updates || {});
@@ -3364,14 +3522,46 @@ const handlers = {
3364
3522
  ateam_github_list_versions: async ({ solution_id }, sid) =>
3365
3523
  get(`/deploy/solutions/${solution_id}/versions/dev`, sid),
3366
3524
 
3367
- ateam_delete_solution: async ({ solution_id }, sid) =>
3368
- del(`/deploy/solutions/${solution_id}`, sid),
3525
+ ateam_delete_solution: async ({ solution_id, confirm, confirm_solution_id }, sid) => {
3526
+ if (confirm !== true) {
3527
+ return {
3528
+ ok: false,
3529
+ error: "⚠️ REFUSED: ateam_delete_solution requires confirm:true. This is irreversible in Core + Builder FS. GitHub source is preserved — ateam_github_pull rebuilds from `main` if you already deleted by mistake.",
3530
+ recovery: "ateam_github_pull(solution_id, ref:'main')",
3531
+ };
3532
+ }
3533
+ if (confirm_solution_id !== solution_id) {
3534
+ return {
3535
+ ok: false,
3536
+ error: `⚠️ REFUSED: confirm_solution_id must exactly equal solution_id. Got confirm_solution_id="${confirm_solution_id}" but solution_id="${solution_id}". This check defeats typos and hallucinated ids — you should not be able to wipe a solution whose id you can't spell correctly.`,
3537
+ expected: solution_id,
3538
+ received: confirm_solution_id,
3539
+ };
3540
+ }
3541
+ return del(`/deploy/solutions/${solution_id}`, sid);
3542
+ },
3369
3543
 
3370
- ateam_delete_skill: async ({ solution_id, skill_id }, sid) =>
3371
- del(`/deploy/solutions/${solution_id}/skills/${skill_id}`, sid),
3544
+ ateam_delete_skill: async ({ solution_id, skill_id, confirm }, sid) => {
3545
+ if (confirm !== true) {
3546
+ return {
3547
+ ok: false,
3548
+ error: `⚠️ REFUSED: ateam_delete_skill requires confirm:true. Kills the running MCP process and deletes the skill from Core + Builder FS. GitHub source is preserved — ateam_github_pull rebuilds the whole solution.`,
3549
+ recovery: "ateam_github_pull(solution_id, ref:'main') — no per-skill restore path",
3550
+ };
3551
+ }
3552
+ return del(`/deploy/solutions/${solution_id}/skills/${skill_id}`, sid);
3553
+ },
3372
3554
 
3373
- ateam_delete_connector: async ({ solution_id, connector_id }, sid) =>
3374
- del(`/deploy/solutions/${solution_id}/connectors/${connector_id}`, sid),
3555
+ ateam_delete_connector: async ({ solution_id, connector_id, confirm }, sid) => {
3556
+ if (confirm !== true) {
3557
+ return {
3558
+ ok: false,
3559
+ error: `⚠️ REFUSED: ateam_delete_connector requires confirm:true. Cascading — any skill wired to this connector's tools will fail its next execution. GitHub source is preserved.`,
3560
+ recovery: "ateam_build_and_run(solution_id, github:true) can resurrect from GitHub",
3561
+ };
3562
+ }
3563
+ return del(`/deploy/solutions/${solution_id}/connectors/${connector_id}`, sid);
3564
+ },
3375
3565
 
3376
3566
  ateam_upload_connector: async ({ solution_id, connector_id, github, files, ref, replace }, sid) =>
3377
3567
  post(