@ateam-ai/mcp 0.4.16 → 0.4.18
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/api.js +8 -3
- package/src/tools.js +308 -75
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -393,7 +393,7 @@ function headers(sessionId) {
|
|
|
393
393
|
/**
|
|
394
394
|
* Format an API error into a user-friendly message with actionable hints.
|
|
395
395
|
*/
|
|
396
|
-
function formatError(method, path, status, body) {
|
|
396
|
+
function formatError(method, path, status, body, baseUrl) {
|
|
397
397
|
const hints = {
|
|
398
398
|
400: "Bad request — see the error details above for what to fix.",
|
|
399
399
|
401: "Your API key may be invalid or expired. Get a valid key at https://mcp.ateam-ai.com/get-api-key then call ateam_auth(api_key: \"your_key\").",
|
|
@@ -434,7 +434,12 @@ function formatError(method, path, status, body) {
|
|
|
434
434
|
const hint = hints[status] || "";
|
|
435
435
|
const detail = typeof body === "string" && body.length > 0 && body.length < 2000 ? body : "";
|
|
436
436
|
|
|
437
|
-
|
|
437
|
+
// Always show the FULL URL actually hit — ateam-mcp is a PUBLIC MCP with a
|
|
438
|
+
// configurable base (prod default, dev/self-host overrides), so a bare
|
|
439
|
+
// "POST /deploy/..." 404 is ambiguous: is the route missing, or did the
|
|
440
|
+
// request go to the wrong base? The full URL disambiguates instantly.
|
|
441
|
+
const target = baseUrl ? `${baseUrl}${path}` : path;
|
|
442
|
+
let msg = `A-Team API error: ${method} ${target} returned ${status}`;
|
|
438
443
|
if (detail) msg += ` — ${detail}`;
|
|
439
444
|
if (hint) msg += `\nHint: ${hint}`;
|
|
440
445
|
|
|
@@ -486,7 +491,7 @@ async function request(method, path, body, sessionId, opts = {}) {
|
|
|
486
491
|
|
|
487
492
|
if (!res.ok) {
|
|
488
493
|
const text = await res.text().catch(() => "");
|
|
489
|
-
throw new Error(formatError(method, path, res.status, text));
|
|
494
|
+
throw new Error(formatError(method, path, res.status, text, baseUrl));
|
|
490
495
|
}
|
|
491
496
|
|
|
492
497
|
return res.json();
|
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 —
|
|
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
|
|
1788
|
-
//
|
|
1789
|
-
|
|
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
|
-
|
|
1792
|
-
|
|
1936
|
+
// ── Request handler ──
|
|
1937
|
+
async function handle(req) {
|
|
1938
|
+
const { id, method, params } = req || {};
|
|
1793
1939
|
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
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
|
-
|
|
1800
|
-
const
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
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
|
-
|
|
1975
|
+
if (typeof method === "string" && method.startsWith("notifications/")) return null;
|
|
1976
|
+
return err(id, -32601, \`Unknown method: \${method}\`);
|
|
1977
|
+
}
|
|
1813
1978
|
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
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
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
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
|
|
1869
|
-
|
|
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
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -2028,10 +2181,10 @@ export default {
|
|
|
2028
2181
|
// ui.listPlugins is generated from ui-dist/<plugin>/manifest.json pick it up
|
|
2029
2182
|
// automatically, and for connectors with a HARDCODED plugin list (e.g.
|
|
2030
2183
|
// personal-assistant-ui-mcp) copy this render block into their ui.getPlugin.
|
|
2031
|
-
const mode =
|
|
2184
|
+
const mode = kind === "iframe" ? "iframe" : kind === "rn" ? "react-native" : "adaptive";
|
|
2032
2185
|
const render = { mode };
|
|
2033
|
-
if (
|
|
2034
|
-
if (
|
|
2186
|
+
if (kind === "iframe" || kind === "adaptive") render.iframeUrl = `/ui/${pluginName}/index.html`;
|
|
2187
|
+
if (kind === "rn" || kind === "adaptive") render.reactNative = { component: pluginName };
|
|
2035
2188
|
const prettyName = pluginName.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2036
2189
|
files.push({
|
|
2037
2190
|
path: `ui-dist/${pluginName}/manifest.json`,
|
|
@@ -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:
|
|
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
|
-
?
|
|
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:
|
|
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
|
|
3932
|
-
|
|
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
|
-
|
|
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 ───────────────────────────────────────────
|