@ateam-ai/mcp 0.3.49 → 0.3.50

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 +159 -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.50",
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,62 @@ 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 + reply routing + engagement-flip).\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
+ " • Routing-hook tests (does user's next reply route to the right skill given reply_handler?)\n" +
282
+ " • Engagement-flip tests (does the receiver-skill's tool call flip engaged:true on the right notification?)\n" +
283
+ " • Channel fan-out smoke (does telegram/push/app actually receive it?)\n\n" +
284
+ "⚠️ SAFETY:\n" +
285
+ " • The text is prefixed with [TEST] in the actual notification — visible to the user, anti-phishing.\n" +
286
+ " • Rate-limited: 10 calls/min per session.\n" +
287
+ " • Every call is audited (caller, tenant, actor, content hash) regardless of outcome.\n" +
288
+ " • actor_id is scoped to your tenant — cross-tenant targeting is rejected by Core.\n" +
289
+ " • reply_handler is passed through unchanged (Core handles TTL). v2 will add a tenant allowlist + context schema validation; until then, only set reply_handler against skills you trust to receive arbitrary context.",
290
+ inputSchema: {
291
+ type: "object",
292
+ properties: {
293
+ solution_id: {
294
+ type: "string",
295
+ description: "The solution ID (required for tenant scoping + audit context).",
296
+ },
297
+ actor_id: {
298
+ type: "string",
299
+ description: "Target actor ID in your tenant (e.g. 'usr_arie_admin_0001'). Must exist; Core rejects if not found in your tenant.",
300
+ },
301
+ content: {
302
+ type: "string",
303
+ description: "Notification text. Will be sent to all of the actor's enabled channels, prefixed with [TEST] for the recipient.",
304
+ },
305
+ urgency: {
306
+ type: "string",
307
+ enum: ["low", "normal", "high"],
308
+ description: "Notification urgency. Default 'normal'.",
309
+ },
310
+ source: {
311
+ type: "string",
312
+ description: "Audit label for message.source. Default 'ateam-test'.",
313
+ },
314
+ metadata: {
315
+ type: "object",
316
+ description: "Optional metadata merged into message.metadata. Useful for correlation IDs.",
317
+ },
318
+ reply_handler: {
319
+ type: "object",
320
+ description: "OPTIONAL — install a routing hook so the user's next reply goes to a specific skill with injected context. Shape: { skill: 'skill_id', context: { ...arbitrary } }. Common contexts: dialogueId, observationId, proposalId, candidateSpecs, patternId, mode.\n\n⚠️ This semantically hijacks the user's next reply. Only use against skills designed to receive notification replies (e.g. 'ui-companion', 'pcm-companion'). v2 will enforce a tenant allowlist.",
321
+ properties: {
322
+ skill: { type: "string", description: "Skill ID to route the next reply to." },
323
+ context: { type: "object", description: "Object injected into the receiving job's triggerContext." },
324
+ },
325
+ },
326
+ },
327
+ required: ["solution_id", "actor_id", "content"],
328
+ },
329
+ },
274
330
  {
275
331
  name: "ateam_conversation",
276
332
  core: true,
@@ -1439,6 +1495,7 @@ const TENANT_TOOLS = new Set([
1439
1495
  "ateam_get_execution_logs",
1440
1496
  "ateam_conversation",
1441
1497
  "ateam_test_skill",
1498
+ "ateam_test_notification",
1442
1499
  "ateam_test_pipeline",
1443
1500
  "ateam_test_voice",
1444
1501
  "ateam_test_status",
@@ -2751,6 +2808,108 @@ const handlers = {
2751
2808
  return post(`/deploy/solutions/${solution_id}/skills/${skill_id}/test`, body, sid, { timeoutMs });
2752
2809
  },
2753
2810
 
2811
+ ateam_test_notification: async ({ solution_id, actor_id, content, urgency, source, metadata, reply_handler }, sid) => {
2812
+ if (!solution_id) throw new Error("solution_id required");
2813
+ if (!actor_id) throw new Error("actor_id required");
2814
+ if (!content || typeof content !== "string") throw new Error("content required (string)");
2815
+
2816
+ // Rate limit: 10 calls / minute / session. In-memory; bounded leak fine
2817
+ // for a test tool. Survives until process restart, which is acceptable
2818
+ // (the bound is per-session, not per-tenant).
2819
+ const RATE_LIMIT = 10;
2820
+ const RATE_WINDOW_MS = 60_000;
2821
+ if (!globalThis.__notifyRateLimit) globalThis.__notifyRateLimit = new Map();
2822
+ const bucket = globalThis.__notifyRateLimit;
2823
+ const now = Date.now();
2824
+ const entry = bucket.get(sid) || { times: [] };
2825
+ entry.times = entry.times.filter(t => now - t < RATE_WINDOW_MS);
2826
+ if (entry.times.length >= RATE_LIMIT) {
2827
+ const waitMs = RATE_WINDOW_MS - (now - entry.times[0]);
2828
+ throw new Error(`Rate limited: max ${RATE_LIMIT} ateam_test_notification calls per minute per session. Retry in ${Math.ceil(waitMs / 1000)}s.`);
2829
+ }
2830
+ entry.times.push(now);
2831
+ bucket.set(sid, entry);
2832
+
2833
+ // Get the authed tenant — used for X-ADAS-TENANT header (Core scopes
2834
+ // the actor lookup by tenant, so cross-tenant targeting is rejected at
2835
+ // Core regardless of what we pass).
2836
+ const creds = getCredentials(sid);
2837
+ const tenant = creds?.tenant;
2838
+ if (!tenant) throw new Error("No tenant in session — call ateam_auth first.");
2839
+
2840
+ const coreUrl = process.env.ADAS_CORE_URL || "http://adas-backend:4000";
2841
+ const coreSecret = process.env.CORE_MCP_SECRET;
2842
+ if (!coreSecret) {
2843
+ 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.");
2844
+ }
2845
+
2846
+ // Force [TEST] prefix on the user-visible content. Anti-phishing rail:
2847
+ // even if a tenant admin api key were misused, the recipient sees
2848
+ // [TEST] on the actual message — they can't be fooled into thinking
2849
+ // it's a system-initiated production notification.
2850
+ const safeContent = content.startsWith("[TEST]") ? content : `[TEST] ${content}`;
2851
+
2852
+ // Audit log (cheap — console). Replace with structured audit when one exists.
2853
+ const contentHash = (await import("node:crypto")).createHash("sha256").update(content).digest("hex").slice(0, 12);
2854
+ console.log(JSON.stringify({
2855
+ audit: "ateam_test_notification",
2856
+ tenant,
2857
+ solution_id,
2858
+ actor_id,
2859
+ caller_session: sid?.slice(0, 8),
2860
+ content_preview: content.slice(0, 60),
2861
+ content_hash: contentHash,
2862
+ reply_handler_skill: reply_handler?.skill || null,
2863
+ urgency: urgency || "normal",
2864
+ at: new Date().toISOString(),
2865
+ }));
2866
+
2867
+ if (reply_handler) {
2868
+ console.warn(`[ateam_test_notification] reply_handler set → next user reply will route to skill="${reply_handler.skill}" with caller-supplied context. v2 will enforce a tenant allowlist + context schema validation.`);
2869
+ }
2870
+
2871
+ const body = {
2872
+ actorId: actor_id,
2873
+ content: safeContent,
2874
+ urgency: urgency || "normal",
2875
+ metadata: { ...(metadata || {}), source: source || "ateam-test", _test: true },
2876
+ ...(reply_handler ? { reply_handler } : {}),
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
+ ...(reply_handler && { reply_handler_armed: { skill: reply_handler.skill } }),
2910
+ };
2911
+ },
2912
+
2754
2913
  ateam_test_pipeline: async ({ solution_id, skill_id, message }, sid) =>
2755
2914
  post(`/deploy/solutions/${solution_id}/skills/${skill_id}/test-pipeline`, { message }, sid, { timeoutMs: 30_000 }),
2756
2915