@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.
- package/package.json +1 -1
- package/src/tools.js +313 -77
package/package.json
CHANGED
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
|
-
"
|
|
775
|
-
"
|
|
776
|
-
"
|
|
777
|
-
"
|
|
778
|
-
"
|
|
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 —
|
|
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
|
|
1785
|
-
//
|
|
1786
|
-
|
|
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
|
-
|
|
1789
|
-
|
|
1936
|
+
// ── Request handler ──
|
|
1937
|
+
async function handle(req) {
|
|
1938
|
+
const { id, method, params } = req || {};
|
|
1790
1939
|
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
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
|
-
|
|
1797
|
-
const
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
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
|
-
|
|
1975
|
+
if (typeof method === "string" && method.startsWith("notifications/")) return null;
|
|
1976
|
+
return err(id, -32601, \`Unknown method: \${method}\`);
|
|
1977
|
+
}
|
|
1810
1978
|
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
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
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
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
|
|
1866
|
-
|
|
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
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
?
|
|
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:
|
|
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
|
|
3929
|
-
|
|
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
|
-
|
|
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 ───────────────────────────────────────────
|