@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.
- package/package.json +1 -1
- package/src/tools.js +159 -0
package/package.json
CHANGED
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
|
|