@ateam-ai/mcp 0.4.15 → 0.4.17

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 +313 -77
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ateam-ai/mcp",
3
- "version": "0.4.15",
3
+ "version": "0.4.17",
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
@@ -74,6 +74,88 @@ async function pollDeployJob(jobId, sid, { label = 'deploy', maxMs = 15 * 60_000
74
74
  };
75
75
  }
76
76
 
77
+ // ─── Widget health verification ────────────────────────────────────
78
+ //
79
+ // A skill/solution that declares UI plugins (ui_plugins[]) can silently ship
80
+ // a NON-RENDERING widget: the connector may not expose the plugin via
81
+ // ui.listPlugins, the manifest may lack a render block, or the declared id may
82
+ // be mistyped. Core's live catalog (GET /api/ui-plugins) reflects what Core
83
+ // ACTUALLY discovered — it calls each connector's ui.listPlugins live — so we
84
+ // cross-check every declared plugin against it AND assert a usable render
85
+ // block. Callers fold the report into deploy/verify output so a broken widget
86
+ // is surfaced at deploy time, not discovered later as a blank panel.
87
+ //
88
+ // Returns null when the solution declares no widgets (nothing to check), else
89
+ // { ok, checked, healthy, plugins[], issues[]?, hint? }.
90
+ function _widgetHasRender(r) {
91
+ if (!r || typeof r !== "object" || !r.mode) return false;
92
+ const hasIframe = !!(r.iframeUrl || r.iframe?.iframeUrl);
93
+ const hasRn = !!(r.reactNative?.component);
94
+ if (r.mode === "iframe") return hasIframe;
95
+ if (r.mode === "react-native") return hasRn;
96
+ if (r.mode === "adaptive") return hasIframe || hasRn;
97
+ return true; // unknown mode — don't false-positive on a custom render
98
+ }
99
+
100
+ async function verifyWidgetHealth(solution_id, sid) {
101
+ // 1. Declared plugins — solution.ui_plugins[]
102
+ let declared = [];
103
+ try {
104
+ const def = await get(`/deploy/solutions/${solution_id}/definition`, sid);
105
+ const sol = def?.solution || def?.definition || def || {};
106
+ declared = Array.isArray(sol.ui_plugins) ? sol.ui_plugins : [];
107
+ } catch (e) {
108
+ return { ok: false, error: `widget health: could not read solution definition — ${e.message}` };
109
+ }
110
+ if (declared.length === 0) return null; // no widgets → nothing to verify
111
+
112
+ // 2. Live catalog — what Core actually discovered/serves right now
113
+ const apiKey = getCredentials(sid)?.apiKey;
114
+ let live = [];
115
+ try {
116
+ const coreUrl = process.env.ADAS_CORE_URL || "http://adas-backend:4000";
117
+ const res = await fetch(`${coreUrl}/api/ui-plugins`, {
118
+ headers: { "x-api-key": apiKey, "X-ADAS-SERVICE": "ateam-mcp.verify_widget_health" },
119
+ signal: AbortSignal.timeout(15_000),
120
+ });
121
+ const data = await res.json().catch(() => ({}));
122
+ live = Array.isArray(data?.plugins) ? data.plugins : [];
123
+ } catch (e) {
124
+ return { ok: false, error: `widget health: could not read live plugin catalog — ${e.message}` };
125
+ }
126
+ const liveById = new Map(live.map((p) => [p?.id, p]));
127
+
128
+ // 3. Cross-check each declared plugin against live discovery + render block
129
+ const plugins = declared.map((d) => {
130
+ const id = typeof d === "string" ? d : d?.id;
131
+ const problems = [];
132
+ const found = id ? liveById.get(id) : null;
133
+ if (!id) {
134
+ problems.push("ui_plugins entry has no id");
135
+ } else if (!found) {
136
+ problems.push("not discovered by Core — the owning connector's ui.listPlugins does not return this id (check the plugin id, and that the connector is ui_capable + deployed)");
137
+ }
138
+ const render = found?.render || (typeof d === "object" ? d?.render : null);
139
+ const render_ok = _widgetHasRender(render);
140
+ if (found && !render_ok) {
141
+ problems.push("no usable render block — need render.mode + iframeUrl (iframe) or reactNative.component (RN)");
142
+ }
143
+ return { id: id || "(missing)", discovered: !!found, render_ok, problems };
144
+ });
145
+
146
+ const unhealthy = plugins.filter((p) => p.problems.length);
147
+ return {
148
+ ok: unhealthy.length === 0,
149
+ checked: plugins.length,
150
+ healthy: plugins.length - unhealthy.length,
151
+ plugins,
152
+ ...(unhealthy.length && {
153
+ issues: unhealthy.map((p) => `${p.id}: ${p.problems.join("; ")}`),
154
+ hint: "A declared widget Core doesn't discover will render as a blank panel. If the connector was scaffolded by ateam_create_connector, confirm the plugin's ui-dist/<plugin>/manifest.json deployed; for a hardcoded-list connector, add the plugin to its ui.listPlugins/getPlugin. Re-check with ateam_get_widget_catalog.",
155
+ }),
156
+ };
157
+ }
158
+
77
159
  // ─── Dotted-field resolver ─────────────────────────────────────────
