sc-elections-mcp 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +187 -0
  2. package/dist/api/ethics-client.d.ts +45 -0
  3. package/dist/api/ethics-client.js +662 -0
  4. package/dist/api/ethics-client.js.map +1 -0
  5. package/dist/api/vrems-client.d.ts +18 -0
  6. package/dist/api/vrems-client.js +93 -0
  7. package/dist/api/vrems-client.js.map +1 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +30 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/parsers/candidate-detail.d.ts +12 -0
  12. package/dist/parsers/candidate-detail.js +112 -0
  13. package/dist/parsers/candidate-detail.js.map +1 -0
  14. package/dist/parsers/candidate-search.d.ts +6 -0
  15. package/dist/parsers/candidate-search.js +39 -0
  16. package/dist/parsers/candidate-search.js.map +1 -0
  17. package/dist/parsers/csv-export.d.ts +5 -0
  18. package/dist/parsers/csv-export.js +81 -0
  19. package/dist/parsers/csv-export.js.map +1 -0
  20. package/dist/tools/campaign.d.ts +2 -0
  21. package/dist/tools/campaign.js +191 -0
  22. package/dist/tools/campaign.js.map +1 -0
  23. package/dist/tools/cross-search.d.ts +26 -0
  24. package/dist/tools/cross-search.js +219 -0
  25. package/dist/tools/cross-search.js.map +1 -0
  26. package/dist/tools/overlap.d.ts +25 -0
  27. package/dist/tools/overlap.js +201 -0
  28. package/dist/tools/overlap.js.map +1 -0
  29. package/dist/tools/search.d.ts +2 -0
  30. package/dist/tools/search.js +146 -0
  31. package/dist/tools/search.js.map +1 -0
  32. package/dist/tools/sei.d.ts +2 -0
  33. package/dist/tools/sei.js +99 -0
  34. package/dist/tools/sei.js.map +1 -0
  35. package/dist/tools/vrems.d.ts +2 -0
  36. package/dist/tools/vrems.js +138 -0
  37. package/dist/tools/vrems.js.map +1 -0
  38. package/dist/types.d.ts +430 -0
  39. package/dist/types.js +5 -0
  40. package/dist/types.js.map +1 -0
  41. package/package.json +46 -0
