@ateam-ai/mcp 0.3.49 → 0.3.51

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 +158 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ateam-ai/mcp",
3
- "version": "0.3.49",
3
+ "version": "0.3.51",
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
@@ -271,6 +271,53 @@ export const tools = [
271
271
  required: ["solution_id", "skill_id", "message"],
272
272
  },
273
273
  },
274
+ {
275
+ name: "ateam_test_notification",
276
+ core: true,
277
+ description:
278
+ "Fire a REAL notification at an existing actor in a deployed solution — for end-to-end testing of the system-initiated notification path (telegram/push/app channels).\n\n" +
279
+ "Unlike ateam_test_skill (synthetic test actor with no channels) and ateam_conversation (user-initiated thread), this calls the /api/internal/notify-user path that PCM and other sibling services use — so the actor's real enabled channels actually receive the message.\n\n" +
280
+ "Use for:\n" +
281
+ " • Channel fan-out smoke (does telegram/push/app actually receive it?)\n" +
282
+ " • Delivery-result verification (per-channel ok/failed in the response).\n\n" +
283
+ "⚠️ SAFETY (v1):\n" +
284
+ " • The text is prefixed with [TEST] in the actual notification — visible to the user, anti-phishing.\n" +
285
+ " • Rate-limited: 10 calls/min per session.\n" +
286
+ " • Every call is audited (caller, tenant, actor, content hash) regardless of outcome.\n" +
287
+ " • actor_id is scoped to your tenant — cross-tenant targeting is rejected by Core.\n" +
288
+ " • reply_handler is INTENTIONALLY NOT SUPPORTED in v1. Routing the user's next reply to an arbitrary skill is a privilege-escalation surface (caller-supplied skill + context). v2 will add a tenant skill allowlist + context schema validation. Until then, use ateam_test_skill for routing/engagement tests.",
289
+ inputSchema: {
290
+ type: "object",
291
+ properties: {
292
+ solution_id: {
293
+ type: "string",
294
+ description: "The solution ID (required for tenant scoping + audit context).",
295
+ },
296
+ actor_id: {
297
+ type: "string",
298
+ description: "Target actor ID in your tenant (e.g. 'usr_arie_admin_0001'). Must exist; Core rejects if not found in your tenant.",
299
+ },
300
+ content: {
301
+ type: "string",
302
+ description: "Notification text. Will be sent to all of the actor's enabled channels, prefixed with [TEST] for the recipient.",
303
+ },
304
+ urgency: {
305
+ type: "string",
306
+ enum: ["low", "normal", "high"],
307
+ description: "Notification urgency. Default 'normal'.",
308
+ },
309
+ source: {
310
+ type: "string",
311
+ description: "Audit label for message.source. Default 'ateam-test'.",
312
+ },
313
+ metadata: {
314
+ type: "object",
315
+ description: "Optional metadata merged into message.metadata. Useful for correlation IDs.",
316
+ },
317
+ },
318
+ required: ["solution_id", "actor_id", "content"],
319
+ },
320
+ },
274
321
  {
275
322
  name: "ateam_conversation",
276
323
  core: true,
@@ -1439,6 +1486,7 @@ const TENANT_TOOLS = new Set([
1439
1486
  "ateam_get_execution_logs",
1440
1487
  "ateam_conversation",
1441
1488
  "ateam_test_skill",
1489
+ "ateam_test_notification",
1442
1490
  "ateam_test_pipeline",
1443
1491
  "ateam_test_voice",
1444
1492
  "ateam_test_status",
@@ -2751,6 +2799,116 @@ const handlers = {
2751
2799
  return post(`/deploy/solutions/${solution_id}/skills/${skill_id}/test`, body, sid, { timeoutMs });
2752
2800
  },
2753
2801
 
2802
+ ateam_test_notification: async ({ solution_id, actor_id, content, urgency, source, metadata, reply_handler, ...rest }, sid) => {
2803
+ if (!solution_id) throw new Error("solution_id required");
2804
+ if (!actor_id) throw new Error("actor_id required");
2805
+ if (!content || typeof content !== "string") throw new Error("content required (string)");
2806
+
2807
+ // v1: reply_handler is intentionally NOT supported (privilege-escalation
2808
+ // surface — caller could route user's next reply to any skill with
2809
+ // arbitrary context). Reject the field rather than silently dropping it,
2810
+ // so callers know to stop relying on it. v2 will add allowlist + schema.
2811
+ if (reply_handler !== undefined) {
2812
+ throw new Error("reply_handler is not supported in v1 of ateam_test_notification (security: caller-supplied skill + context = privilege escalation). v2 will add a tenant skill allowlist + context schema. For routing/engagement tests, use ateam_test_skill instead.");
2813
+ }
2814
+ // Defense-in-depth: also reject any unknown field that might smuggle a
2815
+ // reply_handler via case variants or aliases.
2816
+ for (const k of Object.keys(rest || {})) {
2817
+ if (/reply/i.test(k) || /handler/i.test(k)) {
2818
+ throw new Error(`Unsupported field "${k}" in ateam_test_notification (likely a reply_handler alias — see v1 safety note).`);
2819
+ }
2820
+ }
2821
+
2822
+ // Rate limit: 10 calls / minute / session. In-memory; bounded leak fine
2823
+ // for a test tool. Survives until process restart, which is acceptable
2824
+ // (the bound is per-session, not per-tenant).
2825
+ const RATE_LIMIT = 10;
2826
+ const RATE_WINDOW_MS = 60_000;
2827
+ if (!globalThis.__notifyRateLimit) globalThis.__notifyRateLimit = new Map();
2828
+ const bucket = globalThis.__notifyRateLimit;
2829
+ const now = Date.now();
2830
+ const entry = bucket.get(sid) || { times: [] };
2831
+ entry.times = entry.times.filter(t => now - t < RATE_WINDOW_MS);
2832
+ if (entry.times.length >= RATE_LIMIT) {
2833
+ const waitMs = RATE_WINDOW_MS - (now - entry.times[0]);
2834
+ throw new Error(`Rate limited: max ${RATE_LIMIT} ateam_test_notification calls per minute per session. Retry in ${Math.ceil(waitMs / 1000)}s.`);
2835
+ }
2836
+ entry.times.push(now);
2837
+ bucket.set(sid, entry);
2838
+
2839
+ // Get the authed tenant — used for X-ADAS-TENANT header (Core scopes
2840
+ // the actor lookup by tenant, so cross-tenant targeting is rejected at
2841
+ // Core regardless of what we pass).
2842
+ const creds = getCredentials(sid);
2843
+ const tenant = creds?.tenant;
2844
+ if (!tenant) throw new Error("No tenant in session — call ateam_auth first.");
2845
+
2846
+ const coreUrl = process.env.ADAS_CORE_URL || "http://adas-backend:4000";
2847
+ const coreSecret = process.env.CORE_MCP_SECRET;
2848
+ if (!coreSecret) {
2849
+ throw new Error("Server config error: CORE_MCP_SECRET not set. ateam_test_notification requires the platform shared secret (sibling-service auth). Contact platform admin.");
2850
+ }
2851
+
2852
+ // Force [TEST] prefix on the user-visible content. Anti-phishing rail:
2853
+ // even if a tenant admin api key were misused, the recipient sees
2854
+ // [TEST] on the actual message — they can't be fooled into thinking
2855
+ // it's a system-initiated production notification.
2856
+ const safeContent = content.startsWith("[TEST]") ? content : `[TEST] ${content}`;
2857
+
2858
+ // Audit log (cheap — console). Replace with structured audit when one exists.
2859
+ const contentHash = (await import("node:crypto")).createHash("sha256").update(content).digest("hex").slice(0, 12);
2860
+ console.log(JSON.stringify({
2861
+ audit: "ateam_test_notification",
2862
+ tenant,
2863
+ solution_id,
2864
+ actor_id,
2865
+ caller_session: sid?.slice(0, 8),
2866
+ content_preview: content.slice(0, 60),
2867
+ content_hash: contentHash,
2868
+ urgency: urgency || "normal",
2869
+ at: new Date().toISOString(),
2870
+ }));
2871
+
2872
+ const body = {
2873
+ actorId: actor_id,
2874
+ content: safeContent,
2875
+ urgency: urgency || "normal",
2876
+ metadata: { ...(metadata || {}), source: source || "ateam-test", _test: true },
2877
+ };
2878
+
2879
+ const res = await fetch(`${coreUrl}/api/internal/notify-user`, {
2880
+ method: "POST",
2881
+ headers: {
2882
+ "Content-Type": "application/json",
2883
+ "X-ADAS-TOKEN": coreSecret,
2884
+ "X-ADAS-TENANT": tenant,
2885
+ "X-ADAS-SERVICE": "ateam-mcp.test_notification",
2886
+ },
2887
+ body: JSON.stringify(body),
2888
+ signal: AbortSignal.timeout(15_000),
2889
+ });
2890
+
2891
+ const text = await res.text();
2892
+ let data;
2893
+ try { data = JSON.parse(text); } catch { data = { ok: false, error: text.slice(0, 400) }; }
2894
+
2895
+ if (!res.ok) {
2896
+ // Surface Core's actual reason — "actor not found in tenant" is the
2897
+ // most common (caller mistyped the actor_id), 502 = notif-router down.
2898
+ throw new Error(`Core /api/internal/notify-user returned ${res.status}: ${data.error || JSON.stringify(data).slice(0, 200)}`);
2899
+ }
2900
+
2901
+ return {
2902
+ ok: true,
2903
+ tenant,
2904
+ actor_id,
2905
+ dispatchId: data.dispatchId || null,
2906
+ notification_id: data.dispatchId || null, // alias matching the spec
2907
+ results: data.results || [],
2908
+ content_preview: safeContent.slice(0, 80),
2909
+ };
2910
+ },
2911
+
2754
2912
  ateam_test_pipeline: async ({ solution_id, skill_id, message }, sid) =>
2755
2913
  post(`/deploy/solutions/${solution_id}/skills/${skill_id}/test-pipeline`, { message }, sid, { timeoutMs: 30_000 }),
2756
2914