78
160
  //
79
161
  // Given an object and a dotted field name, walk down the path creating
@@ -771,11 +853,14 @@ export const tools = [
771
853
  "Eliminates ~50% of identical plugin boilerplate (imports, theme/bridge hooks, " +
772
854
  "postMessage protocol, default export shape). You then fill in the component body. " +
773
855
  "Use kind='iframe' for web-only, 'rn' for mobile-only, 'adaptive' for both. " +
774
- "Auto-discovery (Phase 5 of the strip) picks up the new plugin at next deploy " +
775
- "without a manifest declaration. " +
776
- "The scaffold MERGES into the existing connector (its server.js + other files are preserved) " +
777
- "works on both GitHub-backed and repo-less (locally-deployed) tenants; the merge base is the " +
778
- "GitHub repo when connected, otherwise the deployed connector source.",
856
+ "Also writes ui-dist/<plugin>/manifest.json with the required render block.\n\n" +
857
+ "⚠️ RENDERING IS NOT AUTOMATIC. At deploy, Phase 5 discovers plugins by calling each connector's " +
858
+ "ui.listPlugins + ui.getPlugin a plugin only appears (and renders) if the connector ADVERTISES it there " +
859
+ "with a render.{mode, iframeUrl?, reactNative?} block. Dropping the scaffold files alone does NOT register it. " +
860
+ "If the connector generates its plugin list from ui-dist/<plugin>/manifest.json, the emitted manifest is picked up automatically; " +
861
+ "if the connector has a HARDCODED list (e.g. personal-assistant-ui-mcp: UI_PLUGINS[] + PLUGIN_MANIFESTS{} in server.js), you MUST add this plugin there (copy the render block from the manifest.json). " +
862
+ "Verify after deploy with ateam_get_solution(solution_id, 'connectors_health') or ateam_get_widget_catalog. Then declare it at solution ui_plugins[] so a skill can open it via sys.focusUiPlugin (see ateam_get_spec topic:'widgets').\n\n" +
863
+ "The scaffold MERGES into the existing connector (server.js + other files preserved) — works on GitHub-backed AND repo-less tenants; merge base is the GitHub repo when connected, else the deployed connector source.",
779
864
  inputSchema: {
780
865
  type: "object",
781
866
  properties: {
@@ -1777,80 +1862,151 @@ function _scaffoldConnectorFiles({ connectorId, displayName, uiCapable }) {
1777
1862
  const safeName = displayName || connectorId;
1778
1863
  const files = [];
1779
1864
 
1780
- // server.js — minimal stdio MCP server with one tool stub + ui handlers if ui_capable
1865
+ // server.js — raw-stdio JSON-RPC 2.0 MCP server. This mirrors the PROVEN
1866
+ // pattern deployed connectors use (e.g. nutrition-mcp): tools declared in
1867
+ // tools/list, dispatched in tools/call, no MCP SDK. UI plugins are exposed
1868
+ // as the `ui.listPlugins` / `ui.getPlugin` TOOLS, which is how Core detects a
1869
+ // UI-capable connector (it checks the tools/list output) and fetches each
1870
+ // manifest. Those two tools read the connector's OWN ui-dist/<plugin>/
1871
+ // manifest.json at call time — drop a plugin's files and it renders, with no
1872
+ // server.js edit needed.
1781
1873
  const serverJs = `#!/usr/bin/env node
1782
- // ${connectorId} — stdio MCP server. Generated by ateam_create_connector.
1874
+ // ${connectorId} — stdio JSON-RPC 2.0 MCP server. Generated by ateam_create_connector.
1783
1875
  //
1784
- // Fill in the tool implementations below. The MCP scaffolding
1785
- // (server setup, tool registration, error handling, stdio transport)
1786
- // is template-provided. You write the integration logic.
1876
+ // Fill in your real tools below (see TOOLS + the tools/call dispatch). The
1877
+ // JSON-RPC framing, actor isolation, stdio loop${uiCapable ? ", and ui-dist plugin discovery" : ""} are template-provided.
1878
+ ${uiCapable ? `
1879
+ import { readdirSync, readFileSync, existsSync } from "node:fs";
1880
+ import { fileURLToPath } from "node:url";
1881
+ import { dirname, join } from "node:path";
1882
+ ` : ""}
1883
+ const PROTOCOL_VERSION = "2024-11-05";
1884
+
1885
+ // ── JSON-RPC helpers ──
1886
+ function ok(id, result) { return { jsonrpc: "2.0", id, result }; }
1887
+ function err(id, code, message) { return { jsonrpc: "2.0", id, error: { code, message } }; }
1888
+ function toText(data) { return { content: [{ type: "text", text: JSON.stringify(data) }] }; }
1889
+
1890
+ // ── Actor isolation ── every data tool is per-actor. Core injects _adas_actor
1891
+ // into the call args; refuse to operate without it (prevents cross-actor leaks).
1892
+ function getActorId(args) {
1893
+ const id = args?._adas_actor;
1894
+ if (!id) throw new Error("${connectorId}: no actor context — _adas_actor missing.");
1895
+ return id;
1896
+ }
1897
+ ${uiCapable ? `
1898
+ // ── UI plugin discovery ── read ui-dist/<plugin>/manifest.json at call time so
1899
+ // a newly-uploaded plugin appears with no server.js change. The manifest is the
1900
+ // single source of truth for the render block (ateam_create_plugin writes it).
1901
+ const __dir = dirname(fileURLToPath(import.meta.url));
1902
+ const UI_DIST = join(__dir, "ui-dist");
1903
+
1904
+ function discoverPlugins() {
1905
+ const out = [];
1906
+ if (!existsSync(UI_DIST)) return out;
1907
+ for (const entry of readdirSync(UI_DIST, { withFileTypes: true })) {
1908
+ if (!entry.isDirectory()) continue;
1909
+ const mp = join(UI_DIST, entry.name, "manifest.json");
1910
+ if (!existsSync(mp)) continue;
1911
+ try {
1912
+ const m = JSON.parse(readFileSync(mp, "utf8"));
1913
+ out.push({ ...m, id: m.id || entry.name });
1914
+ } catch (e) {
1915
+ console.error(\`[${connectorId}] bad manifest for \${entry.name}: \${e.message}\`);
1916
+ }
1917
+ }
1918
+ return out;
1919
+ }
1920
+ ` : ""}
1921
+ // ── Tool definitions ── Core reads this list. A tool named "ui.listPlugins"
1922
+ // is how Core knows this connector is UI-capable.
1923
+ function toolSchemas() {
1924
+ const actor = { _adas_actor: { type: "string" }, _adas_tenant: { type: "string" } };
1925
+ return [
1926
+ {
1927
+ name: "${connectorId}.echo",
1928
+ description: "Echo back the input. Replace with your real tools.",
1929
+ inputSchema: { type: "object", properties: { message: { type: "string" }, ...actor }, required: ["message"] },
1930
+ },${uiCapable ? `
1931
+ { name: "ui.listPlugins", description: "List available UI plugins.", inputSchema: { type: "object", properties: {} } },
1932
+ { name: "ui.getPlugin", description: "Get a UI plugin manifest by id.", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },` : ""}
1933
+ ];
1934
+ }
1787
1935
 
1788
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1789
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1936
+ // ── Request handler ──
1937
+ async function handle(req) {
1938
+ const { id, method, params } = req || {};
1790
1939
 
1791
- const server = new Server(
1792
- { name: "${connectorId}", version: "1.0.0" },
1793
- { capabilities: { tools: {}${uiCapable ? ', ui: {}' : ''} } },
1794
- );
1940
+ if (method === "initialize") {
1941
+ return ok(id, { protocolVersion: PROTOCOL_VERSION, serverInfo: { name: "${connectorId}", version: "1.0.0" }, capabilities: { tools: {} } });
1942
+ }
1943
+ if (method === "tools/list") return ok(id, { tools: toolSchemas() });
1795
1944
 
1796
- // ── Tool definitions ────────────────────────────────────────────────
1797
- const TOOLS = [
1798
- {
1799
- name: "${connectorId}.echo",
1800
- description: "Echo back the input. Replace with your real tools.",
1801
- inputSchema: {
1802
- type: "object",
1803
- properties: { message: { type: "string" } },
1804
- required: ["message"],
1805
- },
1806
- },
1807
- ];
1945
+ if (method === "tools/call") {
1946
+ const name = params?.name;
1947
+ const args = params?.arguments || {};
1948
+ try {
1949
+ ${uiCapable ? ` // ── UI registry plumbing (no actor required) ──
1950
+ if (name === "ui.listPlugins") {
1951
+ const plugins = discoverPlugins().map((p) => ({
1952
+ id: p.id, name: p.name || p.id, version: p.version || "1.0.0",
1953
+ description: p.description || "",
1954
+ ...(p.uiActions ? { uiActions: p.uiActions } : {}),
1955
+ ...(p.surface ? { surface: p.surface } : {}),
1956
+ }));
1957
+ return ok(id, toText({ plugins }));
1958
+ }
1959
+ if (name === "ui.getPlugin") {
1960
+ const p = discoverPlugins().find((pl) => pl.id === args.id);
1961
+ if (!p) return ok(id, toText({ error: \`Plugin \${args.id} not found\` }));
1962
+ return ok(id, toText(p)); // manifest already carries the render block
1963
+ }
1964
+ ` : ""} // ── Your tools (actor-scoped) ──
1965
+ const actorId = getActorId(args);
1966
+ if (name === "${connectorId}.echo") {
1967
+ return ok(id, toText({ ok: true, echo: args.message, actor: actorId }));
1968
+ }
1969
+ return err(id, -32601, \`Unknown tool: \${name}\`);
1970
+ } catch (e) {
1971
+ return err(id, -32000, String(e?.message || e));
1972
+ }
1973
+ }
1808
1974
 
1809
- server.setRequestHandler({ method: "tools/list" }, async () => ({ tools: TOOLS }));
1975
+ if (typeof method === "string" && method.startsWith("notifications/")) return null;
1976
+ return err(id, -32601, \`Unknown method: \${method}\`);
1977
+ }
1810
1978
 
1811
- server.setRequestHandler({ method: "tools/call" }, async (req) => {
1812
- const { name, arguments: args } = req.params;
1813
- switch (name) {
1814
- case "${connectorId}.echo":
1815
- return { content: [{ type: "text", text: \`Echo: \${args.message}\` }] };
1816
- default:
1817
- throw new Error(\`Unknown tool: \${name}\`);
1979
+ // ── stdio loop ──
1980
+ let buf = "";
1981
+ process.stdin.setEncoding("utf8");
1982
+ process.stdin.on("data", async (chunk) => {
1983
+ buf += chunk;
1984
+ const lines = buf.split("\\n");
1985
+ buf = lines.pop() || "";
1986
+ for (const line of lines) {
1987
+ const trimmed = line.trim();
1988
+ if (!trimmed) continue;
1989
+ let msg;
1990
+ try { msg = JSON.parse(trimmed); } catch { continue; }
1991
+ const resp = await handle(msg);
1992
+ if (resp) process.stdout.write(JSON.stringify(resp) + "\\n");
1818
1993
  }
1819
1994
  });
1820
- ${uiCapable ? `
1821
- // ── UI plugin handlers (ui_capable connector) ──────────────────────
1822
- server.setRequestHandler({ method: "ui.listPlugins" }, async () => ({
1823
- plugins: [
1824
- // List your plugins here. Plugin source files live in
1825
- // plugins/<plugin-name>/ (RN) and ui-dist/<plugin-name>/ (iframe).
1826
- // Plugin manifests are auto-discovered at deploy time per Phase 5
1827
- // of the strip — no need to repeat them here unless you want overrides.
1828
- ],
1829
- }));
1830
-
1831
- server.setRequestHandler({ method: "ui.getPlugin" }, async (req) => {
1832
- const { id } = req.params;
1833
- // Return plugin manifest by id. Auto-discovery covers the default case.
1834
- return { ok: false, error: \`Plugin \${id} not found\` };
1835
- });
1836
- ` : ''}
1837
- // ── Boot ────────────────────────────────────────────────────────────
1838
- const transport = new StdioServerTransport();
1839
- await server.connect(transport);
1840
- console.error("[${connectorId}] connected via stdio");
1995
+ process.stdin.on("end", () => process.exit(0));
1996
+
1997
+ console.error("[${connectorId}] Server started (stdio)");
1841
1998
  `;
1842
1999
  files.push({ path: "server.js", content: serverJs });
1843
2000
 
1844
- // package.json
2001
+ // package.json — raw stdio needs ZERO npm deps (node built-ins only), so
2002
+ // deploys skip npm install entirely.
1845
2003
  const pkg = {
1846
2004
  name: connectorId,
1847
2005
  version: "1.0.0",
1848
2006
  type: "module",
1849
2007
  description: `${safeName} — A-Team MCP connector`,
1850
2008
  main: "server.js",
1851
- dependencies: {
1852
- "@modelcontextprotocol/sdk": "^1.0.0",
1853
- },
2009
+ dependencies: {},
1854
2010
  };
1855
2011
  files.push({ path: "package.json", content: JSON.stringify(pkg, null, 2) + "\n" });
1856
2012
 
@@ -1862,21 +2018,21 @@ ${uiCapable ? "UI-capable: yes" : ""}
1862
2018
 
1863
2019
  ## Adding tools
1864
2020
 
1865
- Edit \`server.js\`. Add entries to the \`TOOLS\` array, then add a
1866
- matching case in the \`tools/call\` handler.
2021
+ Edit \`server.js\`: add an entry to \`toolSchemas()\` and a matching branch in
2022
+ the \`tools/call\` dispatch. Data tools are per-actor — call \`getActorId(args)\`.
1867
2023
 
1868
2024
  ## Adding UI plugins (ui_capable connectors)
1869
2025
 
1870
- Drop iframe plugins under \`ui-dist/<plugin-name>/index.html\` and/or
1871
- React Native plugins under \`plugins/<plugin-name>/index.tsx\`.
1872
- Phase 5 of the strip auto-discovers them at deploy — no manifest
1873
- declaration needed unless you want overrides (drop a
1874
- \`manifest.json\` next to the source).
2026
+ Use \`ateam_create_plugin\` (or drop the files yourself): iframe plugins go under
2027
+ \`ui-dist/<plugin-name>/index.html\` with a \`ui-dist/<plugin-name>/manifest.json\`
2028
+ (RN under \`plugins/<plugin-name>/index.tsx\`). This connector's
2029
+ \`ui.listPlugins\` / \`ui.getPlugin\` read those manifests at call time, so a new
2030
+ plugin renders with NO server.js edit.
1875
2031
 
1876
2032
  ## Deploy
1877
2033
 
1878
- Use \`ateam_upload_connector\` to push the latest source to Core
1879
- without a full skill redeploy.
2034
+ Use \`ateam_upload_connector\` to push the latest source to Core without a full
2035
+ skill redeploy.
1880
2036
  `;
1881
2037
  files.push({ path: "README.md", content: readme });
1882
2038
 
@@ -2723,6 +2879,17 @@ const handlers = {
2723
2879
  phases.push({ phase: "agent_doc", status: "skipped", reason: err.message });
2724
2880
  }
2725
2881
 
2882
+ // Phase 6: Widget health — if the solution declares UI plugins, verify each
2883
+ // one actually renders (Core discovered it + it has a render block). Catches
2884
+ // the silent "declared but non-rendering" widget at deploy time.
2885
+ let widget_health = null;
2886
+ try {
2887
+ widget_health = await verifyWidgetHealth(solutionId, sid);
2888
+ if (widget_health) {
2889
+ phases.push({ phase: "widget_health", status: widget_health.ok ? "done" : "warn", checked: widget_health.checked });
2890
+ }
2891
+ } catch { /* advisory — never fail a successful deploy on the health check */ }
2892
+
2726
2893
  return {
2727
2894
  ok: true,
2728
2895
  solution_id: solutionId,
@@ -2735,11 +2902,14 @@ const handlers = {
2735
2902
  ...(deploy.auto_expanded_skills?.length > 0 && { auto_expanded: deploy.auto_expanded_skills }),
2736
2903
  },
2737
2904
  health,
2905
+ ...(widget_health && { widget_health }),
2738
2906
  ...(test_result && { test_result }),
2739
2907
  ...(github_result && !github_result.error && !github_result.skipped && { github: github_result }),
2740
2908
  ...(agent_doc_result && !agent_doc_result.error && { agent_doc: agent_doc_result }),
2741
2909
  ...(validation.warnings?.length > 0 && { validation_warnings: validation.warnings }),
2742
- _status: '✅ Deployed to Core + pushed to main.',
2910
+ _status: widget_health && !widget_health.ok
2911
+ ? `✅ Deployed to Core + pushed to main. ⚠️ ${widget_health.issues?.length || 0} widget(s) not rendering — see widget_health.`
2912
+ : '✅ Deployed to Core + pushed to main.',
2743
2913
  _next: 'Create a checkpoint before making more changes: ateam_github_promote(solution_id)',
2744
2914
  };
2745
2915
  },
@@ -3084,6 +3254,17 @@ const handlers = {
3084
3254
 
3085
3255
  const redeployOk = phases.some(p => p.phase === "redeploy" && p.status === "done");
3086
3256
  const store = isLocal ? "Builder store (local)" : "GitHub";
3257
+
3258
+ // Widget health — if the redeploy landed and the solution declares UI
3259
+ // plugins, verify each renders (Core discovered it + has a render block).
3260
+ let widget_health = null;
3261
+ if (redeployOk) {
3262
+ try {
3263
+ widget_health = await verifyWidgetHealth(solution_id, sid);
3264
+ if (widget_health) phases.push({ phase: "widget_health", status: widget_health.ok ? "done" : "warn", checked: widget_health.checked });
3265
+ } catch { /* advisory — never downgrade a successful patch on the health check */ }
3266
+ }
3267
+
3087
3268
  return {
3088
3269
  ok: true,
3089
3270
  solution_id,
@@ -3093,9 +3274,12 @@ const handlers = {
3093
3274
  patched: patched,
3094
3275
  ...(isNewSkill && { created_skill: skill_id }),
3095
3276
  ...(redeployResult && { redeploy: redeployResult }),
3277
+ ...(widget_health && { widget_health }),
3096
3278
  ...(test_result && { test_result }),
3097
3279
  _status: redeployOk
3098
- ? `✅ Patched on ${store} + redeployed.`
3280
+ ? (widget_health && !widget_health.ok
3281
+ ? `✅ Patched on ${store} + redeployed. ⚠️ ${widget_health.issues?.length || 0} widget(s) not rendering — see widget_health.`
3282
+ : `✅ Patched on ${store} + redeployed.`)
3099
3283
  : `⚠️ Patched on ${store} ✅ but redeploy timed out. Run: ateam_redeploy(solution_id` + (skill_id ? `, skill_id: "${skill_id}"` : '') + ')',
3100
3284
  _next: isLocal
3101
3285
  ? 'Local edit saved + redeployed. When the tenant connects a GitHub repo, the local state is pushed → GitHub (which then becomes master).'
@@ -3868,9 +4052,14 @@ const handlers = {
3868
4052
  displayName: name || connector_id,
3869
4053
  uiCapable: !!ui_capable,
3870
4054
  });
4055
+ // replace:true — this is a NEW connector: the scaffold IS the complete file
4056
+ // set, and there's nothing to merge against (no GitHub base, nothing
4057
+ // deployed yet). Without it the upload route's merge-protection 409s a
4058
+ // brand-new connector on a repo-less tenant ("no existing base to merge").
4059
+ // Partial uploads (ateam_create_plugin) still merge; a full create replaces.
3871
4060
  const result = await post(
3872
4061
  `/deploy/solutions/${solution_id}/connectors/${connector_id}/upload`,
3873
- { files },
4062
+ { files, replace: true },
3874
4063
  sid,
3875
4064
  { timeoutMs: 120_000, retries: 1 },
3876
4065
  );
@@ -3912,12 +4101,45 @@ const handlers = {
3912
4101
  sid,
3913
4102
  { timeoutMs: 120_000, retries: 1 },
3914
4103
  );
4104
+
4105
+ // Verify the plugin actually became RENDERABLE — poll Core's live catalog
4106
+ // (which calls the connector's ui.listPlugins) for this plugin id. The
4107
+ // connector restarts on upload, so allow a few seconds to re-scan ui-dist
4108
+ // and re-announce. Turns create_plugin into a VERIFIED result (renders:true
4109
+ // or a concrete reason) instead of a hopeful "files written".
4110
+ const pluginId = `mcp:${connector_id}:${plugin_name}`;
4111
+ let verified = { renders: false, note: "not yet discovered by Core after upload" };
4112
+ try {
4113
+ const apiKey = getCredentials(sid)?.apiKey;
4114
+ const coreUrl = process.env.ADAS_CORE_URL || "http://adas-backend:4000";
4115
+ for (let attempt = 0; attempt < 4; attempt++) {
4116
+ await new Promise((r) => setTimeout(r, attempt === 0 ? 1500 : 2500));
4117
+ const res = await fetch(`${coreUrl}/api/ui-plugins`, {
4118
+ headers: { "x-api-key": apiKey, "X-ADAS-SERVICE": "ateam-mcp.create_plugin_verify" },
4119
+ signal: AbortSignal.timeout(15_000),
4120
+ }).catch(() => null);
4121
+ if (!res || !res.ok) continue;
4122
+ const data = await res.json().catch(() => ({}));
4123
+ const found = (data?.plugins || []).find((p) => p?.id === pluginId);
4124
+ if (found) {
4125
+ verified = _widgetHasRender(found.render)
4126
+ ? { renders: true, render_ok: true, note: "discovered by Core with a valid render block — it will render" }
4127
+ : { renders: false, render_ok: false, note: "discovered, but its manifest has no usable render block (need render.mode + iframeUrl/reactNative)" };
4128
+ break;
4129
+ }
4130
+ }
4131
+ if (!verified.renders && !("render_ok" in verified)) {
4132
+ verified.hint = "Not in Core's live catalog yet. If the connector is lazy (stopped until first call), its plugins only appear once declared in solution ui_plugins[] — declare it, or ensure the connector is ui_capable + connected. Re-check with ateam_get_widget_catalog.";
4133
+ }
4134
+ } catch { /* advisory — never fail the create on the verify probe */ }
4135
+
3915
4136
  return {
3916
4137
  ok: true,
3917
- plugin_id: `mcp:${connector_id}:${plugin_name}`,
4138
+ plugin_id: pluginId,
3918
4139
  kind: k,
3919
4140
  files_created: files.map(f => f.path),
3920
4141
  upload_result: result,
4142
+ verified,
3921
4143
  next_steps: [
3922
4144
  k === "rn" || k === "adaptive"
3923
4145
  ? `Edit plugins/${plugin_name}/index.tsx — fill in the Component body.`
@@ -3925,8 +4147,9 @@ const handlers = {
3925
4147
  k === "iframe" || k === "adaptive"
3926
4148
  ? `Edit ui-dist/${plugin_name}/index.html — replace the placeholder UI.`
3927
4149
  : null,
3928
- `A manifest.json (with the required render block) was written to ui-dist/${plugin_name}/manifest.json.`,
3929
- `⚠️ REQUIRED to render: the plugin must appear in this connector's ui.listPlugins / ui.getPlugin output WITH that render block. Dropping the files alone does NOT register it. If the connector has a HARDCODED plugin list (e.g. personal-assistant-ui-mcp: UI_PLUGINS[] + PLUGIN_MANIFESTS{} in server.js), add this plugin there — copy the render block from the manifest.json. Verify with ateam_get_solution(solution_id, "connectors_health") or ateam_get_widget_catalog after deploy.`,
4150
+ `A manifest.json (with the render block) was written to ui-dist/${plugin_name}/manifest.json — this is the source of truth Core reads.`,
4151
+ `If this connector was scaffolded by ateam_create_connector, its ui.listPlugins / ui.getPlugin read ui-dist/*/manifest.json automatically nothing else to register, it renders on the next deploy. ⚠️ ONLY a connector with a HARDCODED plugin list (legacy, e.g. personal-assistant-ui-mcp: UI_PLUGINS[] + PLUGIN_MANIFESTS{} in server.js) needs this plugin added there by hand — copy the render block from manifest.json.`,
4152
+ `Verify with ateam_get_widget_catalog (or ateam_get_solution(solution_id, "connectors_health")) after deploy.`,
3930
4153
  `Then declare it at solution level (ui_plugins[]) so a skill can open it via sys.focusUiPlugin — see ateam_get_spec(topic:"widgets").`,
3931
4154
  ].filter(Boolean),
3932
4155
  };
@@ -3992,7 +4215,7 @@ const handlers = {
3992
4215
  const deployedCount = result.deployed ?? (result.ok ? (skill_id ? 1 : (result.skills?.filter(s => s.ok !== false).length || 0)) : 0);
3993
4216
  const totalCount = result.total ?? (deployedCount + failedCount);
3994
4217
 
3995
- return {
4218
+ const out = {
3996
4219
  ok: result.ok,
3997
4220
  solution_id,
3998
4221
  ...(skill_id && { skill_id }),
@@ -4014,6 +4237,19 @@ const handlers = {
4014
4237
  ? `Re-deploy failed: ${result.error}${result.hint ? ` — ${result.hint}` : ''}`
4015
4238
  : `Re-deploy had ${failedCount} failure(s). Check skills array or call the underlying endpoint with verbose:true.`),
4016
4239
  };
4240
+ // If the deploy landed and the solution declares widgets, verify each one
4241
+ // actually renders (discovered by Core + has a render block). A silently
4242
+ // non-rendering widget is a common, hard-to-notice failure — surface it here.
4243
+ if (result.ok) {
4244
+ try {
4245
+ const wh = await verifyWidgetHealth(solution_id, sid);
4246
+ if (wh) {
4247
+ out.widget_health = wh;
4248
+ if (!wh.ok) out.message += ` ⚠️ ${wh.issues?.length || 0} widget issue(s) — see widget_health.`;
4249
+ }
4250
+ } catch { /* health check is advisory — never fail the deploy on it */ }
4251
+ }
4252
+ return out;
4017
4253
  },
4018
4254
 
4019
4255
  // ─── Master Key Bulk Tools ───────────────────────────────────────────