Skip to main content
Glama
sh-patterson

fec-mcp-server

get_receipts

Retrieve itemized contributions received by a campaign committee. Filter by amount, contributor type, and sort by amount or date to analyze donation patterns.

Instructions

Retrieve itemized contributions (Schedule A) received by a campaign committee. Shows individual and organizational donors, amounts, and contributor details. Automatically classifies PAC contributions by type (Corporate, Labor, Trade, Leadership PAC) for deeper analysis. Supports filtering by amount threshold for researching significant contributions and campaign finance patterns.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
committee_idYesFEC committee ID (e.g., "C00401224")
min_amountNoMinimum contribution amount to filter (default: $1,000)
two_year_transaction_periodNoTwo-year period (e.g., 2024 covers 2023-2024).
cycleNoAlias for two_year_transaction_period to align with finance cycle filters.
contributor_typeNoFilter by contributor type: "individual" or "committee" (PAC)
include_notableNoInclude flagged-first notable analysis block in output (default: true)
fuzzy_thresholdNoFuzzy match confidence threshold for reference list matching (default: 90)
limitNoNumber of results to return (default: 20, max: 100)
sort_byNoSort results by "amount" (descending) or "date" (most recent first)amount

Implementation Reference

  • The main handler function `executeGetReceipts` that retrieves itemized contributions (Schedule A) with PAC classification. It fetches receipts from the FEC API, classifies PAC committees, enriches data, optionally runs notable receipt analysis, and formats the output.
    export async function executeGetReceipts(
      client: FECClient,
      params: {
        committee_id: string;
        min_amount?: number;
        two_year_transaction_period?: number;
        cycle?: number;
        contributor_type?: 'individual' | 'committee';
        include_notable?: boolean;
        fuzzy_threshold?: number;
        limit?: number;
        sort_by?: 'amount' | 'date';
      }
    ): Promise<GetReceiptsResult> {
      try {
        const transactionPeriod = params.two_year_transaction_period ?? params.cycle;
        const includeNotable = params.include_notable ?? true;
        const fuzzyThreshold = params.fuzzy_threshold ?? 90;
    
        const response = await client.getScheduleA({
          committee_id: params.committee_id,
          min_amount: params.min_amount ?? 1000,
          two_year_transaction_period: transactionPeriod,
          contributor_type: params.contributor_type,
          limit: params.limit ?? 20,
          sort_by: params.sort_by ?? 'amount',
        });
    
        // Get unique PAC committee IDs for enrichment
        const pacCommitteeIds = [
          ...new Set(
            response.results
              .filter(r => r.contributor_committee_id)
              .map(r => r.contributor_committee_id as string)
          ),
        ];
    
        // Fetch PAC details in parallel (limit to 10 to avoid rate limiting)
        const pacDetailsMap = new Map<string, PACClassification>();
        const pacIdsToFetch = pacCommitteeIds.slice(0, 10);
    
        if (pacIdsToFetch.length > 0) {
          const pacDetailsPromises = pacIdsToFetch.map(async (id) => {
            try {
              const committeeResponse = await client.getCommittee(id);
              if (committeeResponse.results.length > 0) {
                return { id, classification: classifyPAC(committeeResponse.results[0]) };
              }
              return null;
            } catch {
              // If lookup fails, skip this PAC
              return null;
            }
          });
    
          const pacResults = await Promise.all(pacDetailsPromises);
          for (const result of pacResults) {
            if (result) {
              pacDetailsMap.set(result.id, result.classification);
            }
          }
        }
    
        // Transform to enriched receipts with PAC classification
        const enrichedReceipts: EnrichedReceipt[] = response.results.map(record => {
          const base = transformScheduleA(record);
          const pacId = record.contributor_committee_id;
    
          return {
            ...base,
            contributor_committee_id: pacId,
            pac_classification: pacId ? (pacDetailsMap.get(pacId) || null) : null,
          };
        });
    
        // Get committee name from first result if available
        const committeeName = response.results[0]?.committee_name;
    
        // Build response text
        const lines: string[] = [];
    
        if (committeeName) {
          lines.push(`## Contributions to ${committeeName}`);
        } else {
          lines.push(`## Contributions to ${params.committee_id}`);
        }
    
        // Add filter info
        const filters: string[] = [];
        if (params.min_amount) {
          filters.push(`minimum $${params.min_amount.toLocaleString()}`);
        }
        if (params.contributor_type) {
          filters.push(`${params.contributor_type}s only`);
        }
        if (transactionPeriod) {
          filters.push(
            params.two_year_transaction_period
              ? `${transactionPeriod} cycle`
              : `${transactionPeriod} cycle (auto-aligned from cycle)`
          );
        }
    
        if (filters.length > 0) {
          lines.push(`*Filters: ${filters.join(', ')}*`);
        }
    
        lines.push(`*Showing ${enrichedReceipts.length} of ${response.pagination.count} results*`);
    
        const committeeOrOrgReceipts = enrichedReceipts.filter(
          (receipt) => receipt.contributor_type !== 'Individual'
        );
        if (committeeOrOrgReceipts.length > 0) {
          const classifiedCommitteeOrOrgReceipts = committeeOrOrgReceipts.filter(
            (receipt) => receipt.pac_classification !== null
          ).length;
          const unclassifiedCommitteeOrOrgReceipts =
            committeeOrOrgReceipts.length - classifiedCommitteeOrOrgReceipts;
    
          lines.push(
            `*Committee/organization receipts: ${committeeOrOrgReceipts.length} (PAC-classified: ${classifiedCommitteeOrOrgReceipts}, unclassified: ${unclassifiedCommitteeOrOrgReceipts})*`
          );
        }
        lines.push('');
    
        if (includeNotable) {
          const referenceData = loadReferenceData();
          const notableItems = classifyNotableReceipts(
            enrichedReceipts,
            committeeName || params.committee_id,
            referenceData,
            fuzzyThreshold
          );
          lines.push(formatNotableReceiptsText(notableItems, Math.min(params.limit ?? 20, 10)));
          lines.push('');
        }
    
        // Format enriched receipts
        const receiptsText = formatEnrichedReceiptsText(enrichedReceipts, undefined);
        lines.push(receiptsText);
    
        return {
          content: [{ type: 'text', text: lines.join('\n') }],
        };
      } catch (error) {
        return {
          content: [{ type: 'text', text: formatErrorForToolResponse(error) }],
          isError: true,
        };
      }
    }
  • Tool definition object `GET_RECEIPTS_TOOL` with name 'get_receipts', description, and input schema reference.
    export const GET_RECEIPTS_TOOL = {
      name: 'get_receipts',
      description: `Retrieve itemized contributions (Schedule A) received by a campaign committee. Shows individual and organizational donors, amounts, and contributor details. Automatically classifies PAC contributions by type (Corporate, Labor, Trade, Leadership PAC) for deeper analysis. Supports filtering by amount threshold for researching significant contributions and campaign finance patterns.`,
      inputSchema: getReceiptsInputSchema,
    };
  • Input schema definition for get_receipts using Zod, defining all parameters: committee_id, min_amount, two_year_transaction_period, cycle, contributor_type, include_notable, fuzzy_threshold, limit, and sort_by.
    export const getReceiptsInputSchema = {
      committee_id: z
        .string()
        .regex(committeeIdPattern, 'Committee ID must be in format C00000000')
        .describe('FEC committee ID (e.g., "C00401224")'),
    
      min_amount: z
        .number()
        .positive('Minimum amount must be positive')
        .optional()
        .default(1000)
        .describe('Minimum contribution amount to filter (default: $1,000)'),
    
      two_year_transaction_period: z
        .number()
        .int()
        .min(1980)
        .max(2030)
        .optional()
        .describe('Two-year period (e.g., 2024 covers 2023-2024).'),
    
      cycle: z
        .number()
        .int()
        .min(1980)
        .max(2030)
        .optional()
        .describe('Alias for two_year_transaction_period to align with finance cycle filters.'),
    
      contributor_type: z
        .enum(['individual', 'committee'])
        .optional()
        .describe('Filter by contributor type: "individual" or "committee" (PAC)'),
    
      include_notable: z
        .boolean()
        .optional()
        .default(true)
        .describe('Include flagged-first notable analysis block in output (default: true)'),
    
      fuzzy_threshold: z
        .number()
        .int()
        .min(80)
        .max(99)
        .optional()
        .default(90)
        .describe('Fuzzy match confidence threshold for reference list matching (default: 90)'),
    
      limit: z
        .number()
        .int()
        .min(1, 'Limit must be at least 1')
        .max(100, 'Limit cannot exceed 100')
        .optional()
        .default(20)
        .describe('Number of results to return (default: 20, max: 100)'),
    
      sort_by: z
        .enum(['amount', 'date'])
        .optional()
        .default('amount')
        .describe('Sort results by "amount" (descending) or "date" (most recent first)'),
    };
    
    export const getReceiptsParamsSchema = z.object(getReceiptsInputSchema);
    
    export type GetReceiptsInput = z.infer<typeof getReceiptsParamsSchema>;
  • Registration of the get_receipts tool in `registerTools()`: maps GET_RECEIPTS_TOOL def, getReceiptsParamsSchema, and executeGetReceipts handler into the server via server.tool() (lines 130-144, 212-229).
    export function registerTools(server: McpServer, config: Config): void {
      const client = new FECClient({
        apiKey: config.fecApiKey,
        baseUrl: config.fecApiBaseUrl,
        timeout: config.fecApiTimeoutMs,
      });
    
      const toolRegistrations: ToolRegistration[] = [
        {
          def: SEARCH_CANDIDATES_TOOL,
          paramsSchema: searchCandidatesParamsSchema,
          execute: async (params) =>
            executeSearchCandidates(client, params as {
              q: string;
              election_year?: number;
              office?: 'H' | 'S' | 'P';
              state?: string;
              party?: string;
            }),
        },
        {
          def: GET_COMMITTEE_FINANCES_TOOL,
          paramsSchema: getCommitteeFinancesParamsSchema,
          execute: async (params) =>
            executeGetCommitteeFinances(client, params as {
              committee_id: string;
              cycle?: number;
            }),
        },
        {
          def: GET_RECEIPTS_TOOL,
          paramsSchema: getReceiptsParamsSchema,
          execute: async (params) =>
            executeGetReceipts(client, params as {
              committee_id: string;
              min_amount?: number;
              two_year_transaction_period?: number;
              cycle?: number;
              contributor_type?: 'individual' | 'committee';
              include_notable?: boolean;
              fuzzy_threshold?: number;
              limit?: number;
              sort_by?: 'amount' | 'date';
            }),
        },
        {
          def: GET_DISBURSEMENTS_TOOL,
          paramsSchema: getDisbursementsParamsSchema,
          execute: async (params) =>
            executeGetDisbursements(client, params as {
              committee_id: string;
              min_amount?: number;
              two_year_transaction_period?: number;
              cycle?: number;
              purpose?: string;
              include_notable?: boolean;
              fuzzy_threshold?: number;
              limit?: number;
              sort_by?: 'amount' | 'date';
            }),
        },
        {
          def: GET_INDEPENDENT_EXPENDITURES_TOOL,
          paramsSchema: getIndependentExpendituresParamsSchema,
          execute: async (params) =>
            executeGetIndependentExpenditures(client, params as {
              candidate_id?: string;
              committee_id?: string;
              support_oppose?: 'support' | 'oppose';
              min_amount?: number;
              cycle?: number;
              limit?: number;
            }),
        },
        {
          def: GET_COMMITTEE_FLAGS_TOOL,
          paramsSchema: getCommitteeFlagsParamsSchema,
          execute: async (params) =>
            executeGetCommitteeFlags(client, params as {
              committee_id: string;
              cycle?: number;
            }),
        },
        {
          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,
              };
            }
          }
        );
      }
  • Re-export of GET_RECEIPTS_TOOL and executeGetReceipts from the tools index for use by other modules.
    export {
      SEARCH_CANDIDATES_TOOL,
      executeSearchCandidates,
      GET_COMMITTEE_FINANCES_TOOL,
      executeGetCommitteeFinances,
      GET_RECEIPTS_TOOL,
      executeGetReceipts,
      GET_DISBURSEMENTS_TOOL,
      executeGetDisbursements,
      GET_INDEPENDENT_EXPENDITURES_TOOL,
      executeGetIndependentExpenditures,
      GET_COMMITTEE_FLAGS_TOOL,
      executeGetCommitteeFlags,
      SEARCH_DONORS_TOOL,
      executeSearchDonors,
      SEARCH_SPENDING_TOOL,
      executeSearchSpending,
    };
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description must disclose behavioral traits. It notes the auto-classification of PAC contributions and filtering, but does not state that it is read-only, describe pagination, or mention any rate limits. Adequate but lacks full transparency.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences, each adding value. The most important information (action and resource) comes first. No extraneous content, though structure could be slightly improved with bullet points.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description mentions output includes donors, amounts, and details, but lacks specifics on return structure or pagination. Given 9 parameters and no output schema, more detail on the response format would enhance completeness.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3. The description adds value by explaining auto-classification of PAC types (related to contributor_type) and the 'notable analysis block' (include_notable), providing context beyond parameter descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clearly states it retrieves itemized contributions (Schedule A) for a campaign committee, distinguishing it from sibling tools like get_disbursements or get_independent_expenditures. The verb 'Retrieve' and the specific reference to contributions make the purpose unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies the tool is for contributions received, but does not explicitly state when to use it versus alternatives. Given sibling tools exist, explicit guidance on when to use this tool (e.g., for receipts, not disbursements) would improve clarity.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sh-patterson/fec-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server