@ateam-ai/mcp 0.4.72 → 0.4.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/tools.js +93 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ateam-ai/mcp",
3
- "version": "0.4.72",
3
+ "version": "0.4.74",
4
4
  "mcpName": "io.github.ariekogan/ateam-mcp",
5
5
  "description": "A-Team MCP Server — build, validate, and deploy multi-agent solutions from any AI environment",
6
6
  "type": "module",
@@ -13,7 +13,7 @@
13
13
  "start:http": "node src/index.js --http",
14
14
  "dev": "node --watch src/index.js",
15
15
  "dev:http": "node --watch src/index.js --http",
16
- "test": "node test/session-isolation.test.mjs"
16
+ "test": "node test/session-isolation.test.mjs && node test/widget-protocol.test.mjs"
17
17
  },
18
18
  "keywords": [
19
19
  "mcp",
package/src/tools.js CHANGED
@@ -223,6 +223,69 @@ function _widgetHasRender(r) {
223
223
  return true; // unknown mode — don't false-positive on a custom render
224
224
  }
225
225
 
226
+
227
+ // ── WIDGET POSTMESSAGE PROTOCOL: the failure that RENDERS FINE ────────────────
228
+ //
229
+ // A widget using the wrong postMessage shape draws its chrome, calls the host,
230
+ // and hangs until its own timeout. Nothing errors: the connector is healthy,
231
+ // tools/list is correct, the plugin "renders", and only the DATA is missing.
232
+ // A clinic dashboard shipped that way and was reported as working (2026-08-29).
233
+ //
234
+ // The host matches action === "mcp-call" with payload.requestId. Three shapes
235
+ // are fatal on their own, and a widget can get two right and still hang:
236
+ // type:"tool.call" — never existed in the host, at any version
237
+ // correlationId — the host echoes requestId; the pending map misses
238
+ // type:"mcp-call" on SEND — send is matched on message.action, not .type
239
+ //
240
+ // Detected here rather than left to a person noticing an empty panel, and
241
+ // reported with the repair attached — a signal the reasoning engine can act on.
242
+ export function _widgetProtocolProblems(html) {
243
+ if (typeof html !== "string" || !html.includes("postMessage")) return [];
244
+
245
+ // STRUCTURE, NOT TOKENS. The first version matched /correlationId/ anywhere in
246
+ // the file, so a correct widget with an unrelated local named correlationId was
247
+ // reported broken — and `fix_with` would then steer an agent into editing
248
+ // working code. Reviewer was right to reject it. Each check below anchors to
249
+ // the actual send or receive shape.
250
+ const problems = [];
251
+
252
+ // Only look at what is POSTED to the host: postMessage(...) argument objects.
253
+ const sends = [...html.matchAll(/postMessage\s*\(\s*\{[\s\S]{0,400}/g)].map((m) => m[0]);
254
+ const sendBlob = sends.join("\n");
255
+ const isPluginSend = /source\s*:\s*["']adas-plugin["']/.test(sendBlob);
256
+
257
+ if (isPluginSend) {
258
+ if (/type\s*:\s*["']tool\.call["']/.test(sendBlob)) {
259
+ problems.push('sends message.type:"tool.call" — the host has NEVER accepted it, at any version (silent timeout, no error). Send message:{action:"mcp-call",payload:{requestId,connectorId,tool,args}}.');
260
+ } else if (!/action\s*:\s*["']mcp-call["']/.test(sendBlob)) {
261
+ // type:"mcp-call" is the near-miss worth naming separately: right name,
262
+ // wrong key. The host matches on message.action and ignores message.type.
263
+ if (/type\s*:\s*["']mcp-call["']/.test(sendBlob)) {
264
+ problems.push('sends message.TYPE:"mcp-call" — the host matches on message.ACTION for sends, so this is ignored. Use message:{action:"mcp-call",payload:{...}}.');
265
+ } else {
266
+ problems.push('never sends action:"mcp-call" — the host ignores the message entirely.');
267
+ }
268
+ }
269
+ // correlationId only counts as a defect where it carries the request id.
270
+ if (/payload\s*:\s*\{[^}]*correlationId/.test(sendBlob) || /correlationId\s*:\s*correlationId/.test(sendBlob)) {
271
+ problems.push('sends the request id as correlationId — the host echoes payload.requestId, so responses never match the pending request and every call times out even when the host answered.');
272
+ }
273
+ }
274
+
275
+ // RECEIVE side: only flag reading correlationId OFF THE HOST PAYLOAD.
276
+ if (/payload\s*(\?\.|\.)\s*correlationId/.test(html) ||
277
+ /payload\s*&&\s*[\w$.]*payload\.correlationId/.test(html) ||
278
+ /\{\s*correlationId[^}]*\}\s*=\s*[\w$.]*payload/.test(html)) {
279
+ problems.push('reads payload.correlationId from the host response — the host sends payload.requestId, so the pending request is never matched.');
280
+ }
281
+ if (/type\s*===?\s*["']tool\.response["']/.test(html)) {
282
+ problems.push('listens for message.type:"tool.response" — the host replies with "mcp-result".');
283
+ }
284
+
285
+ return problems;
286
+ }
287
+
288
+
226
289
  async function verifyWidgetHealth(solution_id, sid) {
227
290
  // 1. Declared plugins — solution.ui_plugins[]
228
291
  let declared = [];
@@ -290,6 +353,36 @@ async function verifyWidgetHealth(solution_id, sid) {
290
353
  return { id: id || "(missing)", discovered: !!found, render_ok, problems };
291
354
  });
292
355
 
356
+ // 4. PROTOCOL CHECK on the served HTML. Steps 1-3 prove a widget is declared,
357
+ // discovered and renderable — none of which catches a widget that renders
358
+ // and then silently times out on every call.
359
+ // Read the SOURCE we serve rather than fetching the rendered iframe: the
360
+ // source is what a fix would edit, and it needs no browser or signed URL.
361
+ // Plugin ids are `mcp:<connector>:<name>`, so one source read per connector
362
+ // covers all of its widgets.
363
+ const connectorsSeen = new Map(); // connectorId -> [{path, content}]
364
+ for (const p of plugins) {
365
+ const connectorId = String(p.id || "").startsWith("mcp:") ? String(p.id).split(":")[1] : null;
366
+ if (!connectorId) continue;
367
+ if (!connectorsSeen.has(connectorId)) {
368
+ try {
369
+ const src = await get(`/deploy/solutions/${solution_id}/connectors/${connectorId}/source`, sid);
370
+ connectorsSeen.set(connectorId, Array.isArray(src?.files) ? src.files : []);
371
+ } catch {
372
+ connectorsSeen.set(connectorId, []); // unreadable source is not a verdict
373
+ }
374
+ }
375
+ const html = (connectorsSeen.get(connectorId) || [])
376
+ .filter((f) => /ui-dist\/.*\.html$/.test(f?.path || ""))
377
+ .map((f) => f?.content || "")
378
+ .join("\n");
379
+ const protoProblems = _widgetProtocolProblems(html);
380
+ if (protoProblems.length) {
381
+ p.problems.push(...protoProblems);
382
+ p.fix_with = `Fix the widget's postMessage code and redeploy: ateam_github_patch(solution_id, "connectors/${connectorId}/ui-dist/<widget>/index.html", ...) then ateam_upload_connector(solution_id, "${connectorId}", github:true). The exact working shape is in ateam_get_examples(type:"ui-plugin-iframe").`;
383
+ }
384
+ }
385
+
293
386
  const unhealthy = plugins.filter((p) => p.problems.length);
294
387
  return {
295
388
  ok: unhealthy.length === 0,