diff --git a/mcp/connect-server.ts b/mcp/connect-server.ts index 3697e1b..c5f61be 100644 --- a/mcp/connect-server.ts +++ b/mcp/connect-server.ts @@ -506,6 +506,16 @@ server.tool('commcare_create_lookup_table', async (args) => runAtom(async () => (await commcareClient()).createLookupTable(args)) ); +// commcare_list_conditional_alerts — DEFERRED (atom code in place but +// AJAX endpoint doesn't behave as source suggests on live HQ as of +// 2026-05-22. corehq/messaging/scheduling/views.py:653 documents +// `?action=list_conditional_alerts` returning JSON, but live calls +// return the HTML page regardless of action/Accept/X-Requested-With. +// Possible causes: deployed HQ predates the handler; middleware +// strips the param; or JS uses a different endpoint that the source +// doesn't expose. Atom code is preserved in commcare.ts for future +// debugging; not registered to avoid shipping a broken atom. + server.tool('commcare_list_user_fields', 'Read the current custom-user-data field definition for a CommCare HQ domain. GET /a//users/user_data/ and parse the
initial_page_data div (HQ\'s standard Django→JS bootstrap). Returns the list of fields (slug, label, is_required, choices, regex) + the list of profiles. Requires can_edit_commcare_users permission; 302s to settings/users/ surface as a typed error.', { domain: z.string() }, diff --git a/mcp/connect/backends/commcare.ts b/mcp/connect/backends/commcare.ts index b429a95..0b3693b 100644 --- a/mcp/connect/backends/commcare.ts +++ b/mcp/connect/backends/commcare.ts @@ -230,6 +230,25 @@ export interface ListUserFieldsArgs { domain: string; } +export interface ConditionalAlert { + id: number; + name: string; + case_type: string; + /** Whether the rule's *schedule* is active (not the rule itself — see ConditionalAlertListView docstring). */ + active: boolean; + /** Whether the alert can be edited from the UI (false for SMS-survey-using alerts on subscriptions without inbound SMS). */ + editable: boolean; + locked_for_editing: boolean; + progress_pct: number; +} + +export interface ListConditionalAlertsArgs { + domain: string; + /** Server-side substring filter on rule name. */ + query?: string; + limit?: number; +} + export interface SetUserFieldsArgs { domain: string; /** @@ -1334,6 +1353,67 @@ export class CommCareBackend { }); } + /** + * List Conditional Alerts on a domain. GET + * /a//messaging/conditional/?action=list_conditional_alerts&page=N&limit=Y. + * + * The ConditionalAlertListView has a dedicated AJAX list endpoint + * (verified against /tmp/ace-refs/hq/corehq/messaging/scheduling/views.py + * :653-665 `get_conditional_alerts_ajax_response`) that returns JSON + * `{rules: [{id, name, case_type, active, editable, ...}], total: N}`. + * + * Gated by REMINDERS_FRAMEWORK (Standard+ subscription). + * + * NB: `active` here is the rule's schedule's active flag (not the + * rule.active flag). The list view's docstring explains: + * "Therefore rule processing occurs unconditionally every time a + * rule is saved." For verifier purposes treat any rule whose + * schedule is active as "live." + * + * The CREATE counterpart is deferred — see notes in + * docs/connect-interviews/v1-acceptance.md. + */ + async listConditionalAlerts(args: ListConditionalAlertsArgs): Promise<{ alerts: ConditionalAlert[]; total: number }> { + return this.runWithSessionRetry(async (request) => { + const params = new URLSearchParams({ + action: 'list_conditional_alerts', + page: '1', + limit: String(args.limit ?? 100), + }); + if (args.query) params.set('query', args.query); + const path = `/a/${encodeURIComponent(args.domain)}/messaging/conditional/?${params.toString()}`; + const res = await request.get(`${this.opts.baseUrl}${path}`, { + maxRedirects: 0, + headers: { + 'X-Requested-With': 'XMLHttpRequest', + Accept: 'application/json', + }, + }); + if (res.status() === 302) { + CommCareBackend.assertNotLoginRedirect(res, `commcare_list_conditional_alerts GET ${path}`); + } + if (res.status() === 404) { + throw new Error( + `commcare_list_conditional_alerts: 404 — domain ${args.domain} lacks REMINDERS_FRAMEWORK privilege (Standard+).`, + ); + } + if (res.status() !== 200) { + throw new Error(`commcare_list_conditional_alerts GET ${path} returned ${res.status()}: ${(await res.text()).slice(0, 300)}`); + } + const body = JSON.parse(await res.text()) as { rules?: any[]; total?: number }; + const alerts = (body.rules ?? []).map((r) => ({ + id: Number(r.id), + name: String(r.name ?? ''), + case_type: String(r.case_type ?? ''), + active: !!r.active, + editable: !!r.editable, + locked_for_editing: !!r.locked_for_editing, + progress_pct: Number(r.progress_pct ?? 0), + })); + return { alerts, total: body.total ?? alerts.length }; + }); + } + /** * Read the current custom-user-data field definition for a domain. * GETs /a//users/user_data/ and parses the