Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/lucky-eels-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
2 changes: 1 addition & 1 deletion .changeset/slimy-paths-beam.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
---
---

Add list_paying_members admin tool and infer membership tier from subscription amount in get_account.
Fix list_paying_members returning incomplete results: raise default limit from 50 to 200 (max 500), include members with past_due/unpaid subscriptions flagged in output, add truncation warning when results are capped.
46 changes: 35 additions & 11 deletions server/src/addie/mcp/admin-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1083,17 +1083,21 @@ Roles: member (default), admin (can manage team), owner (full control)`,
{
name: 'list_paying_members',
description: 'List all paying members grouped by subscription level ($50K ICL, $10K corporate, $2.5K SMB, individual). Includes individual members by default. Pass include_individual: false for corporate-only. Each entry includes the primary contact name and email.',
usage_hints: 'Use when asked about paying members, subscription breakdown, who pays what, membership revenue by tier, listing members for events/outreach, or getting member contact lists.',
usage_hints: 'Use when asked about paying members, subscription breakdown, who pays what, membership revenue by tier, listing members for events/outreach, getting member contact lists, or checking for payment issues.',
input_schema: {
type: 'object' as const,
properties: {
include_individual: {
type: 'boolean',
description: 'Include individual (personal) memberships (default: true)',
},
include_payment_issues: {
type: 'boolean',
description: 'Also include members with past_due or unpaid subscriptions, flagged in output (default: false)',
},
limit: {
type: 'number',
description: 'Maximum results (default: 50)',
description: 'Maximum results (default: 50, max: 200)',
},
},
},
Expand Down Expand Up @@ -6610,7 +6614,11 @@ Use add_committee_leader to assign a leader.`;
try {
const pool = getPool();
const includeIndividual = input.include_individual !== false;
const limit = Math.min(Math.max((input.limit as number) || 50, 1), 100);
const includePaymentIssues = input.include_payment_issues === true;
const limit = Math.min(Math.max((input.limit as number) || 50, 1), 200);
const allowedStatuses = includePaymentIssues
? ['active', 'past_due', 'unpaid']
: ['active'];

const result = await pool.query(
`SELECT
Expand All @@ -6635,16 +6643,16 @@ Use add_committee_leader to assign a leader.`;
ORDER BY om.created_at ASC
LIMIT 1
) primary_contact ON true
WHERE o.subscription_status = 'active'
WHERE o.subscription_status = ANY($3::text[])
AND o.subscription_canceled_at IS NULL
AND ($1 = true OR o.is_personal = false)
ORDER BY o.subscription_amount DESC NULLS LAST, o.name ASC
LIMIT $2`,
[includeIndividual, limit]
[includeIndividual, limit, allowedStatuses]
);

if (result.rows.length === 0) {
return `No active members found${includeIndividual ? '' : ' (corporate only)'}.`;
return `No paying members found${includeIndividual ? '' : ' (corporate only)'}.`;
}

// Group by annual subscription amount level.
Expand Down Expand Up @@ -6674,7 +6682,7 @@ Use add_committee_leader to assign a leader.`;
}
}

const formatRow = (org: { name: string; subscription_amount: number | null; subscription_currency: string | null; subscription_interval: string | null; created_at: Date; contact_email: string | null; contact_first_name: string | null; contact_last_name: string | null }) => {
const formatRow = (org: { name: string; subscription_status: string; subscription_amount: number | null; subscription_currency: string | null; subscription_interval: string | null; created_at: Date; contact_email: string | null; contact_first_name: string | null; contact_last_name: string | null }) => {
const amount = org.subscription_amount
? formatCurrency(org.subscription_amount, org.subscription_currency || 'usd')
: 'Comped';
Expand All @@ -6688,13 +6696,29 @@ Use add_committee_leader to assign a leader.`;
: org.contact_email
? ` — ${org.contact_email}`
: '';
return `- **${org.name}**${contact} — ${amount}${interval} (since ${since})\n`;
const statusFlag = org.subscription_status === 'past_due' ? ' [PAST DUE]'
: org.subscription_status === 'unpaid' ? ' [UNPAID]'
: '';
return `- **${org.name}**${statusFlag}${contact} — ${amount}${interval} (since ${since})\n`;
};

let response = `## Active Members\n\n`;
response += `**${result.rows.length} active member${result.rows.length !== 1 ? 's' : ''}**`;
const pastDueCount = result.rows.filter((r: { subscription_status: string }) => r.subscription_status === 'past_due').length;
const unpaidCount = result.rows.filter((r: { subscription_status: string }) => r.subscription_status === 'unpaid').length;
const activeCount = result.rows.length - pastDueCount - unpaidCount;

let response = includePaymentIssues
? `## Members\n\n`
: `## Active Members\n\n`;
response += `**${result.rows.length} member${result.rows.length !== 1 ? 's' : ''}**`;
if (!includeIndividual) response += ` (corporate only)`;
response += `\n\n`;
if (includePaymentIssues) {
response += ` — ${activeCount} active, ${pastDueCount} past due, ${unpaidCount} unpaid`;
}
response += `\n`;
if (result.rows.length >= limit) {
response += `> Results truncated at ${limit}. Increase limit for full list.\n`;
}
response += `\n`;

if (groups.icl.length > 0) {
response += `### Industry Council Leaders ($50K/yr)\n`;
Expand Down
Loading