@ateam-ai/mcp 0.4.78 → 0.4.80

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 (3) hide show
  1. package/package.json +2 -2
  2. package/src/api.js +30 -0
  3. package/src/tools.js +19 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ateam-ai/mcp",
3
- "version": "0.4.78",
3
+ "version": "0.4.80",
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",
@@ -13,7 +13,7 @@
13
13
  "start:http": "node src/index.js --http",
14
14
  "dev": "node --watch src/index.js",
15
15
  "dev:http": "node --watch src/index.js --http",
16
- "test": "node test/session-isolation.test.mjs && node test/widget-protocol.test.mjs"
16
+ "test": "node test/session-isolation.test.mjs && node test/widget-protocol.test.mjs && node test/actor-binding.test.mjs"
17
17
  },
18
18
  "keywords": [
19
19
  "mcp",
package/src/api.js CHANGED
@@ -173,6 +173,27 @@ export function isExplicitlyAuthenticated(sessionId) {
173
173
  * Record activity on a session — called on every tool call.
174
174
  * Keeps the session alive and updates context for smarter UX.
175
175
  */
176
+ /**
177
+ * Forget the session's bound actor.
178
+ *
179
+ * A session binds an actor from args or from a run-starting tool's result, and
180
+ * NOTHING ever unbound it — so a session that bound a bad value ("dev", a
181
+ * branch name a caller mistook for an actor) sent it on every later request
182
+ * forever, each one 401-ing with `Actor "dev" not found`. Restricting where a
183
+ * bind may come from narrows the entrance; it does not open an exit.
184
+ *
185
+ * Called when Core tells us the actor does not exist. Self-healing beats a
186
+ * hint telling a human to re-authenticate a key that was never the problem.
187
+ */
188
+ export function clearSessionActor(sessionId, reason = "") {
189
+ const session = sessionId ? sessions.get(sessionId) : null;
190
+ if (!session?.context?.actorId) return false;
191
+ const had = session.context.actorId;
192
+ delete session.context.actorId;
193
+ console.warn(`[Auth] Unbound session actor "${had}"${reason ? ` — ${reason}` : ""}. Later calls act as the tenant until an actor is passed again.`);
194
+ return true;
195
+ }
196
+
176
197
  export function touchSession(sessionId, { toolName, solutionId, skillId, actorId } = {}) {
177
198
  const session = sessions.get(sessionId);
178
199
  if (!session) return;
@@ -641,6 +662,15 @@ async function request(method, path, body, sessionId, opts = {}) {
641
662
 
642
663
  if (!res.ok) {
643
664
  const text = await res.text().catch(() => "");
665
+ // SELF-HEAL A BAD ACTOR BINDING. Core saying `Actor "X" not found` is
666
+ // proof the session is carrying an actor that does not exist, and every
667
+ // subsequent request would send it again. Restricting where a bind may
668
+ // come from narrows the entrance; this is the exit. Unbinding costs
669
+ // nothing when the actor was genuinely wrong, and the next call simply
670
+ // acts as the tenant until a real actor is passed.
671
+ if (res.status === 401 && /Actor\s+\\?"?[^"\\,}]+\\?"?\s+not found|unknown actor/i.test(text)) {
672
+ clearSessionActor(sessionId, `Core rejected it on ${method} ${path}`);
673
+ }
644
674
  // Attach the HTTP status so callers can distinguish a genuine 404
645
675
  // (resource absent) from a transient/5xx failure. ateam_patch relies
646
676
  // on this to NOT scaffold-clobber an existing skill on a read error.
package/src/tools.js CHANGED
@@ -4966,7 +4966,25 @@ const handlers = {
4966
4966
  throw new Error("Pass chain_id (the whole run — recommended) or job_id. ateam_conversation returns chain_id.");
4967
4967
  }
4968
4968
  if (!skill_id) {
4969
- throw new Error(`job_id "${job_id}" needs skill_id too the per-job endpoint is scoped by skill. Pass chain_id instead to poll the whole run without knowing which skill ran it.`);
4969
+ // ASK FOR NOTHING THE TOOL CAN WORK OUT ITSELF. This used to throw
4970
+ // "job_id needs skill_id too — pass chain_id instead". The message was
4971
+ // accurate and well-written, and it was reached by FAILING FIRST, every
4972
+ // time: 4 occurrences on 4 different job ids in a single clean e2e, each
4973
+ // one a wasted turn. A good error still costs a round trip.
4974
+ //
4975
+ // /deploy/jobs/:id/status resolves ANY job id — it is the same endpoint
4976
+ // the chain path above already uses — so a job id without a skill needs no
4977
+ // skill at all. `scope` says which question was answered, because "this
4978
+ // job" and "the whole run" genuinely differ: a root job can read completed
4979
+ // while a handoff is still running.
4980
+ const data = await get(`/deploy/jobs/${encodeURIComponent(job_id)}/status`, sid);
4981
+ return {
4982
+ ok: true,
4983
+ scope: "job",
4984
+ job_id,
4985
+ ...data,
4986
+ _note: `Answered for THIS JOB. For the whole run — including handoffs a root job does not cover — pass chain_id instead${data?.chainId ? ` (this job's chain is "${data.chainId}")` : ""}.`,
4987
+ };
4970
4988
  }
4971
4989
  // Existing single-job snapshot via Builder (unchanged shape for back-compat).
4972
4990
  const single = await get(`/deploy/solutions/${solution_id}/skills/${skill_id}/test/${job_id}`, sid);