@ateam-ai/mcp 0.4.16 → 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 +305 -72
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ateam-ai/mcp",
3
- "version": "0.4.16",
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
@@ -1780,80 +1862,151 @@ function _scaffoldConnectorFiles({ connectorId, displayName, uiCapable }) {
1780
1862
  const safeName = displayName || connectorId;
1781
1863
  const files = [];
1782
1864
 
1783
- // 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.
1784
1873
  const serverJs = `#!/usr/bin/env node
1785
- // ${connectorId} — stdio MCP server. Generated by ateam_create_connector.
1874
+ // ${connectorId} — stdio JSON-RPC 2.0 MCP server. Generated by ateam_create_connector.
1786
1875
  //
1787
- // Fill in the tool implementations below. The MCP scaffolding
1788
- // (server setup, tool registration, error handling, stdio transport)
1789
- // 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
+ }
1790
1935
 
1791
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1792
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1936
+ // ── Request handler ──
1937
+ async function handle(req) {
1938
+ const { id, method, params } = req || {};
1793
1939
 
1794
- const server = new Server(
1795
- { name: "${connectorId}", version: "1.0.0" },
1796
- { capabilities: { tools: {}${uiCapable ? ', ui: {}' : ''} } },
1797
- );
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() });
1798
1944
 
1799
- // ── Tool definitions ────────────────────────────────────────────────
1800
- const TOOLS = [
1801
- {
1802
- name: "${connectorId}.echo",
1803
- description: "Echo back the input. Replace with your real tools.",
1804
- inputSchema: {
1805
- type: "object",
1806
- properties: { message: { type: "string" } },
1807
- required: ["message"],
1808
- },
1809
- },
1810
- ];
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
+ }
1811
1974
 
1812
- 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
+ }
1813
1978
 
1814
- server.setRequestHandler({ method: "tools/call" }, async (req) => {
1815
- const { name, arguments: args } = req.params;
1816
- switch (name) {
1817
- case "${connectorId}.echo":
1818
- return { content: [{ type: "text", text: \`Echo: \${args.message}\` }] };
1819
- default:
1820
- 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");
1821
1993
  }
1822
1994
  });
1823
- ${uiCapable ? `
1824
- // ── UI plugin handlers (ui_capable connector) ──────────────────────
1825
- server.setRequestHandler({ method: "ui.listPlugins" }, async () => ({
1826
- plugins: [
1827
- // List your plugins here. Plugin source files live in
1828
- // plugins/<plugin-name>/ (RN) and ui-dist/<plugin-name>/ (iframe).
1829
- // Plugin manifests are auto-discovered at deploy time per Phase 5
1830
- // of the strip — no need to repeat them here unless you want overrides.
1831
- ],
1832
- }));
1833
-
1834
- server.setRequestHandler({ method: "ui.getPlugin" }, async (req) => {
1835
- const { id } = req.params;
1836
- // Return plugin manifest by id. Auto-discovery covers the default case.
1837
- return { ok: false, error: \`Plugin \${id} not found\` };
1838
- });
1839
- ` : ''}
1840
- // ── Boot ────────────────────────────────────────────────────────────
1841
- const transport = new StdioServerTransport();
1842
- await server.connect(transport);
1843
- console.error("[${connectorId}] connected via stdio");
1995
+ process.stdin.on("end", () => process.exit(0));
1996
+
1997
+ console.error("[${connectorId}] Server started (stdio)");
1844
1998
  `;
1845
1999
  files.push({ path: "server.js", content: serverJs });
1846
2000
 
1847
- // package.json
2001
+ // package.json — raw stdio needs ZERO npm deps (node built-ins only), so
2002
+ // deploys skip npm install entirely.
1848
2003
  const pkg = {
1849
2004
  name: connectorId,
1850
2005
  version: "1.0.0",
1851
2006
  type: "module",
1852
2007
  description: `${safeName} — A-Team MCP connector`,
1853
2008
  main: "server.js",
1854
- dependencies: {
1855
- "@modelcontextprotocol/sdk": "^1.0.0",
1856
- },
2009
+ dependencies: {},
1857
2010
  };
1858
2011
  files.push({ path: "package.json", content: JSON.stringify(pkg, null, 2) + "\n" });
1859
2012
 
@@ -1865,21 +2018,21 @@ ${uiCapable ? "UI-capable: yes" : ""}
1865
2018
 
1866
2019
  ## Adding tools
1867
2020
 
1868
- Edit \`server.js\`. Add entries to the \`TOOLS\` array, then add a
1869
- 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)\`.
1870
2023
 
1871
2024
  ## Adding UI plugins (ui_capable connectors)
1872
2025
 
1873
- Drop iframe plugins under \`ui-dist/<plugin-name>/index.html\` and/or
1874
- React Native plugins under \`plugins/<plugin-name>/index.tsx\`.
1875
- Phase 5 of the strip auto-discovers them at deploy — no manifest
1876
- declaration needed unless you want overrides (drop a
1877
- \`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.
1878
2031
 
1879
2032
  ## Deploy
1880
2033
 
1881
- Use \`ateam_upload_connector\` to push the latest source to Core
1882
- without a full skill redeploy.
2034
+ Use \`ateam_upload_connector\` to push the latest source to Core without a full
2035
+ skill redeploy.
1883
2036
  `;
1884
2037
  files.push({ path: "README.md", content: readme });
1885
2038
 
@@ -2726,6 +2879,17 @@ const handlers = {
2726
2879
  phases.push({ phase: "agent_doc", status: "skipped", reason: err.message });
2727
2880
  }
2728
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
+
2729
2893
  return {
2730
2894
  ok: true,
2731
2895
  solution_id: solutionId,
@@ -2738,11 +2902,14 @@ const handlers = {
2738
2902
  ...(deploy.auto_expanded_skills?.length > 0 && { auto_expanded: deploy.auto_expanded_skills }),
2739
2903
  },
2740
2904
  health,
2905
+ ...(widget_health && { widget_health }),
2741
2906
  ...(test_result && { test_result }),
2742
2907
  ...(github_result && !github_result.error && !github_result.skipped && { github: github_result }),
2743
2908
  ...(agent_doc_result && !agent_doc_result.error && { agent_doc: agent_doc_result }),
2744
2909
  ...(validation.warnings?.length > 0 && { validation_warnings: validation.warnings }),
2745
- _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.',
2746
2913
  _next: 'Create a checkpoint before making more changes: ateam_github_promote(solution_id)',
2747
2914
  };
2748
2915
  },
@@ -3087,6 +3254,17 @@ const handlers = {
3087
3254
 
3088
3255
  const redeployOk = phases.some(p => p.phase === "redeploy" && p.status === "done");
3089
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
+
3090
3268
  return {
3091
3269
  ok: true,
3092
3270
  solution_id,
@@ -3096,9 +3274,12 @@ const handlers = {
3096
3274
  patched: patched,
3097
3275
  ...(isNewSkill && { created_skill: skill_id }),
3098
3276
  ...(redeployResult && { redeploy: redeployResult }),
3277
+ ...(widget_health && { widget_health }),
3099
3278
  ...(test_result && { test_result }),
3100
3279
  _status: redeployOk
3101
- ? `✅ 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.`)
3102
3283
  : `⚠️ Patched on ${store} ✅ but redeploy timed out. Run: ateam_redeploy(solution_id` + (skill_id ? `, skill_id: "${skill_id}"` : '') + ')',
3103
3284
  _next: isLocal
3104
3285
  ? 'Local edit saved + redeployed. When the tenant connects a GitHub repo, the local state is pushed → GitHub (which then becomes master).'
@@ -3871,9 +4052,14 @@ const handlers = {
3871
4052
  displayName: name || connector_id,
3872
4053
  uiCapable: !!ui_capable,
3873
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.
3874
4060
  const result = await post(
3875
4061
  `/deploy/solutions/${solution_id}/connectors/${connector_id}/upload`,
3876
- { files },
4062
+ { files, replace: true },
3877
4063
  sid,
3878
4064
  { timeoutMs: 120_000, retries: 1 },
3879
4065
  );
@@ -3915,12 +4101,45 @@ const handlers = {
3915
4101
  sid,
3916
4102
  { timeoutMs: 120_000, retries: 1 },
3917
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
+
3918
4136
  return {
3919
4137
  ok: true,
3920
- plugin_id: `mcp:${connector_id}:${plugin_name}`,
4138
+ plugin_id: pluginId,
3921
4139
  kind: k,
3922
4140
  files_created: files.map(f => f.path),
3923
4141
  upload_result: result,
4142
+ verified,
3924
4143
  next_steps: [
3925
4144
  k === "rn" || k === "adaptive"
3926
4145
  ? `Edit plugins/${plugin_name}/index.tsx — fill in the Component body.`
@@ -3928,8 +4147,9 @@ const handlers = {
3928
4147
  k === "iframe" || k === "adaptive"
3929
4148
  ? `Edit ui-dist/${plugin_name}/index.html — replace the placeholder UI.`
3930
4149
  : null,
3931
- `A manifest.json (with the required render block) was written to ui-dist/${plugin_name}/manifest.json.`,
3932
- `⚠️ 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.`,
3933
4153
  `Then declare it at solution level (ui_plugins[]) so a skill can open it via sys.focusUiPlugin — see ateam_get_spec(topic:"widgets").`,
3934
4154
  ].filter(Boolean),
3935
4155
  };
@@ -3995,7 +4215,7 @@ const handlers = {
3995
4215
  const deployedCount = result.deployed ?? (result.ok ? (skill_id ? 1 : (result.skills?.filter(s => s.ok !== false).length || 0)) : 0);
3996
4216
  const totalCount = result.total ?? (deployedCount + failedCount);
3997
4217
 
3998
- return {
4218
+ const out = {
3999
4219
  ok: result.ok,
4000
4220
  solution_id,
4001
4221
  ...(skill_id && { skill_id }),
@@ -4017,6 +4237,19 @@ const handlers = {
4017
4237
  ? `Re-deploy failed: ${result.error}${result.hint ? ` — ${result.hint}` : ''}`
4018
4238
  : `Re-deploy had ${failedCount} failure(s). Check skills array or call the underlying endpoint with verbose:true.`),
4019
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;
4020
4253
  },
4021
4254
 
4022
4255
  // ─── Master Key Bulk Tools ───────────────────────────────────────────