From 3f45b92c8268d3c921bb8303482f0adabd5301e1 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 1 Apr 2026 22:12:33 +0900 Subject: [PATCH 1/4] fix: list_paying_members returns all active members The tool's default limit of 50 (max 100) was silently truncating results, causing Addie to report 58 members when there are ~132 active. Raise default to 200 (max 500) and add a truncation warning so the agent knows when results are incomplete. Co-Authored-By: Claude Opus 4.6 (1M context) --- .changeset/slimy-paths-beam.md | 2 +- server/src/addie/mcp/admin-tools.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.changeset/slimy-paths-beam.md b/.changeset/slimy-paths-beam.md index 46eca22c64..60359e4c70 100644 --- a/.changeset/slimy-paths-beam.md +++ b/.changeset/slimy-paths-beam.md @@ -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), add truncation warning when results are capped. diff --git a/server/src/addie/mcp/admin-tools.ts b/server/src/addie/mcp/admin-tools.ts index e3f664e00b..01bf5f54ac 100644 --- a/server/src/addie/mcp/admin-tools.ts +++ b/server/src/addie/mcp/admin-tools.ts @@ -1093,7 +1093,7 @@ Roles: member (default), admin (can manage team), owner (full control)`, }, limit: { type: 'number', - description: 'Maximum results (default: 50)', + description: 'Maximum results (default: 200, max: 500)', }, }, }, @@ -6610,7 +6610,7 @@ 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 limit = Math.min(Math.max((input.limit as number) || 200, 1), 500); const result = await pool.query( `SELECT @@ -6694,6 +6694,7 @@ Use add_committee_leader to assign a leader.`; let response = `## Active Members\n\n`; response += `**${result.rows.length} active member${result.rows.length !== 1 ? 's' : ''}**`; if (!includeIndividual) response += ` (corporate only)`; + if (result.rows.length >= limit) response += ` (results truncated at ${limit} — increase limit for full list)`; response += `\n\n`; if (groups.icl.length > 0) { From cda56dc59f0b9033f3b0f24793410b7e8f5fea6b Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 1 Apr 2026 22:41:02 +0900 Subject: [PATCH 2/4] fix: list_paying_members shows payment issues and raises limit - Raise default limit from 50 to 200 (max 500) so all ~132 members are returned - Add include_payment_issues param (default: true) to include past_due/unpaid members with [PAST DUE] / [UNPAID] flags - Show status breakdown in summary (e.g. "134 active, 2 past due, 1 unpaid") - Add truncation warning when results hit the limit Co-Authored-By: Claude Opus 4.6 (1M context) --- .changeset/slimy-paths-beam.md | 2 +- server/src/addie/mcp/admin-tools.ts | 35 ++++++++++++++++++++++------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/.changeset/slimy-paths-beam.md b/.changeset/slimy-paths-beam.md index 60359e4c70..baccad5425 100644 --- a/.changeset/slimy-paths-beam.md +++ b/.changeset/slimy-paths-beam.md @@ -1,4 +1,4 @@ --- --- -Fix list_paying_members returning incomplete results: raise default limit from 50 to 200 (max 500), add truncation warning when results are capped. +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. diff --git a/server/src/addie/mcp/admin-tools.ts b/server/src/addie/mcp/admin-tools.ts index 01bf5f54ac..2266284cb4 100644 --- a/server/src/addie/mcp/admin-tools.ts +++ b/server/src/addie/mcp/admin-tools.ts @@ -1082,8 +1082,8 @@ 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.', + description: 'List all paying members grouped by subscription level ($50K ICL, $10K corporate, $2.5K SMB, individual). Members with past_due or unpaid subscriptions are flagged. 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, getting member contact lists, or checking for payment issues.', input_schema: { type: 'object' as const, properties: { @@ -1091,6 +1091,10 @@ Roles: member (default), admin (can manage team), owner (full control)`, 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: true)', + }, limit: { type: 'number', description: 'Maximum results (default: 200, max: 500)', @@ -6610,8 +6614,13 @@ Use add_committee_leader to assign a leader.`; try { const pool = getPool(); const includeIndividual = input.include_individual !== false; + const includePaymentIssues = input.include_payment_issues !== false; const limit = Math.min(Math.max((input.limit as number) || 200, 1), 500); + const statusFilter = includePaymentIssues + ? `o.subscription_status IN ('active', 'past_due', 'unpaid')` + : `o.subscription_status = 'active'`; + const result = await pool.query( `SELECT o.name, @@ -6635,7 +6644,7 @@ 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 ${statusFilter} 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 @@ -6644,7 +6653,7 @@ Use add_committee_leader to assign a leader.`; ); 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. @@ -6674,7 +6683,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'; @@ -6688,12 +6697,22 @@ 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 = `## Members\n\n`; + response += `**${result.rows.length} member${result.rows.length !== 1 ? 's' : ''}**`; if (!includeIndividual) response += ` (corporate only)`; + if (includePaymentIssues) { + response += ` (${activeCount} active, ${pastDueCount} past due, ${unpaidCount} unpaid)`; + } if (result.rows.length >= limit) response += ` (results truncated at ${limit} — increase limit for full list)`; response += `\n\n`; From 986ee00917d56625e7e26ed137535808772e71fe Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 1 Apr 2026 22:47:27 +0900 Subject: [PATCH 3/4] fix: address code and security review feedback - Replace SQL string interpolation with parameterized ANY($3::text[]) - Default include_payment_issues to false (opt-in, not opt-out) - Lower default limit to 50 (max 200) to reduce context window usage - Restore "Active Members" header when payment issues excluded - Move truncation warning to its own blockquote line Co-Authored-By: Claude Opus 4.6 (1M context) --- server/src/addie/mcp/admin-tools.ts | 34 ++++++++++++++++------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/server/src/addie/mcp/admin-tools.ts b/server/src/addie/mcp/admin-tools.ts index 2266284cb4..f07603a8db 100644 --- a/server/src/addie/mcp/admin-tools.ts +++ b/server/src/addie/mcp/admin-tools.ts @@ -1082,7 +1082,7 @@ 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). Members with past_due or unpaid subscriptions are flagged. Includes individual members by default. Pass include_individual: false for corporate-only. Each entry includes the primary contact name and email.', + 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, getting member contact lists, or checking for payment issues.', input_schema: { type: 'object' as const, @@ -1093,11 +1093,11 @@ Roles: member (default), admin (can manage team), owner (full control)`, }, include_payment_issues: { type: 'boolean', - description: 'Also include members with past_due or unpaid subscriptions, flagged in output (default: true)', + description: 'Also include members with past_due or unpaid subscriptions, flagged in output (default: false)', }, limit: { type: 'number', - description: 'Maximum results (default: 200, max: 500)', + description: 'Maximum results (default: 50, max: 200)', }, }, }, @@ -6614,12 +6614,11 @@ Use add_committee_leader to assign a leader.`; try { const pool = getPool(); const includeIndividual = input.include_individual !== false; - const includePaymentIssues = input.include_payment_issues !== false; - const limit = Math.min(Math.max((input.limit as number) || 200, 1), 500); - - const statusFilter = includePaymentIssues - ? `o.subscription_status IN ('active', 'past_due', 'unpaid')` - : `o.subscription_status = 'active'`; + 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 @@ -6644,12 +6643,12 @@ Use add_committee_leader to assign a leader.`; ORDER BY om.created_at ASC LIMIT 1 ) primary_contact ON true - WHERE ${statusFilter} + 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) { @@ -6707,14 +6706,19 @@ Use add_committee_leader to assign a leader.`; const unpaidCount = result.rows.filter((r: { subscription_status: string }) => r.subscription_status === 'unpaid').length; const activeCount = result.rows.length - pastDueCount - unpaidCount; - let response = `## Members\n\n`; + 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)`; if (includePaymentIssues) { - response += ` (${activeCount} active, ${pastDueCount} past due, ${unpaidCount} unpaid)`; + response += ` — ${activeCount} active, ${pastDueCount} past due, ${unpaidCount} unpaid`; } - if (result.rows.length >= limit) response += ` (results truncated at ${limit} — increase limit for full list)`; - response += `\n\n`; + 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`; From 2ed9056a1871e7087833d05270aeca55c30c8bb4 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 1 Apr 2026 22:53:08 +0900 Subject: [PATCH 4/4] chore: add empty changeset for CI Co-Authored-By: Claude Opus 4.6 (1M context) --- .changeset/lucky-eels-thank.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changeset/lucky-eels-thank.md diff --git a/.changeset/lucky-eels-thank.md b/.changeset/lucky-eels-thank.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/lucky-eels-thank.md @@ -0,0 +1,2 @@ +--- +---