search_donors
Search FEC filings for individual donors by name, employer, or occupation to track donor patterns and identify industry contributions.
Instructions
Search for individual donors across all FEC filings by name, employer, or occupation. Essential for tracking donor patterns, identifying industry contributions, or researching specific individuals' political giving. Supports searching by employer (e.g., "Goldman Sachs") or occupation (e.g., "Lobbyist", "Government Affairs").
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| contributor_name | No | Donor name to search for (partial match supported) | |
| contributor_employer | No | Employer name to search for (e.g., "Google", "Goldman Sachs") | |
| contributor_occupation | No | Occupation to search for (e.g., "Attorney", "Government Affairs", "Lobbyist") | |
| contributor_state | No | Two-letter state code to filter by (e.g., "CA", "NY") | |
| min_amount | No | Minimum contribution amount to include | |
| cycle | No | Two-year election cycle (e.g., 2024) | |
| limit | No | Maximum number of results to return (default: 20) |
Implementation Reference
- src/tools/search-donors.ts:22-117 (handler)The main handler function executeSearchDonors that calls the FEC client and formats results into markdown. Accepts optional search params: contributor_name, contributor_employer, contributor_occupation, contributor_state, min_amount, cycle, limit. Groups results by committee, shows per-committee totals and individual contributions.
export async function executeSearchDonors( client: FECClient, params: { contributor_name?: string; contributor_employer?: string; contributor_occupation?: string; contributor_state?: string; min_amount?: number; cycle?: number; limit?: number; } ): Promise<SearchDonorsResult> { try { const response = await client.searchDonors({ contributor_name: params.contributor_name, contributor_employer: params.contributor_employer, contributor_occupation: params.contributor_occupation, contributor_state: params.contributor_state, min_amount: params.min_amount ?? 200, // Default to itemized threshold two_year_transaction_period: params.cycle, limit: params.limit ?? 20, }, 60_000); // Build header const lines: string[] = ['## Donor Search Results']; // Show search criteria const criteria: string[] = []; if (params.contributor_name) criteria.push(`name: "${params.contributor_name}"`); if (params.contributor_employer) criteria.push(`employer: "${params.contributor_employer}"`); if (params.contributor_occupation) criteria.push(`occupation: "${params.contributor_occupation}"`); if (params.contributor_state) criteria.push(`state: ${params.contributor_state}`); if (params.min_amount) criteria.push(`minimum: ${formatCurrency(params.min_amount)}`); if (params.cycle) criteria.push(`cycle: ${params.cycle}`); lines.push(`*Search: ${criteria.join(', ')}*`); lines.push(`*Found ${response.pagination.count} contributions, showing ${response.results.length}*`); lines.push(''); if (response.results.length === 0) { lines.push('No contributions found matching the criteria.'); return { content: [{ type: 'text', text: lines.join('\n') }], }; } // Calculate totals const totalAmount = response.results.reduce((sum, r) => sum + r.contribution_receipt_amount, 0); lines.push(`**Total (shown):** ${formatCurrency(totalAmount)}`); lines.push(''); // Group by recipient committee for better readability const byCommittee = new Map<string, typeof response.results>(); for (const result of response.results) { const key = result.committee_name || result.committee_id; if (!byCommittee.has(key)) { byCommittee.set(key, []); } byCommittee.get(key)!.push(result); } // Format results let index = 1; for (const [committeeName, contributions] of byCommittee) { const committeeTotal = contributions.reduce((sum, c) => sum + c.contribution_receipt_amount, 0); lines.push(`### ${committeeName} (${formatCurrency(committeeTotal)})`); for (const contrib of contributions) { const location = [contrib.contributor_city, contrib.contributor_state].filter(Boolean).join(', '); lines.push(`${index}. **${contrib.contributor_name}** - ${formatCurrency(contrib.contribution_receipt_amount)}`); lines.push(` - Date: ${formatDate(contrib.contribution_receipt_date)}`); if (contrib.contributor_employer) { lines.push(` - Employer: ${contrib.contributor_employer}`); } if (contrib.contributor_occupation) { lines.push(` - Occupation: ${contrib.contributor_occupation}`); } if (location) { lines.push(` - Location: ${location}`); } lines.push(''); index++; } } return { content: [{ type: 'text', text: lines.join('\n') }], }; } catch (error) { return { content: [{ type: 'text', text: formatErrorForToolResponse(error) }], isError: true, }; } } - Zod input schema (searchDonorsInputSchema) defining all parameters with descriptions, and searchDonorsParamsSchema with a superRefine ensuring at least one search criterion is provided (contributor_name, contributor_employer, or contributor_occupation).
export const searchDonorsInputSchema = { contributor_name: z .string() .optional() .describe('Donor name to search for (partial match supported)'), contributor_employer: z .string() .optional() .describe('Employer name to search for (e.g., "Google", "Goldman Sachs")'), contributor_occupation: z .string() .optional() .describe('Occupation to search for (e.g., "Attorney", "Government Affairs", "Lobbyist")'), contributor_state: z .string() .length(2) .optional() .describe('Two-letter state code to filter by (e.g., "CA", "NY")'), min_amount: z .number() .min(0) .optional() .describe('Minimum contribution amount to include'), cycle: z .number() .int() .min(2000) .max(2030) .optional() .describe('Two-year election cycle (e.g., 2024)'), limit: z .number() .int() .min(1) .max(100) .optional() .describe('Maximum number of results to return (default: 20)'), }; export const searchDonorsParamsSchema = z .object(searchDonorsInputSchema) .superRefine((value, ctx) => { if ( !value.contributor_name && !value.contributor_employer && !value.contributor_occupation ) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Please provide at least one search criterion: contributor_name, contributor_employer, or contributor_occupation.', }); } }); export type SearchDonorsInput = z.infer<typeof searchDonorsParamsSchema>; - src/tools/index.ts:183-231 (registration)Registration of the search_donors tool in the registerTools function. Maps SEARCH_DONORS_TOOL definition + searchDonorsParamsSchema + executeSearchDonors into an MCP server.tool() call (lines 213-229 for the generic registration loop).
{ def: SEARCH_DONORS_TOOL, paramsSchema: searchDonorsParamsSchema, execute: async (params) => executeSearchDonors(client, params as { contributor_name?: string; contributor_employer?: string; contributor_occupation?: string; contributor_state?: string; min_amount?: number; cycle?: number; limit?: number; }), }, { def: SEARCH_SPENDING_TOOL, paramsSchema: searchSpendingParamsSchema, execute: async (params) => executeSearchSpending(client, params as { description?: string; recipient_name?: string; recipient_state?: string; min_amount?: number; cycle?: number; limit?: number; }), }, ]; for (const { def, paramsSchema, execute } of toolRegistrations) { server.tool( def.name, def.description, def.inputSchema, async (params): Promise<ToolResult> => { try { const validatedParams = await paramsSchema.parseAsync(params); const result = await execute(validatedParams); return { ...result } as ToolResult; } catch (error) { return { content: [{ type: 'text', text: formatErrorForToolResponse(error) }], isError: true, }; } } ); } } - src/tools/search-donors.ts:11-15 (registration)Tool definition object SEARCH_DONORS_TOOL with name 'search_donors', description, and inputSchema reference.
export const SEARCH_DONORS_TOOL = { name: 'search_donors', description: `Search for individual donors across all FEC filings by name, employer, or occupation. Essential for tracking donor patterns, identifying industry contributions, or researching specific individuals' political giving. Supports searching by employer (e.g., "Goldman Sachs") or occupation (e.g., "Lobbyist", "Government Affairs").`, inputSchema: searchDonorsInputSchema, }; - src/api/client.ts:363-375 (helper)FECClient.searchDonors() method - the API client helper that calls the FEC Schedule A endpoint with contributor filters, sorting by amount descending, and limiting to individual donors (is_individual=true).
async searchDonors(params: SearchDonorsParams, timeout?: number): Promise<FECApiResponse<FECScheduleA>> { return this.get<FECScheduleA>(ENDPOINTS.SCHEDULE_A, { contributor_name: params.contributor_name, contributor_employer: params.contributor_employer, contributor_occupation: params.contributor_occupation, contributor_state: params.contributor_state, min_amount: params.min_amount, two_year_transaction_period: params.two_year_transaction_period, is_individual: true, // Only search individual donors sort: '-contribution_receipt_amount', per_page: params.limit || DEFAULT_PER_PAGE, }, timeout); }