@ateam-ai/mcp 0.4.50 → 0.4.52

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.4.50",
3
+ "version": "0.4.52",
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/server.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  CallToolRequestSchema,
11
11
  ListToolsRequestSchema,
12
12
  } from "@modelcontextprotocol/sdk/types.js";
13
- import { tools, coreTools, handleToolCall } from "./tools.js";
13
+ import { tools, coreTools, handleToolCall, MCP_VERSION } from "./tools.js";
14
14
 
15
15
  /**
16
16
  * @param {string} sessionId — identifier for credential isolation.
@@ -18,7 +18,11 @@ import { tools, coreTools, handleToolCall } from "./tools.js";
18
18
  */
19
19
  export function createServer(sessionId = "stdio") {
20
20
  const server = new Server(
21
- { name: "ateam-mcp", version: "0.3.0" },
21
+ // Read the REAL version from package.json. This was hardcoded "0.3.0" while
22
+ // the package shipped 0.4.x — so the MCP handshake advertised a version that
23
+ // had been wrong for dozens of releases, and "which build am I talking to?"
24
+ // could not be answered from the protocol at all.
25
+ { name: "ateam-mcp", version: MCP_VERSION },
22
26
  {
23
27
  capabilities: { tools: {} },
24
28
  instructions: [
package/src/tools.js CHANGED
@@ -31,6 +31,18 @@ const STAMP_WHERE_TOOLS = new Set([
31
31
  import { renderAgentDocHeader, mergeAgentDoc, AGENT_DOC_SENTINEL } from "./agentDoc.js";
32
32
  import { deriveErrorCode, isLogicalFailure } from "./mcpFailure.js";
33
33
 
34
+ // The RUNNING version, read from package.json — never hardcoded. "Deployed" means
35
+ // three different things here (the mac1 container, npm, and each developer's local
36
+ // checkout+process), and a stale local PROCESS is indistinguishable from a broken
37
+ // fix without this. Surfaced in the MCP handshake and in ateam_bootstrap so
38
+ // "which build am I talking to?" is a five-second check, not a three-message
39
+ // round-trip (2026-08-16).
40
+ import { createRequire as _createRequire } from "node:module";
41
+ export const MCP_VERSION = (() => {
42
+ try { return _createRequire(import.meta.url)("../package.json").version; }
43
+ catch { return "unknown"; }
44
+ })();
45
+
34
46
  // ─── Async deploy helper ────────────────────────────────────────────
35
47
  //
36
48
  // All long-running deploy endpoints (build_and_run, redeploy, github_pull)
@@ -450,9 +462,9 @@ export const tools = [
450
462
  properties: {
451
463
  topic: {
452
464
  type: "string",
453
- enum: ["overview", "skill", "solution", "enums", "connector-multi-user", "python_helpers", "widgets", "ui-plugins", "actor-storage", "voice", "voice-native", "triggers", "sub-agent", "consumer-roles", "mobile-connector"],
465
+ enum: ["overview", "skill", "solution", "enums", "connector-multi-user", "python_helpers", "widgets", "ui-plugins", "actor-storage", "voice", "voice-native", "triggers", "sub-agent", "consumer-roles", "mobile-connector", "monitoring"],
454
466
  description:
455
- "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), 'widgets' = widget (UI plugin) spec: catalog model, how_to_use block shape (solution.json snippet + opener_call + persona_phrasing + binding_notes), and rules for declaring ui_plugins. Pair with ateam_get_widget_catalog for the live per-tenant inventory. 'ui-plugins' = the DEEP React Native (mobile) plugin build guide: author in rn-src/, compile with a build:rn esbuild script (format=cjs, target=es2015, external react/react-native/@adas/plugin-sdk) to rn-bundle/index.bundle.js, plain-object export — read this before authoring any MOBILE widget.",
467
+ "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), 'widgets' = widget (UI plugin) spec: catalog model, how_to_use block shape (solution.json snippet + opener_call + persona_phrasing + binding_notes), and rules for declaring ui_plugins. Pair with ateam_get_widget_catalog for the live per-tenant inventory. 'ui-plugins' = the DEEP React Native (mobile) plugin build guide: author in rn-src/, compile with a build:rn esbuild script (format=cjs, target=es2015, external react/react-native/@adas/plugin-sdk) to rn-bundle/index.bundle.js, plain-object export — read this before authoring any MOBILE widget. 'monitoring' = THE MONITORING CONTRACT: which tools are safe to call in a poll loop (with cost / poll interval / whether output stays bounded as the run grows), which are not and what to use instead, plus the running ateam-mcp version. Read this BEFORE writing any loop that watches a build — the safe poll is ateam_chain_status, never ateam_get_chain.",
456
468
  },
457
469
  section: {
458
470
  type: "string",
@@ -1324,7 +1336,10 @@ export const tools = [
1324
1336
  },
1325
1337
  {
1326
1338
  name: "ateam_get_execution_logs",
1327
- core: false,
1339
+ // Advertised (was core:false): these are the RUNTIME DIAGNOSTICS a caller needs
1340
+ // mid-run, but a connector-wildcard grant expands over ADVERTISED tools only, so
1341
+ // hiding them made them ungrantable — invisible to every agent that needed them.
1342
+ core: true,
1328
1343
  description:
1329
1344
  "Get execution logs for a solution — recent jobs with step traces, tool calls, errors, and timing. Essential for debugging what actually happened during skill execution. (Advanced.)",
1330
1345
  inputSchema: {
@@ -1353,6 +1368,10 @@ export const tools = [
1353
1368
  {
1354
1369
  name: "ateam_test_status",
1355
1370
  core: true,
1371
+ // Monitoring-safe for ONE test job (bounded), but include_chain:true pulls the
1372
+ // full tree — that call is NOT safe to loop.
1373
+ monitoring: { safe: true, cost: "cheap", latency_ms_p95: 500, output: "bounded", poll_interval_s: 2,
1374
+ note: "safe:false when include_chain:true (that fetches the full tree)." },
1356
1375
  description:
1357
1376
  "Poll the progress of an async skill test. Returns iteration count, tool call steps, status (running/completed/failed), and result when done.\n\n" +
1358
1377
  "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.",
@@ -1383,6 +1402,10 @@ export const tools = [
1383
1402
  {
1384
1403
  name: "ateam_get_chain",
1385
1404
  core: true,
1405
+ // NOT monitoring-safe: returns the FULL job tree — output grows with the run
1406
+ // (a 200-step job returns 200 steps), which is the property that silently
1407
+ // degrades. Use once at the end; poll ateam_chain_status instead.
1408
+ monitoring: { safe: false, cost: "heavy", output: "grows_with_run", use_instead: "ateam_chain_status" },
1386
1409
  description:
1387
1410
  "Inspect the full chain tree for any job — rooted at the given job_id, walking down through every handoff and askAnySkill subcall.\n\n" +
1388
1411
  "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" +
@@ -1408,6 +1431,17 @@ export const tools = [
1408
1431
  {
1409
1432
  name: "ateam_chain_status",
1410
1433
  core: true,
1434
+ // MONITORING-SAFE — the one tool built for a poll loop. See MONITORING_CONTRACT.
1435
+ // cheap: one status read, no tree walk, no LLM. bounded: the response does NOT
1436
+ // grow with the run (a 200-step job returns the same shape as a 2-step one).
1437
+ monitoring: {
1438
+ safe: true,
1439
+ cost: "cheap",
1440
+ latency_ms_p95: 500,
1441
+ output: "bounded",
1442
+ poll_interval_s: 2,
1443
+ note: "Returns last_activity_at / idle_seconds / activity_source. A LARGE idle_seconds does NOT mean dead — on this platform a healthy build sits minutes inside a single provider call. Read activity_source (what it was doing) with it; never apply an 'idle > N ⇒ dead' rule.",
1444
+ },
1411
1445
  description:
1412
1446
  "SLIM chain status — the chip-quick poll. Given a chain_id (from ateam_conversation), returns the WHOLE-CHAIN aggregate status cheaply: chain_status + chain_done (true only when the ENTIRE chain — root job + every handoff + askAnySkill subcall — is terminal), plus pending_question, result, and a short progress line.\n\n" +
1413
1447
  "This is what you poll on a loop after ateam_conversation — NOT ateam_get_chain (that returns the full tree; too heavy for periodic polling). A single job can finish while the chain is still running, so poll chain_done, not a job's status.\n\n" +
@@ -1540,7 +1574,11 @@ export const tools = [
1540
1574
  },
1541
1575
  {
1542
1576
  name: "ateam_get_metrics",
1543
- core: false,
1577
+ monitoring: { safe: true, cost: "cheap", latency_ms_p95: 1000, output: "bounded", poll_interval_s: 30 },
1578
+ // Advertised (was core:false): these are the RUNTIME DIAGNOSTICS a caller needs
1579
+ // mid-run, but a connector-wildcard grant expands over ADVERTISED tools only, so
1580
+ // hiding them made them ungrantable — invisible to every agent that needed them.
1581
+ core: true,
1544
1582
  description:
1545
1583
  "Get execution metrics — timing, tool stats, bottlenecks, signals, and recommendations. (Advanced.)",
1546
1584
  inputSchema: {
@@ -1565,6 +1603,9 @@ export const tools = [
1565
1603
  {
1566
1604
  name: "ateam_verify",
1567
1605
  core: true,
1606
+ // Real end-state check — probes connectors/widgets/skills. Correct but SLOW;
1607
+ // for a run in flight poll ateam_chain_status and call this once at the end.
1608
+ monitoring: { safe: false, cost: "heavy", output: "bounded", use_instead: "ateam_chain_status" },
1568
1609
  description:
1569
1610
  "ONE call that returns the REAL runtime end-state of a solution — connectors connected + tools discovered, every declared widget actually rendering, skills deployed — with the EXACT failing gaps. Use this instead of guess-and-check after a deploy/patch: it tells you the truth (what's actually live) and names precisely what's broken, not a generic warning. Reliable from any connection (routes through the Builder, not a direct Core call).",
1570
1611
  inputSchema: {
@@ -1970,6 +2011,8 @@ export const tools = [
1970
2011
  {
1971
2012
  name: "ateam_connector_logs",
1972
2013
  core: true,
2014
+ // Bounded by the caller's line limit, but it reads container logs — poll slowly.
2015
+ monitoring: { safe: true, cost: "moderate", latency_ms_p95: 3000, output: "bounded", poll_interval_s: 30 },
1973
2016
  description:
1974
2017
  "Read what a connector process actually PRINTED to stderr. This is the only place a connector's " +
1975
2018
  "internal failure is visible: a tool that catches its own error still returns ok:true, and the widget " +
@@ -2021,6 +2064,8 @@ export const tools = [
2021
2064
  {
2022
2065
  name: "ateam_status_all",
2023
2066
  core: true,
2067
+ // Safe but not cheap-per-second: health across ALL solutions. Poll sparingly.
2068
+ monitoring: { safe: true, cost: "moderate", latency_ms_p95: 3000, output: "bounded", poll_interval_s: 30 },
2024
2069
  description:
2025
2070
  "Show GitHub sync status for ALL tenants and solutions in one call. Requires master key authentication. Returns a summary table of every tenant's solutions with their GitHub sync state.",
2026
2071
  inputSchema: {
@@ -2055,6 +2100,23 @@ export const tools = [
2055
2100
  */
2056
2101
  export const coreTools = tools.filter(t => t.core !== false);
2057
2102
 
2103
+ // MONITORING CONTRACT — machine-filterable answer to "what may I poll on a loop?"
2104
+ // The knowledge used to exist only as prose, scattered and contradictory across
2105
+ // tool descriptions ("too heavy to loop on" in one, "poll every ~2s" in another,
2106
+ // different words, different tools), so a caller had to read paragraphs carefully
2107
+ // to learn which call was safe in a loop. Now it is a field.
2108
+ // safe — may be called repeatedly in a poll loop
2109
+ // cost — cheap | moderate | heavy
2110
+ // output — "bounded" means bounded IN THE RUN'S SIZE, not merely small
2111
+ // today. The silent-degradation failure is a tool that is
2112
+ // concise on a 5-step job and returns 200 steps on a 200-step
2113
+ // job; that is output:"grows_with_run", never safe.
2114
+ // poll_interval_s — the interval the tool is designed for
2115
+ // A tool with NO monitoring field is UNCLASSIFIED — treat as unsafe to poll.
2116
+ export const monitoringTools = tools
2117
+ .filter(t => t.monitoring?.safe === true)
2118
+ .map(t => ({ name: t.name, ...t.monitoring }));
2119
+
2058
2120
  // ─── Tool handlers ──────────────────────────────────────────────────
2059
2121
 
2060
2122
  const SPEC_PATHS = {
@@ -2606,6 +2668,11 @@ module.exports.default = plugin;
2606
2668
 
2607
2669
  const handlers = {
2608
2670
  ateam_bootstrap: async () => ({
2671
+ runtime: {
2672
+ ateam_mcp_version: MCP_VERSION,
2673
+ base_url: getBaseUrl(),
2674
+ _note: "The version of the ateam-mcp process actually serving this call, and the API it talks to. If a fix looks missing, check this FIRST — a local MCP process keeps running the code it loaded at session start, so a pushed/published fix is not live until the process restarts.",
2675
+ },
2609
2676
  platform_positioning: {
2610
2677
  name: "A-Team",
2611
2678
  category: "AI Team Solution Platform",
@@ -2971,6 +3038,25 @@ const handlers = {
2971
3038
  },
2972
3039
 
2973
3040
  ateam_get_spec: async ({ topic, section, search }, sid) => {
3041
+ // Served LOCALLY (no round-trip): the monitoring contract is a property of
3042
+ // THIS ateam-mcp build, so it must answer even when the API is unreachable —
3043
+ // that is exactly when a caller is asking "what can I poll to find out?".
3044
+ if (topic === "monitoring") {
3045
+ return {
3046
+ ateam_mcp_version: MCP_VERSION,
3047
+ safe_to_poll: monitoringTools,
3048
+ not_safe_to_poll: tools
3049
+ .filter(t => t.monitoring?.safe === false)
3050
+ .map(t => ({ name: t.name, cost: t.monitoring.cost, output: t.monitoring.output, use_instead: t.monitoring.use_instead })),
3051
+ unclassified_are_unsafe: true,
3052
+ rules: [
3053
+ "output:'bounded' means bounded IN THE RUN'S SIZE — not merely small today. A tool that returns 200 steps for a 200-step job is grows_with_run and is never poll-safe.",
3054
+ "A tool with no monitoring field is UNCLASSIFIED — treat it as unsafe to poll.",
3055
+ "NEVER apply an 'idle_seconds > N ⇒ dead' rule. On this platform a healthy build regularly sits minutes inside a single provider call; read activity_source alongside it.",
3056
+ ],
3057
+ watching_a_run: "Poll ateam_chain_status (every ~2s, or ~30s for a long build) and read chain_done + last_activity_at/idle_seconds/activity_source. Call ateam_get_chain ONCE at the end for the full tree.",
3058
+ };
3059
+ }
2974
3060
  let path = SPEC_PATHS[topic];
2975
3061
  const params = new URLSearchParams();
2976
3062
  if (section) params.set('section', section);
@@ -4178,12 +4264,9 @@ const handlers = {
4178
4264
  while (Date.now() - startedAt < totalTimeoutMs) {
4179
4265
  const qs = new URLSearchParams();
4180
4266
  qs.set("skillSlug", skill_id);
4181
- const res = await fetch(`${coreUrl}/api/job/${encodeURIComponent(rootJobId)}/chain?${qs}`, {
4182
- method: "GET",
4183
- headers: { "x-api-key": apiKey, "X-ADAS-SERVICE": "ateam-mcp.test_skill_chain" },
4184
- signal: AbortSignal.timeout(15_000),
4185
- }).catch(err => ({ ok: false, _err: err.message }));
4186
- const data = res.ok === false && res._err ? { ok: false, error: res._err } : await res.json().catch(() => ({ ok: false, error: "non-json chain response" }));
4267
+ // Builder proxy, not ADAS_CORE_URL (docker-internal; see ateam_chain_status).
4268
+ const data = await get(`/deploy/jobs/${encodeURIComponent(rootJobId)}/chain?${qs}`, sid)
4269
+ .catch(err => ({ ok: false, error: err.message }));
4187
4270
  lastChain = data;
4188
4271
  const jobs = Array.isArray(data?.chainJobsList) ? data.chainJobsList : Array.isArray(data?.chainJobs) ? data.chainJobs : null;
4189
4272
  if (jobs && jobs.length > 0 && jobs.every(j => isTerminal(j.status))) {
@@ -4337,15 +4420,11 @@ const handlers = {
4337
4420
  const creds = getCredentials(sid);
4338
4421
  const apiKey = creds?.apiKey;
4339
4422
  if (!apiKey) return { ...single, chain: { ok: false, error: "include_chain requires api-key auth (call ateam_auth)" } };
4340
- const coreUrl = process.env.ADAS_CORE_URL || "http://adas-backend:4000";
4341
4423
  const qs = new URLSearchParams();
4342
4424
  if (skill_id) qs.set("skillSlug", skill_id);
4343
- const res = await fetch(`${coreUrl}/api/job/${encodeURIComponent(job_id)}/chain?${qs}`, {
4344
- method: "GET",
4345
- headers: { "x-api-key": apiKey, "X-ADAS-SERVICE": "ateam-mcp.test_status_chain" },
4346
- signal: AbortSignal.timeout(15_000),
4347
- }).catch(err => ({ ok: false, _err: err.message }));
4348
- const chain = res.ok === false && res._err ? { ok: false, error: res._err } : await res.json().catch(() => ({ ok: false, error: "non-json chain response" }));
4425
+ // Builder proxy, not ADAS_CORE_URL (docker-internal; see ateam_chain_status).
4426
+ const chain = await get(`/deploy/jobs/${encodeURIComponent(job_id)}/chain?${qs}`, sid)
4427
+ .catch(err => ({ ok: false, error: err.message }));
4349
4428
  return { ...single, chain };
4350
4429
  },
4351
4430
 
@@ -4354,21 +4433,12 @@ const handlers = {
4354
4433
  const creds = getCredentials(sid);
4355
4434
  const apiKey = creds?.apiKey;
4356
4435
  if (!apiKey) throw new Error("No api_key in session — call ateam_auth(api_key) first.");
4357
- const coreUrl = process.env.ADAS_CORE_URL || "http://adas-backend:4000";
4436
+ // Via the Builder proxy (see ateam_chain_status) — Core's hostname is
4437
+ // docker-internal and unreachable from a desktop/laptop MCP process.
4358
4438
  const qs = new URLSearchParams();
4359
4439
  if (skill_slug) qs.set("skillSlug", skill_slug);
4360
- const res = await fetch(`${coreUrl}/api/job/${encodeURIComponent(job_id)}/chain?${qs}`, {
4361
- method: "GET",
4362
- headers: { "x-api-key": apiKey, "X-ADAS-SERVICE": "ateam-mcp.get_chain" },
4363
- signal: AbortSignal.timeout(15_000),
4364
- });
4365
- const text = await res.text();
4366
- let data;
4367
- try { data = JSON.parse(text); } catch { data = { ok: false, error: text.slice(0, 400) }; }
4368
- if (!res.ok) {
4369
- throw new Error(`Core /api/job/${job_id}/chain returned ${res.status}: ${data.error || JSON.stringify(data).slice(0, 200)}`);
4370
- }
4371
- return data;
4440
+ const suffix = qs.toString() ? `?${qs}` : "";
4441
+ return await get(`/deploy/jobs/${encodeURIComponent(job_id)}/chain${suffix}`, sid);
4372
4442
  },
4373
4443
 
4374
4444
  // SLIM chain status — the chip-quick poll. Hits Core /api/job/:id/status
@@ -4380,21 +4450,26 @@ const handlers = {
4380
4450
  ateam_chain_status: async ({ chain_id, job_id }, sid) => {
4381
4451
  const id = chain_id || job_id;
4382
4452
  if (!id) throw new Error("chain_id required");
4383
- const creds = getCredentials(sid);
4384
- const apiKey = creds?.apiKey;
4385
- if (!apiKey) throw new Error("No api_key in session call ateam_auth(api_key) first.");
4386
- const coreUrl = process.env.ADAS_CORE_URL || "http://adas-backend:4000";
4387
- const res = await fetch(`${coreUrl}/api/job/${encodeURIComponent(id)}/status`, {
4388
- method: "GET",
4389
- headers: { "x-api-key": apiKey, "X-ADAS-SERVICE": "ateam-mcp.chain_status" },
4390
- signal: AbortSignal.timeout(15_000),
4391
- });
4392
- const text = await res.text();
4393
- let data;
4394
- try { data = JSON.parse(text); } catch { data = { ok: false, error: text.slice(0, 400) }; }
4395
- if (!res.ok) {
4396
- throw new Error(`Core /api/job/${id}/status returned ${res.status}: ${data.error || JSON.stringify(data).slice(0, 200)}`);
4397
- }
4453
+ // Routed through the BUILDER proxy (/deploy/jobs/:id/status), not
4454
+ // ADAS_CORE_URL directly. Core is the only holder of job state, but its
4455
+ // hostname is docker-internal every laptop MCP session got a bare "fetch
4456
+ // failed" that read as "job not found" (2026-08-15: a full day spent reading
4457
+ // Mongo by hand to answer "is this run alive?"). Same reason ateam_verify
4458
+ // proxies. `get()` also carries the session's auth/tenant headers.
4459
+ const data = await get(`/deploy/jobs/${encodeURIComponent(id)}/status`, sid);
4460
+ // LAST ACTIVITY — the running-vs-corpse discriminator. `status:"running"` is
4461
+ // true for a healthy build AND a dead one; the only way to tell them apart
4462
+ // was querying llm_traces for the newest timestamp. Core bumps job.lastUpdate
4463
+ // in the same setStatus() call that writes job.subStatus, so the timestamp
4464
+ // and "what it was doing" move together one cheap read, no tree walk, so
4465
+ // this stays safe to poll. idle_seconds alone needs interpreting (a live
4466
+ // build can sit minutes inside one provider call), which is why
4467
+ // activity_source ships with it: "idle 180s — in provider call" is a state
4468
+ // you can act on; "idle 180s" is a number you have to guess about.
4469
+ const lastActivityAt = data.lastUpdate ?? data.last_update ?? null;
4470
+ const idleSeconds = lastActivityAt
4471
+ ? Math.max(0, Math.round((Date.now() - new Date(lastActivityAt).getTime()) / 1000))
4472
+ : null;
4398
4473
  // Surface the chain-aggregate truth as the primary fields; keep the raw
4399
4474
  // slim job under `job` for callers that want per-job detail.
4400
4475
  return {
@@ -4404,6 +4479,9 @@ const handlers = {
4404
4479
  pending_question: data.pendingQuestion || null,
4405
4480
  result: data.result ?? null,
4406
4481
  progress: data.progress || null,
4482
+ last_activity_at: lastActivityAt,
4483
+ idle_seconds: idleSeconds,
4484
+ activity_source: data.subStatus || null,
4407
4485
  job: data,
4408
4486
  };
4409
4487
  },