@ateam-ai/mcp 0.3.26 → 0.3.28

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.3.26",
3
+ "version": "0.3.28",
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/agentDoc.js CHANGED
@@ -156,20 +156,28 @@ ${AGENT_DOC_SENTINEL}
156
156
  * Merge the newly-rendered header with an existing CLAUDE.md, preserving
157
157
  * anything below the sentinel (solution-specific notes).
158
158
  *
159
+ * Output is deterministic so consecutive merges on the same inputs produce
160
+ * byte-identical output (required for the idempotent-write path in
161
+ * ateam_write_agent_doc).
162
+ *
159
163
  * @param {string} freshHeader - Output of renderAgentDocHeader
160
164
  * @param {string|null} existing - Current CLAUDE.md contents (or null if new file)
161
165
  * @returns {string} merged markdown
162
166
  */
163
167
  export function mergeAgentDoc(freshHeader, existing) {
164
- if (!existing) return freshHeader;
165
- const idx = existing.indexOf(AGENT_DOC_SENTINEL);
166
- if (idx === -1) {
167
- // Existing file has no sentinel — treat the whole file as manual notes,
168
- // prepend the auto-generated header, separate by sentinel.
169
- return freshHeader + "\n" + existing.trimStart();
170
- }
171
- const tail = existing.slice(idx + AGENT_DOC_SENTINEL.length);
172
- return freshHeader + tail;
168
+ // Normalize freshHeader: strip trailing whitespace, end with exactly "\n".
169
+ // This prevents newline accumulation when the output is read back and merged again.
170
+ const headerNormalized = freshHeader.replace(/\s+$/, "") + "\n";
171
+
172
+ const extractNotes = (src) => {
173
+ if (!src) return "";
174
+ const idx = src.indexOf(AGENT_DOC_SENTINEL);
175
+ const afterSentinel = idx === -1 ? src : src.slice(idx + AGENT_DOC_SENTINEL.length);
176
+ return afterSentinel.trim();
177
+ };
178
+
179
+ const notes = extractNotes(existing);
180
+ return notes ? `${headerNormalized}\n${notes}\n` : headerNormalized;
173
181
  }
174
182
 
175
183
  // ──────────────────────────────────────────────────────────────────────────
package/src/tools.js CHANGED
@@ -1717,22 +1717,21 @@ const handlers = {
1717
1717
  }
1718
1718
 
1719
1719
  // Auto-seed / refresh the agent onboarding doc. Non-fatal — any failure
1720
- // here must not break a successful deploy. The doc renders from the
1721
- // deployed definition, so only attempt after the repo is present.
1720
+ // here is swallowed so it can't break a successful deploy. The tool is
1721
+ // idempotent: if the rendered doc is byte-identical to what's in the
1722
+ // repo, it returns unchanged:true and writes no commit.
1722
1723
  let agent_doc_result = null;
1723
- if (github_result && !github_result.error) {
1724
- try {
1725
- agent_doc_result = await handlers.ateam_write_agent_doc({ solution_id: solutionId }, sid);
1726
- phases.push({
1727
- phase: "agent_doc",
1728
- status: "done",
1729
- created: agent_doc_result?.created || false,
1730
- preserved_notes: agent_doc_result?.preserved_notes || false,
1731
- });
1732
- } catch (err) {
1733
- agent_doc_result = { error: err.message };
1734
- phases.push({ phase: "agent_doc", status: "error", error: err.message });
1735
- }
1724
+ try {
1725
+ agent_doc_result = await handlers.ateam_write_agent_doc({ solution_id: solutionId }, sid);
1726
+ phases.push({
1727
+ phase: "agent_doc",
1728
+ status: agent_doc_result?.unchanged ? "unchanged" : "done",
1729
+ created: agent_doc_result?.created || false,
1730
+ preserved_notes: agent_doc_result?.preserved_notes || false,
1731
+ });
1732
+ } catch (err) {
1733
+ agent_doc_result = { error: err.message };
1734
+ phases.push({ phase: "agent_doc", status: "skipped", reason: err.message });
1736
1735
  }
1737
1736
 
1738
1737
  return {
@@ -2120,17 +2119,29 @@ const handlers = {
2120
2119
 
2121
2120
  const freshHeader = renderAgentDocHeader({ solution, skills, connectors });
2122
2121
  let existing = null;
2123
- if (!overwrite) {
2124
- try {
2125
- const r = await get(
2126
- `/deploy/solutions/${solution_id}/github/read?path=${encodeURIComponent("CLAUDE.md")}`,
2127
- sid,
2128
- );
2129
- existing = r?.content || null;
2130
- } catch { /* file doesn't exist yet — treat as fresh create */ }
2131
- }
2122
+ try {
2123
+ const r = await get(
2124
+ `/deploy/solutions/${solution_id}/github/read?path=${encodeURIComponent("CLAUDE.md")}`,
2125
+ sid,
2126
+ );
2127
+ existing = r?.content || null;
2128
+ } catch { /* file doesn't exist yet — treat as fresh create */ }
2132
2129
  const merged = overwrite ? freshHeader : mergeAgentDoc(freshHeader, existing);
2133
2130
 
2131
+ // Idempotent write: if the merged content is byte-identical to what's
2132
+ // already committed, skip the patch entirely. Prevents a noise commit on
2133
+ // every build_and_run when nothing meaningful changed.
2134
+ if (existing && existing === merged) {
2135
+ return {
2136
+ ok: true,
2137
+ solution_id,
2138
+ unchanged: true,
2139
+ created: false,
2140
+ preserved_notes: existing.includes(AGENT_DOC_SENTINEL),
2141
+ bytes: merged.length,
2142
+ };
2143
+ }
2144
+
2134
2145
  const res = await post(
2135
2146
  `/deploy/solutions/${solution_id}/github/patch`,
2136
2147
  {
@@ -2143,6 +2154,7 @@ const handlers = {
2143
2154
  return {
2144
2155
  ok: Boolean(res?.ok ?? true),
2145
2156
  solution_id,
2157
+ unchanged: false,
2146
2158
  created: !existing,
2147
2159
  preserved_notes: Boolean(existing && existing.includes(AGENT_DOC_SENTINEL)),
2148
2160
  bytes: merged.length,