@ateam-ai/mcp 0.4.87 → 0.4.89

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 +2 -2
  2. package/src/tools.js +205 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ateam-ai/mcp",
3
- "version": "0.4.87",
3
+ "version": "0.4.89",
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 && node test/actor-binding.test.mjs && node test/spec-topics.test.mjs && node --test test/deploy-status-truth.test.mjs"
16
+ "test": "node test/session-isolation.test.mjs && node test/widget-protocol.test.mjs && node test/actor-binding.test.mjs && node test/spec-topics.test.mjs && node test/example-types.test.mjs && node --test test/deploy-status-truth.test.mjs && node --test test/connector-source-provenance.test.mjs"
17
17
  },
18
18
  "keywords": [
19
19
  "mcp",
package/src/tools.js CHANGED
@@ -633,9 +633,9 @@ export const tools = [
633
633
  properties: {
634
634
  type: {
635
635
  type: "string",
636
- enum: ["skill", "connector", "connector-ui", "solution", "script-cache-skill", "ui-plugin-native", "index"],
636
+ enum: ["skill", "connector", "connector-ui", "solution", "script-cache-skill", "ui-plugin-native", "ui-plugin-iframe", "device-tools", "index"],
637
637
  description:
638
- "Example type: 'skill' = Order Support Agent, 'connector' = stdio MCP connector, 'connector-ui' = UI-capable connector, 'solution' = full 3-skill e-commerce solution, 'script-cache-skill' = fat-tool skill with script_cache opt-in (reference implementation of script-level JIT shortcuts — study this before building any browser-automation skill), 'ui-plugin-native' = complete working React Native (mobile) UI plugin (rn-src/index.tsx + esbuild build:rn → rn-bundle, @adas/plugin-sdk, es2015), 'index' = list all available examples",
638
+ "Example type: 'skill' = Order Support Agent, 'connector' = stdio MCP connector, 'connector-ui' = UI-capable connector, 'solution' = full 3-skill e-commerce solution, 'script-cache-skill' = fat-tool skill with script_cache opt-in (reference implementation of script-level JIT shortcuts — study this before building any browser-automation skill), 'ui-plugin-native' = complete working React Native (mobile) UI plugin (rn-src/index.tsx + esbuild build:rn → rn-bundle, @adas/plugin-sdk, es2015), 'ui-plugin-iframe' = complete working web (iframe) UI plugin with the postMessage protocol, 'device-tools' = a solution's OWN tools that execute ON THE PHONE (runtime:\"device\") — BOTH halves and why they must agree: the plugin-bundle implementation, the connector manifest that declares it (Core cannot introspect a phone, so the manifest is the entire contract), and the skill wiring without which the skill gets none of them. Read this before designing anything that needs a LIVE device reading rather than the last synced one, 'index' = list all available examples",
639
639
  },
640
640
  },
641
641
  required: ["type"],
@@ -1856,7 +1856,9 @@ export const tools = [
1856
1856
  name: "ateam_get_connector_source",
1857
1857
  core: true,
1858
1858
  description:
1859
- `Read the source code files of a deployed MCP connector. Returns all files (server.js, package.json, etc.) stored in the mcp_store for this connector. Use this BEFORE patching or rewriting a connector — always read the current code first so you can make surgical fixes instead of blind full rewrites.`,
1859
+ `Read a connector's AUTHORED source — the code the Builder holds as the source of record (its authored store, or GitHub). Returns a file manifest, or one file's content with path:'<file>'. Use this BEFORE patching or rewriting a connector, so you make surgical fixes instead of blind full rewrites. Every answer carries `+
1860
+ `\`provenance\` (authored_fs | github) so you know which store you are reading.\n\n` +
1861
+ `This does NOT return what Core is currently RUNNING. Those are different questions and used to share one answer: a connector could be deployed and healthy while no authored copy of it existed anywhere, and this tool would hand back the runtime bytes as though they were the source. If there is no authored source you get AUTHORED_SOURCE_MISSING, not a silent substitute. To see the deployed copy use ateam_get_deployed_connector_source; to adopt it as authored source use ateam_recover_connector_source.`,
1860
1862
  inputSchema: {
1861
1863
  type: "object",
1862
1864
  properties: {
@@ -1876,6 +1878,45 @@ export const tools = [
1876
1878
  required: ["solution_id", "connector_id"],
1877
1879
  },
1878
1880
  },
1881
+ {
1882
+ name: "ateam_get_deployed_connector_source",
1883
+ core: true,
1884
+ description:
1885
+ `Read the DEPLOYED copy of a connector — the files ADAS Core is actually running. This is a runtime projection of the last successful deploy, NOT the source of record: it can differ from the authored source, and it may exist for a connector the Builder cannot reproduce at all. Answers carry authored_source_of_record:false so that is never in doubt.\n\n` +
1886
+ `Use it to diagnose ("what is actually running?"), to compare against ateam_get_connector_source, or to inspect a connector whose authored source is missing before deciding whether to adopt it with ateam_recover_connector_source. Do NOT copy bytes out of here and re-upload them as if you had authored them — that launders a runtime copy into the source of record and hides the fact that the real source was lost.`,
1887
+ inputSchema: {
1888
+ type: "object",
1889
+ properties: {
1890
+ solution_id: { type: "string", description: "The solution ID" },
1891
+ connector_id: { type: "string", description: "The connector ID to inspect" },
1892
+ path: {
1893
+ type: "string",
1894
+ description: "Optional. Read ONE file. Omit for a manifest (paths + sizes) — a whole connector exceeds the output limit and truncates.",
1895
+ },
1896
+ },
1897
+ required: ["solution_id", "connector_id"],
1898
+ },
1899
+ },
1900
+ {
1901
+ name: "ateam_recover_connector_source",
1902
+ core: true,
1903
+ description:
1904
+ `ADOPT the deployed copy of a connector as its AUTHORED source. The only sanctioned Core→Builder direction, and deliberately explicit: it is never part of a deploy.\n\n` +
1905
+ `Use it when a connector is running in Core but has no authored source (ateam_get_connector_source returns AUTHORED_SOURCE_MISSING) — the running bytes may be the only surviving copy of real work. The recovered files are STAMPED as recovered_from_core with a timestamp, so a reconstruction is never later mistaken for code someone wrote. Refuses with AUTHORED_SOURCE_EXISTS if authored source is already present; pass force:true only after comparing both copies and deciding the deployed one is the keeper.\n\n` +
1906
+ `Binaries and files over 512KB cannot round-trip and are reported under not_recovered — they stay missing. Push the result to GitHub afterwards so the recovered source is not held in one place only.`,
1907
+ inputSchema: {
1908
+ type: "object",
1909
+ properties: {
1910
+ solution_id: { type: "string", description: "The solution ID" },
1911
+ connector_id: { type: "string", description: "The connector ID to recover" },
1912
+ force: {
1913
+ type: "boolean",
1914
+ description: "Overwrite EXISTING authored source with Core's deployed copy. Default false. Only after comparing the two — the deployed copy may be older than what was authored.",
1915
+ },
1916
+ },
1917
+ required: ["solution_id", "connector_id"],
1918
+ },
1919
+ },
1879
1920
  {
1880
1921
  name: "ateam_get_metrics",
1881
1922
  monitoring: { safe: true, cost: "cheap", latency_ms_p95: 1000, output: "bounded", poll_interval_s: 30 },
@@ -2522,6 +2563,8 @@ const EXAMPLE_PATHS = {
2522
2563
  solution: "/spec/examples/solution",
2523
2564
  "script-cache-skill": "/spec/examples/script-cache-skill",
2524
2565
  "ui-plugin-native": "/spec/examples/ui-plugin-native",
2566
+ "ui-plugin-iframe": "/spec/examples/ui-plugin-iframe",
2567
+ "device-tools": "/spec/examples/device-tools",
2525
2568
  };
2526
2569
 
2527
2570
  // Tools that are tenant-aware — require EXPLICIT ateam_auth (env vars alone not enough).
@@ -2572,6 +2615,8 @@ const TENANT_TOOLS = new Set([
2572
2615
  "ateam_chain_status",
2573
2616
  "ateam_get_widget_catalog",
2574
2617
  "ateam_get_connector_source",
2618
+ "ateam_get_deployed_connector_source",
2619
+ "ateam_recover_connector_source",
2575
2620
  "ateam_get_metrics",
2576
2621
  "ateam_diff",
2577
2622
  "ateam_verify_consistency",
@@ -3125,7 +3170,12 @@ function chainTreeOf(resp) {
3125
3170
  };
3126
3171
  }
3127
3172
 
3128
- const handlers = {
3173
+ // Exported for tests. handleToolCall below is the runtime entry point and stays
3174
+ // the only one production code should use; reaching a handler directly lets a
3175
+ // test EXECUTE it instead of asserting against this file's source text, which
3176
+ // is the difference between proving behaviour and matching a string that a
3177
+ // rename would quietly satisfy.
3178
+ export const handlers = {
3129
3179
  ateam_bootstrap: async () => ({
3130
3180
  runtime: {
3131
3181
  ateam_mcp_version: MCP_VERSION,
@@ -3270,7 +3320,7 @@ const handlers = {
3270
3320
  },
3271
3321
  advanced_tools: {
3272
3322
  _note: "These tools are available but hidden from the default tool list. Call them by name when you need fine-grained control.",
3273
- debugging: ["ateam_get_execution_logs", "ateam_connector_logs", "ateam_get_metrics", "ateam_diff", "ateam_get_connector_source"],
3323
+ debugging: ["ateam_get_execution_logs", "ateam_connector_logs", "ateam_get_metrics", "ateam_diff", "ateam_get_connector_source", "ateam_get_deployed_connector_source"],
3274
3324
  manual_lifecycle: ["ateam_validate_skill", "ateam_validate_solution", "ateam_deploy_solution", "ateam_deploy_skill", "ateam_deploy_connector", "ateam_update", "ateam_redeploy"],
3275
3325
  async_testing: ["ateam_test_status", "ateam_test_abort"],
3276
3326
  other: ["ateam_upload_connector_files", "ateam_solution_chat"],
@@ -3559,7 +3609,18 @@ const handlers = {
3559
3609
 
3560
3610
  ateam_get_workflows: async (_args, sid) => get("/spec/workflows", sid),
3561
3611
 
3562
- ateam_get_examples: async ({ type }, sid) => get(EXAMPLE_PATHS[type], sid),
3612
+ ateam_get_examples: async ({ type }, sid) => {
3613
+ // An unknown type used to reach get(undefined) and fetch the API root, so a
3614
+ // typo answered with something that looked like a valid response. Say what
3615
+ // exists instead — the caller cannot see this map.
3616
+ const path = EXAMPLE_PATHS[type];
3617
+ if (!path) {
3618
+ throw new Error(
3619
+ `Unknown example type "${type}". Available: ${Object.keys(EXAMPLE_PATHS).join(", ")}.`
3620
+ );
3621
+ }
3622
+ return get(path, sid);
3623
+ },
3563
3624
 
3564
3625
  // Design-time capability advisor. Proxies to the Builder's /spec/advisor
3565
3626
  // (LLM over the curated capability catalog). Public endpoint (auth-exempt),
@@ -3633,6 +3694,12 @@ const handlers = {
3633
3694
  if (!mcp_store) {
3634
3695
  try {
3635
3696
  const ghStatus = await get(`/deploy/solutions/${solutionId}/github/status`, sid);
3697
+ // repo_url only says a REPO EXISTS. It has never said the repo carries
3698
+ // this solution's connector source, and treating the two as the same
3699
+ // claim is how a connector with nothing in the repo used to slip
3700
+ // through Phase 0 and then vanish from connectors[] below. The real
3701
+ // per-connector answer comes from pull-bundle, and it is acted on
3702
+ // there — this stays a cheap "is GitHub worth asking at all?" gate.
3636
3703
  if (ghStatus?.repo_url) {
3637
3704
  github = true;
3638
3705
  }
@@ -3677,13 +3744,30 @@ const handlers = {
3677
3744
  // shape to the connector id and dedupe, so a mis-keyed mcp_store can never
3678
3745
  // manufacture file-path connectors. (Core also rejects "/"-bearing ids at
3679
3746
  // its boundary as defense-in-depth.)
3680
- if (!connectors?.length && Object.keys(effectiveMcpStore).length > 0) {
3681
- const connIds = [...new Set(
3682
- Object.keys(effectiveMcpStore).map((k) => {
3747
+ //
3748
+ // B5 A CONNECTOR WITH NO SOURCE IN THE REPO MUST NOT VANISH.
3749
+ // Synthesizing purely from mcp_store keys means a declared connector the
3750
+ // repo does not carry is simply ABSENT from connectors[], so nothing
3751
+ // validates it, nothing reports it, and the deploy proceeds as though it
3752
+ // were never part of the solution. Phase 0 made this likelier by
3753
+ // treating "the repo exists" (repo_url) as "the repo has the source" —
3754
+ // two different claims.
3755
+ //
3756
+ // pull-bundle now answers per connector, so union those ids in. A
3757
+ // connector listed here still deploys when its authored source lives in
3758
+ // the Builder's store; what it can no longer do is disappear.
3759
+ const missingSource = Array.isArray(pullResult.connectors_missing_source)
3760
+ ? pullResult.connectors_missing_source : [];
3761
+ const unreadable = Array.isArray(pullResult.connectors_unreadable)
3762
+ ? pullResult.connectors_unreadable : [];
3763
+ if (!connectors?.length && (Object.keys(effectiveMcpStore).length > 0 || missingSource.length > 0)) {
3764
+ const connIds = [...new Set([
3765
+ ...Object.keys(effectiveMcpStore).map((k) => {
3683
3766
  const m = String(k).match(/^connectors\/([^/]+)\//);
3684
3767
  return m ? m[1] : k;
3685
- })
3686
- )];
3768
+ }),
3769
+ ...missingSource.map((c) => (typeof c === "string" ? c : c?.id)).filter(Boolean),
3770
+ ])];
3687
3771
  connectors = connIds.map((id) => ({
3688
3772
  id,
3689
3773
  name: id,
@@ -3697,6 +3781,12 @@ const handlers = {
3697
3781
  connectors_found: pullResult.connectors_found || 0,
3698
3782
  files_loaded: pullResult.files_loaded || 0,
3699
3783
  connectors_synthesized: connectors?.length || 0,
3784
+ // Named, not swallowed. The repo existing said nothing about these.
3785
+ ...(missingSource.length > 0 && {
3786
+ connectors_missing_source: missingSource,
3787
+ note: "These connectors are declared but the repo carries no source for them. They are still deployed if the Builder holds their authored source; if it does not, validation refuses and ateam_get_connector_source / ateam_recover_connector_source tell you which.",
3788
+ }),
3789
+ ...(unreadable.length > 0 && { connectors_unreadable: unreadable }),
3700
3790
  });
3701
3791
  } catch (err) {
3702
3792
  return {
@@ -5234,8 +5324,46 @@ const handlers = {
5234
5324
  },
5235
5325
 
5236
5326
  ateam_get_connector_source: async ({ solution_id, connector_id, path }, sid) => {
5237
- const data = await get(`/deploy/solutions/${solution_id}/connectors/${connector_id}/source`, sid);
5327
+ let data;
5328
+ try {
5329
+ data = await get(`/deploy/solutions/${solution_id}/connectors/${connector_id}/source`, sid);
5330
+ } catch (err) {
5331
+ // AUTHORED_SOURCE_MISSING is a real, actionable answer — not a lookup
5332
+ // failure. Returning the raw 404 would send a caller hunting for a wrong
5333
+ // solution_id, when the true state is "Core may be running this connector
5334
+ // and nobody can reproduce it". Say that, and name the two tools that act
5335
+ // on it, instead of leaving the agent to improvise a rewrite.
5336
+ let parsed = null;
5337
+ try { parsed = JSON.parse(err.body || "{}"); } catch { /* not JSON */ }
5338
+ if (err.status === 404 && parsed?.code === "AUTHORED_SOURCE_MISSING") {
5339
+ return {
5340
+ ok: false,
5341
+ code: "AUTHORED_SOURCE_MISSING",
5342
+ connector_id,
5343
+ error: parsed.error,
5344
+ deployed_in_core: parsed.deployed_in_core === true,
5345
+ next: parsed.deployed_in_core
5346
+ ? [
5347
+ `ateam_get_deployed_connector_source(solution_id:'${solution_id}', connector_id:'${connector_id}') — see what Core is actually running`,
5348
+ `ateam_recover_connector_source(solution_id:'${solution_id}', connector_id:'${connector_id}') — adopt it as authored source, stamped as recovered`,
5349
+ ]
5350
+ : [`ateam_create_connector — nothing has been authored for this connector yet`],
5351
+ warning: parsed.deployed_in_core
5352
+ ? "Do NOT write a replacement from memory. A running connector's code is recoverable; an improvised rewrite silently replaces working code with a guess."
5353
+ : undefined,
5354
+ };
5355
+ }
5356
+ throw err;
5357
+ }
5238
5358
  const files = Array.isArray(data?.files) ? data.files : [];
5359
+ // Provenance travels with every answer. Without it a caller cannot tell the
5360
+ // authored store from GitHub — and historically could not tell either from
5361
+ // Core's runtime copy, which is how a lost source looked like a present one.
5362
+ const prov = {
5363
+ provenance: data?.provenance,
5364
+ ...(data?.scheme && { scheme: data.scheme }),
5365
+ authored_source_of_record: data?.authored_source_of_record !== false,
5366
+ };
5239
5367
  // A whole connector's source easily exceeds the ~50KB tool-output ceiling and
5240
5368
  // truncates (you couldn't read the file you needed). So: no `path` → return a
5241
5369
  // FILE MANIFEST (paths + sizes, no content — small); with `path` → return just
@@ -5244,6 +5372,7 @@ const handlers = {
5244
5372
  return {
5245
5373
  ok: true,
5246
5374
  connector_id,
5375
+ ...prov,
5247
5376
  files: files.map((f) => ({ path: f.path, bytes: (f.content || "").length, encoding: f.encoding || "utf8" })),
5248
5377
  total_bytes: files.reduce((n, f) => n + (f.content || "").length, 0),
5249
5378
  hint: "Large source is not returned inline. Call again with path:'<file>' to read one file (e.g. path:'server.js').",
@@ -5252,9 +5381,71 @@ const handlers = {
5252
5381
  const norm = String(path).replace(/^\.?\//, "");
5253
5382
  const file = files.find((f) => f.path === path || f.path === norm || f.path.replace(/^\.?\//, "") === norm);
5254
5383
  if (!file) {
5255
- return { ok: false, connector_id, error: `file '${path}' not found`, available: files.map((f) => f.path) };
5384
+ return { ok: false, connector_id, ...prov, error: `file '${path}' not found`, available: files.map((f) => f.path) };
5385
+ }
5386
+ return { ok: true, connector_id, ...prov, path: file.path, encoding: file.encoding || "utf8", content: file.content };
5387
+ },
5388
+
5389
+ ateam_get_deployed_connector_source: async ({ solution_id, connector_id, path }, sid) => {
5390
+ const data = await get(`/deploy/solutions/${solution_id}/connectors/${connector_id}/deployed-source`, sid);
5391
+ const files = Array.isArray(data?.files) ? data.files : [];
5392
+ // The label rides on EVERY response shape, including the error one. A
5393
+ // caller that reads one file out of here must not be able to forget which
5394
+ // store it came from — that forgetting is the whole defect this splits.
5395
+ const label = {
5396
+ provenance: data?.provenance || "core_runtime",
5397
+ authored_source_of_record: false,
5398
+ note: "DEPLOYED copy from Core, not authored source. It reflects the last successful deploy and may differ from what the Builder can reproduce.",
5399
+ };
5400
+ if (!path) {
5401
+ return {
5402
+ ok: true,
5403
+ connector_id,
5404
+ ...label,
5405
+ files: files.map((f) => ({ path: f.path, bytes: (f.content || "").length, encoding: f.encoding || "utf8" })),
5406
+ total_bytes: files.reduce((n, f) => n + (f.content || "").length, 0),
5407
+ hint: "Call again with path:'<file>' to read one file.",
5408
+ };
5409
+ }
5410
+ const norm = String(path).replace(/^\.?\//, "");
5411
+ const file = files.find((f) => f.path === path || f.path === norm || f.path.replace(/^\.?\//, "") === norm);
5412
+ if (!file) {
5413
+ return { ok: false, connector_id, ...label, error: `file '${path}' not found`, available: files.map((f) => f.path) };
5414
+ }
5415
+ return { ok: true, connector_id, ...label, path: file.path, encoding: file.encoding || "utf8", content: file.content };
5416
+ },
5417
+
5418
+ ateam_recover_connector_source: async ({ solution_id, connector_id, force = false }, sid) => {
5419
+ try {
5420
+ const data = await post(
5421
+ `/deploy/solutions/${solution_id}/connectors/${connector_id}/recover-from-core`,
5422
+ { force: force === true },
5423
+ sid,
5424
+ );
5425
+ return data;
5426
+ } catch (err) {
5427
+ // The refusal is the point of the tool, so report it as a decision the
5428
+ // caller has to make rather than as a failure it should retry past.
5429
+ let parsed = null;
5430
+ try { parsed = JSON.parse(err.body || "{}"); } catch { /* not JSON */ }
5431
+ if (err.status === 409 && parsed?.code === "AUTHORED_SOURCE_EXISTS") {
5432
+ return {
5433
+ ok: false,
5434
+ code: "AUTHORED_SOURCE_EXISTS",
5435
+ connector_id,
5436
+ error: parsed.error,
5437
+ next: [
5438
+ `ateam_get_connector_source(solution_id:'${solution_id}', connector_id:'${connector_id}') — the authored copy`,
5439
+ `ateam_get_deployed_connector_source(solution_id:'${solution_id}', connector_id:'${connector_id}') — the deployed copy`,
5440
+ "Compare them. Only if the deployed copy is the one to keep, call again with force:true.",
5441
+ ],
5442
+ };
5443
+ }
5444
+ if (err.status === 404 && parsed?.code === "NOTHING_TO_RECOVER") {
5445
+ return { ok: false, code: "NOTHING_TO_RECOVER", connector_id, error: parsed.error };
5446
+ }
5447
+ throw err;
5256
5448
  }
5257
- return { ok: true, connector_id, path: file.path, encoding: file.encoding || "utf8", content: file.content };
5258
5449
  },
5259
5450
 
5260
5451
  // Render + write CLAUDE.md into the solution's GitHub repo.