package/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # sc-elections-mcp
2
+
3
+ MCP server for South Carolina elections data. Combines two public data sources into 15 tools for researching candidates, campaign finance, and official disclosures.
4
+
5
+ **No API keys required** — all data comes from public government websites.
6
+
7
+ ## Data Sources
8
+
9
+ | Source | URL | What it covers |
10
+ |--------|-----|----------------|
11
+ | **SC Ethics Commission** | `ethicsfiling.sc.gov` | Campaign finance reports, contributions, expenditures, Statements of Economic Interest (SEI) |
12
+ | **SC Votes / VREMS** | `vrems.scvotes.sc.gov` | Election listings, candidate filings, contact info, filing documents |
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ # Claude Code
18
+ claude mcp add sc-elections -- npx -y sc-elections-mcp
19
+
20
+ # Claude Desktop — add to claude_desktop_config.json
21
+ {
22
+ "mcpServers": {
23
+ "sc-elections": {
24
+ "command": "npx",
25
+ "args": ["-y", "sc-elections-mcp"]
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ Requires **Node.js >= 20**.
32
+
33
+ ## Tools
34
+
35
+ ### Search & Lookup
36
+
37
+ | Tool | Description | Key params |
38
+ |------|-------------|------------|
39
+ | `search_filers` | Search candidates/officials by name (default limit 50, use last name only) | `name`, `limit`? |
40
+ | `list_filers_by_office` | Find all filers for an office; enriches with balance/campaignId when recent_only | `office`, `recent_only` |
41
+ | `get_filer_profile` | Full profile: address, phone, positions, offices | `candidate_filer_id`, `sei_filer_id` |
42
+ | `list_office_names` | Discover exact office name strings from the database (e.g. "District 50 House") | `keyword`? |
43
+
44
+ ### Campaign Finance
45
+
46
+ | Tool | Description | Key params |
47
+ |------|-------------|------------|
48
+ | `get_campaign_summary` | Open/closed offices, balances, contribution totals | `candidate_filer_id` |
49
+ | `get_campaign_reports` | List filed reports (campaign_id auto-resolved) | `candidate_filer_id`, `campaign_id`?, `office`? |
50
+ | `get_campaign_report_details` | Detailed income/expenditure breakdown for a report | `report_id` |
51
+ | `get_contributions` | Contributions with metadata header, summary mode, filters | `candidate_filer_id`, `campaign_id`?, `office`?, `summary`?, `year`?, `min_amount`?, `limit`? |
52
+ | `get_expenditures` | Expenditures with metadata header, summary mode, filters | `candidate_filer_id`, `campaign_id`?, `office`?, `summary`?, `year`?, `min_amount`?, `limit`? |
53
+
54
+ ### Cross-Candidate Search
55
+
56
+ | Tool | Description | Key params |
57
+ |------|-------------|------------|
58
+ | `search_expenditures` | Search expenditures across ALL candidates statewide (default limit 200) | `candidate`, `vendor_name`, `office`, `year`, `amount`, `limit`? |
59
+ | `search_contributions` | Search contributions across ALL candidates statewide (default limit 200) | `candidate`, `contributor_name`, `office`, `year`, `amount`, `limit`? |
60
+
61
+ ### Statement of Economic Interest
62
+
63
+ | Tool | Description | Key params |
64
+ |------|-------------|------------|
65
+ | `get_sei_details` | Positions, business interests, income, gifts, travel, creditors, lobbyist contacts | `sei_filer_id`, `report_year` (optional) |
66
+
67
+ ### Candidate Filings (SC Votes)
68
+
69
+ | Tool | Description | Key params |
70
+ |------|-------------|------------|
71
+ | `list_elections` | Browse elections by type and year; keyword filter + default limit 50 | `election_type`, `year`, `keyword`?, `limit`? |
72
+ | `search_candidates` | Search candidates in an election (default limit 50) — includes phone, email, address | `election_id`, `limit`?, plus optional filters |
73
+ | `get_candidate_details` | Filing details with document download links (filing form PDF, fee receipt) | `candidate_id`, `election_id` |
74
+
75
+ ### How IDs connect
76
+
77
+ ```
78
+ search_filers("name")
79
+ → candidateFilerId, seiFilerId
80
+
81
+ get_filer_profile(candidateFilerId, seiFilerId)
82
+ → campaignId (from openOffices / closedOffices)
83
+
84
+ Campaign tools use: campaignId + candidateFilerId
85
+ SEI tools use: seiFilerId
86
+ VREMS tools use: electionId → candidateId
87
+ ```
88
+
89
+ ## Workflows
90
+
91
+ ### Research a Known Candidate (Ethics-first)
92
+
93
+ ```
94
+ 1. search_filers("mcmaster")
95
+ → Henry McMaster: candidateFilerId: 27353, seiFilerId: 6579
96
+
97
+ 2. get_contributions(candidate_filer_id: 27353, office: "Governor", summary: true)
98
+ → Auto-resolves campaignId, returns top 20 donors + totals
99
+ → Metadata header confirms: "Henry McMaster — Governor"
100
+
101
+ 3. get_campaign_reports(candidate_filer_id: 27353)
102
+ → Auto-resolves to most recent campaign
103
+
104
+ 4. get_sei_details(6579)
105
+ → Positions, income sources, business interests
106
+ ```
107
+
108
+ For candidates with multiple campaigns, use `office` hint or explicit `campaign_id`.
109
+
110
+ ### Research a Race (VREMS-first)
111
+
112
+ ```
113
+ 1. list_elections({ election_type: "Local", year: 2024, keyword: "Sumter" })
114
+ → Only Sumter-area elections (not all 200+ local elections)
115
+
116
+ 2. search_candidates({ election_id: "22152", status: "Elected" })
117
+ → Who won each race — names, contact info, party
118
+
119
+ 3. search_filers("winner name")
120
+ → Bridge to Ethics: get candidateFilerId, seiFilerId
121
+ → If no match, try last name only or spelling variations
122
+
123
+ 4. get_campaign_summary(candidateFilerId)
124
+ → Campaign finance overview
125
+ ```
126
+
127
+ For staggered terms (e.g. county council), check multiple election cycles to find all current members.
128
+
129
+ ### Follow the Money (Cross-search)
130
+
131
+ ```
132
+ 1. search_expenditures({ vendor_name: "printing", year: 2024 })
133
+ → All candidates who paid printing vendors in 2024
134
+
135
+ 2. search_contributions({ contributor_name: "smith", office: "Governor" })
136
+ → All contributions from "smith" to Governor races
137
+
138
+ 3. search_expenditures({ candidate: "haley" })
139
+ → Everything Haley's campaigns spent money on
140
+ ```
141
+
142
+ ## Caveats
143
+
144
+ These two data sources are independent systems with no shared identifiers:
145
+
146
+ - **Use `list_filers_by_office` for broad discovery.** For questions like "who has filed for [office]", use `list_filers_by_office` with `recent_only: true` — it sweeps the entire Ethics Commission database by office name and enriches results with campaign balance, status, and campaignId. Takes 10-15 seconds but finds candidates invisible to name-based or cross-search tools. Supplement with a web search for candidates who haven't filed yet.
147
+ - **Office names are inconsistent within Ethics.** A candidate can have different office labels across endpoints. Use `list_office_names` to discover the exact strings the database uses (e.g., "District 50 House" not "State House District 50"). In cross-search tools, use the broadest match possible or search by candidate name instead.
148
+ - **Bridge by name, not ID.** VREMS and Ethics Commission use different ID systems. To connect a VREMS candidate to their Ethics campaign finance data, search by name using `search_filers`. Name variations (nicknames, suffixes, maiden names) may require retrying with last name only.
149
+ - **"Open campaign" ≠ "in office."** A candidate can have an open campaign account for an office they never won or no longer hold. Use VREMS election results (`status: "Elected"`) to determine current officeholders.
150
+ - **Initial Reports are the early signal.** The Ethics Commission requires candidates to file an Initial Report when they start raising or spending money — often weeks or months before the VREMS filing period opens. Look for recent Initial Reports to discover new candidates before they appear in VREMS.
151
+ - **VREMS is the authority on officeholders.** Ethics Commission tracks financial filings, not election outcomes. Only VREMS shows who filed, who won, and who withdrew.
152
+ - **All data is public.** No API keys or authentication required.
153
+ - **Filing data has processing lag.** Recent campaign activity may not appear until the next filing deadline passes and reports are processed.
154
+
155
+ ## Development
156
+
157
+ ```bash
158
+ git clone https://github.com/asreynolds1000/sc-elections-mcp.git
159
+ cd sc-elections-mcp
160
+ npm install
161
+ npm run dev # Run with tsx (hot reload)
162
+ npm run build # Compile TypeScript to dist/
163
+ npm start # Run compiled version
164
+ ```
165
+
166
+ ### Testing
167
+
168
+ All tools can be tested against the live public APIs — no mocks or API keys needed.
169
+
170
+ ```bash
171
+ # Quick smoke test: start the server and verify it connects
172
+ echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.1.0"}}}' | node dist/index.js
173
+ ```
174
+
175
+ ## Data Coverage
176
+
177
+ **Ethics Commission** covers anyone who has filed campaign finance reports or Statements of Economic Interest in South Carolina — from statewide offices (Governor, Attorney General) down to local fire districts and school boards. SEI reports include income sources, business interests, gifts, travel, creditors, and lobbyist contacts.
178
+
179
+ **SC Votes / VREMS** covers candidate filings for General, Special, and Local elections. The CSV export provides rich contact data (phone, email, address, filing fee) that isn't available from the HTML search alone.
180
+
181
+ ## License
182
+
183
+ MIT
184
+
185
+ ## Author
186
+
187
+ Alex Reynolds ([@asreynolds1000](https://github.com/asreynolds1000))
@@ -0,0 +1,45 @@
1
+ import type { EthicsFiler, FilerProfile, CampaignSummary, CampaignReport, CampaignReportDetails, CampaignContribution, CampaignExpenditure, CampaignContext, ContributionSummary, ExpenditureSummary, NormalizedOffice, GroupedFiler, CrossSearchExpenditure, CrossSearchContribution, SeiReport, SeiDetails, OfficeFilerResult } from '../types.js';
2
+ export declare function searchFilers(name: string): Promise<EthicsFiler[]>;
3
+ export declare function getFilerProfile(candidateFilerId: number, seiFilerId: number): Promise<FilerProfile>;
4
+ export declare function dedupeKey(filer: EthicsFiler): string;
5
+ /** @internal — exported for testing */
6
+ export declare function isTestAccount(filer: EthicsFiler): boolean;
7
+ export declare function searchFilersByOffice(officeName: string, activeSince?: number): Promise<OfficeFilerResult>;
8
+ export declare function groupFilersByPerson(filers: EthicsFiler[]): GroupedFiler[];
9
+ export declare function listOfficeNames(keyword?: string): Promise<string[]>;
10
+ /** Try to resolve a candidate's display name when summary.name is null */
11
+ export declare function resolveCandidateName(candidateFilerId: number): Promise<string | undefined>;
12
+ export declare function getCampaignSummary(candidateFilerId: number): Promise<CampaignSummary>;
13
+ export declare function getCampaignReports(campaignId: number, candidateFilerId: number): Promise<CampaignReport[]>;
14
+ export declare function getCampaignReportDetails(reportId: number): Promise<CampaignReportDetails>;
15
+ export declare function getContributions(campaignId: number, candidateFilerId: number): Promise<CampaignContribution[]>;
16
+ export declare function getExpenditures(campaignId: number, candidateFilerId: number): Promise<CampaignExpenditure[]>;
17
+ export declare function cachedGetCampaignSummary(candidateFilerId: number): Promise<CampaignSummary>;
18
+ export declare function normalizeOfficeName(raw: string): NormalizedOffice;
19
+ export declare function resolveCampaignContext(summary: CampaignSummary, candidateFilerId: number, campaignId?: number, officeHint?: string, candidateNameOverride?: string): {
20
+ context: CampaignContext;
21
+ resolvedCampaignId: number;
22
+ } | {
23
+ error: string;
24
+ };
25
+ export declare function buildContributionSummary(contributions: CampaignContribution[], context: CampaignContext): ContributionSummary;
26
+ export declare function buildExpenditureSummary(expenditures: CampaignExpenditure[], context: CampaignContext): ExpenditureSummary;
27
+ export declare function searchExpenditures(filters: {
28
+ candidate?: string;
29
+ office?: string;
30
+ vendorName?: string;
31
+ expenditureYear?: number;
32
+ vendorLoc?: string;
33
+ amount?: number;
34
+ expDesc?: string;
35
+ }): Promise<CrossSearchExpenditure[]>;
36
+ export declare function searchContributions(filters: {
37
+ candidate?: string;
38
+ office?: string;
39
+ contributorName?: string;
40
+ contributionYear?: number;
41
+ contributorLoc?: string;
42
+ amount?: number;
43
+ }): Promise<CrossSearchContribution[]>;
44
+ export declare function getSeiReportVersions(seiFilerId: number): Promise<SeiReport[]>;
45
+ export declare function getSeiDetails(seiFilerId: number, reportYear?: number): Promise<SeiDetails | null>;