@ateam-ai/mcp 0.3.52 → 0.3.54

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 +168 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ateam-ai/mcp",
3
- "version": "0.3.52",
3
+ "version": "0.3.54",
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
@@ -131,15 +131,15 @@ export const tools = [
131
131
  name: "ateam_get_spec",
132
132
  core: true,
133
133
  description:
134
- "Get the A-Team specification — schemas, validation rules, system tools, agent guides, and templates. Start here after bootstrap to understand how to build skills and solutions. Use 'section' to get just one part of the skill spec (much smaller than the full spec). Use 'search' to find specific fields or concepts across the spec.",
134
+ "Get the A-Team specification — schemas, validation rules, system tools, agent guides, and templates. Start here after bootstrap to understand how to build skills and solutions. Use 'section' to get just one part of the skill spec (much smaller than the full spec). Use 'search' to find specific fields or concepts across the spec.\n\nWhen designing a persona that orchestrates logic via run_python_script (the Python-as-orchestrator pattern), also fetch topic='python_helpers' — that returns the adas.* helper namespace reference. Skills designed without knowing about adas.* produce 5-10x larger / brittler scripts.",
135
135
  inputSchema: {
136
136
  type: "object",
137
137
  properties: {
138
138
  topic: {
139
139
  type: "string",
140
- enum: ["overview", "skill", "solution", "enums", "connector-multi-user"],
140
+ enum: ["overview", "skill", "solution", "enums", "connector-multi-user", "python_helpers"],
141
141
  description:
142
- "What to fetch: 'overview' = API overview + endpoints, 'skill' = full skill spec, 'solution' = full solution spec, 'enums' = all enum values, 'connector-multi-user' = multi-user connector guide",
142
+ "What to fetch: 'overview' = API overview + endpoints, 'skill' = full skill spec, 'solution' = full solution spec, 'enums' = all enum values, 'connector-multi-user' = multi-user connector guide, 'python_helpers' = adas.* helper namespace for run_python_script orchestration (read this when designing personas that read state → call tools → checkpoint → status; without it, scripts hand-roll JSON parsing and tool delegation = 5-10x larger and brittler).",
143
143
  },
144
144
  section: {
145
145
  type: "string",
@@ -241,7 +241,11 @@ export const tools = [
241
241
  name: "ateam_test_skill",
242
242
  core: true,
243
243
  description:
244
- "Send a test message to a deployed skill and get the full execution result. By default waits for completion (up to 60s). Set wait=false for async mode — returns job_id immediately, then poll with ateam_test_status.",
244
+ "Send a test message to a deployed skill and get the execution result.\n\n" +
245
+ "Wait modes (wait_for):\n" +
246
+ " • 'root' (default, back-compat) — wait until the message's root job completes, return single-job result. Fast, ignores any sub-skills the root delegated to via askAnySkill.\n" +
247
+ " • 'chain' — wait until EVERY job in the chain (root + handoffs + askAnySkill subcalls, recursively) reaches a terminal state, then return the full chain tree. Use when testing multi-skill flows (orchestrator → workers, builders → sub-builders, etc.). The response.chain field carries chainJobs[] with parentJobId/relation/depth and executionSteps[] with tool-nesting (opId/parentOpId/_toolDepth).\n\n" +
248
+ "Legacy: wait:false is equivalent to wait_for:'never' — returns job_id immediately for polling via ateam_test_status. wait:true is the same as the default wait_for:'root'.",
245
249
  inputSchema: {
246
250
  type: "object",
247
251
  properties: {
@@ -260,7 +264,18 @@ export const tools = [
260
264
  wait: {
261
265
  type: "boolean",
262
266
  description:
263
- "If true (default), wait for completion. If false, return job_id immediately for polling via ateam_test_status.",
267
+ "Legacy: if false, return job_id immediately for polling. If true or omitted, behaves like wait_for:'root'. Prefer wait_for going forward.",
268
+ },
269
+ wait_for: {
270
+ type: "string",
271
+ enum: ["root", "chain", "never"],
272
+ description:
273
+ "What to wait for before returning. 'root' (default) = root job done; 'chain' = every chain job terminal (use for multi-skill flows); 'never' = return job_id immediately (poll via ateam_test_status). When 'chain', the response includes the chain tree under response.chain.",
274
+ },
275
+ chain_timeout_ms: {
276
+ type: "number",
277
+ description:
278
+ "Optional. Max total ms to wait when wait_for:'chain'. Default 300000 (5 min). Long-running chains (skill-factory, large bundle builds) may need higher. Clamped to [10000, 900000].",
264
279
  },
265
280
  actor_id: {
266
281
  type: "string",
@@ -913,7 +928,8 @@ export const tools = [
913
928
  name: "ateam_test_status",
914
929
  core: true,
915
930
  description:
916
- "Poll the progress of an async skill test. Returns iteration count, tool call steps, status (running/completed/failed), and result when done. (Advanced — use ateam_test_skill with wait=true for synchronous testing.)",
931
+ "Poll the progress of an async skill test. Returns iteration count, tool call steps, status (running/completed/failed), and result when done.\n\n" +
932
+ "Set include_chain:true to ALSO include the full chain tree (every job in the chain, rooted at this job_id, with parent/child linkage). Use when this job dispatched askAnySkill subcalls and you want a single snapshot of the whole multi-skill state instead of polling each child job_id separately.",
917
933
  inputSchema: {
918
934
  type: "object",
919
935
  properties: {
@@ -929,10 +945,40 @@ export const tools = [
929
945
  type: "string",
930
946
  description: "The job ID returned by ateam_test_skill",
931
947
  },
948
+ include_chain: {
949
+ type: "boolean",
950
+ description:
951
+ "If true, includes response.chain — the full chain tree rooted at this job_id (chainJobs[] with parentJobId/relation/depth, executionSteps[] with tool-nesting). Costs one extra Core call. Default false (back-compat).",
952
+ },
932
953
  },
933
954
  required: ["solution_id", "skill_id", "job_id"],
934
955
  },
935
956
  },
957
+ {
958
+ name: "ateam_get_chain",
959
+ core: true,
960
+ description:
961
+ "Inspect the full chain tree for any job — rooted at the given job_id, walking down through every handoff and askAnySkill subcall.\n\n" +
962
+ "Use when a chain has already run and you want to analyze the structure: which skill called which, how deep the call tree went, which tool inside which job invoked which sub-tool. The two main shapes:\n" +
963
+ " • response.chain.chainJobs[] — one entry per job in the chain. Fields: jobId, skill, status, iteration, depth (0 = root, +1 per askAnySkill subcall hop), relation ('root' | 'subcall' | 'handoff'), parentJobId, parentSkill, goal.\n" +
964
+ " • response.chain.executionSteps[] — every tool call across all chain jobs, tagged with _skill, _jobId, _depth (= job depth), _relation, _parentSkill, _parentJobId, _toolDepth (tool-in-tool nesting via opId/parentOpId).\n\n" +
965
+ "Differs from ateam_test_status by purpose: status is for live polling of a job you just kicked off; get_chain is for post-hoc tree analysis (debugging multi-skill flows, regression testing, comparing two runs).\n\n" +
966
+ "Auth: forwards your authed api_key. Tenant scoped by the key itself. Actor scoping: you can only inspect chains rooted at jobs your actor has access to.",
967
+ inputSchema: {
968
+ type: "object",
969
+ properties: {
970
+ job_id: {
971
+ type: "string",
972
+ description: "The root job ID of the chain to inspect (or any job inside the chain — Core walks up to the root).",
973
+ },
974
+ skill_slug: {
975
+ type: "string",
976
+ description: "Optional. The skill slug for the job — speeds up the lookup when the job isn't in memory and must be loaded from storage. Omit if you don't have it; lookup still works but does an extra round-trip.",
977
+ },
978
+ },
979
+ required: ["job_id"],
980
+ },
981
+ },
936
982
  {
937
983
  name: "ateam_test_abort",
938
984
  core: true,
@@ -1451,6 +1497,7 @@ const SPEC_PATHS = {
1451
1497
  solution: "/spec/solution",
1452
1498
  enums: "/spec/enums",
1453
1499
  "connector-multi-user": "/spec/multi-user-connector",
1500
+ python_helpers: "/spec/python_helpers",
1454
1501
  };
1455
1502
 
1456
1503
  const EXAMPLE_PATHS = {
@@ -1492,6 +1539,7 @@ const TENANT_TOOLS = new Set([
1492
1539
  "ateam_test_voice",
1493
1540
  "ateam_test_status",
1494
1541
  "ateam_test_abort",
1542
+ "ateam_get_chain",
1495
1543
  "ateam_get_connector_source",
1496
1544
  "ateam_get_metrics",
1497
1545
  "ateam_diff",
@@ -2793,11 +2841,77 @@ const handlers = {
2793
2841
  return post(`/deploy/solutions/${solution_id}/test`, body, sid, { timeoutMs });
2794
2842
  },
2795
2843
 
2796
- ateam_test_skill: async ({ solution_id, skill_id, message, wait, actor_id }, sid) => {
2797
- const asyncMode = wait === false;
2798
- const body = { message, ...(asyncMode ? { async: true } : {}), ...(actor_id ? { actor_id } : {}) };
2799
- const timeoutMs = asyncMode ? 15_000 : 90_000;
2800
- return post(`/deploy/solutions/${solution_id}/skills/${skill_id}/test`, body, sid, { timeoutMs });
2844
+ ateam_test_skill: async ({ solution_id, skill_id, message, wait, wait_for, chain_timeout_ms, actor_id }, sid) => {
2845
+ // Resolve wait mode. Priority: wait_for (new explicit form) > wait (legacy).
2846
+ // wait:false → "never" (return job_id, no polling)
2847
+ // wait:true → "root" (poll root job to completion — current default)
2848
+ // wait_for set use as-is (may also be "chain")
2849
+ let resolvedWait = wait_for || (wait === false ? "never" : "root");
2850
+ if (!["root", "chain", "never"].includes(resolvedWait)) {
2851
+ throw new Error(`Invalid wait_for: ${JSON.stringify(resolvedWait)}. Must be "root", "chain", or "never".`);
2852
+ }
2853
+
2854
+ // Kick off the test (always async on the wire so the Builder doesn't time
2855
+ // out on long-running chains). When wait_for:"root" we then poll the
2856
+ // single-job status; when wait_for:"chain" we poll the chain tree until
2857
+ // every job is terminal; when wait_for:"never" we return the job_id and
2858
+ // caller polls themselves.
2859
+ const isWireAsync = resolvedWait !== "root";
2860
+ const body = { message, ...(isWireAsync ? { async: true } : {}), ...(actor_id ? { actor_id } : {}) };
2861
+ const kickoffTimeoutMs = isWireAsync ? 15_000 : 90_000;
2862
+ const kickoff = await post(`/deploy/solutions/${solution_id}/skills/${skill_id}/test`, body, sid, { timeoutMs: kickoffTimeoutMs });
2863
+
2864
+ if (resolvedWait === "never" || resolvedWait === "root") {
2865
+ // Back-compat path: kickoff response is the same shape callers see today.
2866
+ return kickoff;
2867
+ }
2868
+
2869
+ // wait_for:"chain" — poll the chain tree until every job is terminal.
2870
+ const rootJobId = kickoff?.job_id || kickoff?.jobId;
2871
+ if (!rootJobId) {
2872
+ // Builder returned no job_id — surface kickoff so caller can debug.
2873
+ return { ok: false, error: "ateam_test_skill (wait_for:'chain'): kickoff response has no job_id", kickoff };
2874
+ }
2875
+
2876
+ const POLL_MIN_MS = 10_000;
2877
+ const POLL_MAX_MS = 900_000;
2878
+ const totalTimeoutMs = Math.min(POLL_MAX_MS, Math.max(POLL_MIN_MS, Number(chain_timeout_ms) || 300_000));
2879
+ const POLL_INTERVAL_MS = 2_000;
2880
+ const startedAt = Date.now();
2881
+
2882
+ const creds = getCredentials(sid);
2883
+ const apiKey = creds?.apiKey;
2884
+ if (!apiKey) throw new Error("No api_key in session — call ateam_auth(api_key) first.");
2885
+ const coreUrl = process.env.ADAS_CORE_URL || "http://adas-backend:4000";
2886
+
2887
+ const isTerminal = (status) => status === "done" || status === "completed" || status === "error" || status === "failed" || status === "aborted";
2888
+
2889
+ let lastChain = null;
2890
+ while (Date.now() - startedAt < totalTimeoutMs) {
2891
+ const qs = new URLSearchParams();
2892
+ qs.set("skillSlug", skill_id);
2893
+ const res = await fetch(`${coreUrl}/api/job/${encodeURIComponent(rootJobId)}/chain?${qs}`, {
2894
+ method: "GET",
2895
+ headers: { "x-api-key": apiKey, "X-ADAS-SERVICE": "ateam-mcp.test_skill_chain" },
2896
+ signal: AbortSignal.timeout(15_000),
2897
+ }).catch(err => ({ ok: false, _err: err.message }));
2898
+ const data = res.ok === false && res._err ? { ok: false, error: res._err } : await res.json().catch(() => ({ ok: false, error: "non-json chain response" }));
2899
+ lastChain = data;
2900
+ const jobs = Array.isArray(data?.chainJobsList) ? data.chainJobsList : Array.isArray(data?.chainJobs) ? data.chainJobs : null;
2901
+ if (jobs && jobs.length > 0 && jobs.every(j => isTerminal(j.status))) {
2902
+ return { ok: true, job_id: rootJobId, wait_for: "chain", chain: data, kickoff, elapsed_ms: Date.now() - startedAt };
2903
+ }
2904
+ await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
2905
+ }
2906
+ return {
2907
+ ok: false,
2908
+ error: `Chain wait timed out after ${totalTimeoutMs}ms — some jobs are still running. Increase chain_timeout_ms or poll manually via ateam_test_status(include_chain:true).`,
2909
+ job_id: rootJobId,
2910
+ wait_for: "chain",
2911
+ chain: lastChain,
2912
+ kickoff,
2913
+ elapsed_ms: Date.now() - startedAt,
2914
+ };
2801
2915
  },
2802
2916
 
2803
2917
  ateam_test_notification: async ({ solution_id, actor_id, content, urgency, source, metadata, reply_handler, ...rest }, sid) => {
@@ -2925,8 +3039,49 @@ const handlers = {
2925
3039
  return post(`/deploy/voice-test`, body, sid, { timeoutMs: timeoutTotal });
2926
3040
  },
2927
3041
 
2928
- ateam_test_status: async ({ solution_id, skill_id, job_id }, sid) =>
2929
- get(`/deploy/solutions/${solution_id}/skills/${skill_id}/test/${job_id}`, sid),
3042
+ ateam_test_status: async ({ solution_id, skill_id, job_id, include_chain }, sid) => {
3043
+ // Existing single-job snapshot via Builder (unchanged shape for back-compat).
3044
+ const single = await get(`/deploy/solutions/${solution_id}/skills/${skill_id}/test/${job_id}`, sid);
3045
+ if (!include_chain) return single;
3046
+
3047
+ // Caller asked for the chain tree too. Fetch via Core's /api/job/:id/chain
3048
+ // and merge under response.chain. Single-job fields stay at the top level.
3049
+ const creds = getCredentials(sid);
3050
+ const apiKey = creds?.apiKey;
3051
+ if (!apiKey) return { ...single, chain: { ok: false, error: "include_chain requires api-key auth (call ateam_auth)" } };
3052
+ const coreUrl = process.env.ADAS_CORE_URL || "http://adas-backend:4000";
3053
+ const qs = new URLSearchParams();
3054
+ if (skill_id) qs.set("skillSlug", skill_id);
3055
+ const res = await fetch(`${coreUrl}/api/job/${encodeURIComponent(job_id)}/chain?${qs}`, {
3056
+ method: "GET",
3057
+ headers: { "x-api-key": apiKey, "X-ADAS-SERVICE": "ateam-mcp.test_status_chain" },
3058
+ signal: AbortSignal.timeout(15_000),
3059
+ }).catch(err => ({ ok: false, _err: err.message }));
3060
+ const chain = res.ok === false && res._err ? { ok: false, error: res._err } : await res.json().catch(() => ({ ok: false, error: "non-json chain response" }));
3061
+ return { ...single, chain };
3062
+ },
3063
+
3064
+ ateam_get_chain: async ({ job_id, skill_slug }, sid) => {
3065
+ if (!job_id) throw new Error("job_id required");
3066
+ const creds = getCredentials(sid);
3067
+ const apiKey = creds?.apiKey;
3068
+ if (!apiKey) throw new Error("No api_key in session — call ateam_auth(api_key) first.");
3069
+ const coreUrl = process.env.ADAS_CORE_URL || "http://adas-backend:4000";
3070
+ const qs = new URLSearchParams();
3071
+ if (skill_slug) qs.set("skillSlug", skill_slug);
3072
+ const res = await fetch(`${coreUrl}/api/job/${encodeURIComponent(job_id)}/chain?${qs}`, {
3073
+ method: "GET",
3074
+ headers: { "x-api-key": apiKey, "X-ADAS-SERVICE": "ateam-mcp.get_chain" },
3075
+ signal: AbortSignal.timeout(15_000),
3076
+ });
3077
+ const text = await res.text();
3078
+ let data;
3079
+ try { data = JSON.parse(text); } catch { data = { ok: false, error: text.slice(0, 400) }; }
3080
+ if (!res.ok) {
3081
+ throw new Error(`Core /api/job/${job_id}/chain returned ${res.status}: ${data.error || JSON.stringify(data).slice(0, 200)}`);
3082
+ }
3083
+ return data;
3084
+ },
2930
3085
 
2931
3086
  ateam_test_abort: async ({ solution_id, skill_id, job_id }, sid) =>
2932
3087
  del(`/deploy/solutions/${solution_id}/skills/${skill_id}/test/${job_id}`, sid),