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.
- package/README.md +187 -0
- package/dist/api/ethics-client.d.ts +45 -0
- package/dist/api/ethics-client.js +662 -0
- package/dist/api/ethics-client.js.map +1 -0
- package/dist/api/vrems-client.d.ts +18 -0
- package/dist/api/vrems-client.js +93 -0
- package/dist/api/vrems-client.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/parsers/candidate-detail.d.ts +12 -0
- package/dist/parsers/candidate-detail.js +112 -0
- package/dist/parsers/candidate-detail.js.map +1 -0
- package/dist/parsers/candidate-search.d.ts +6 -0
- package/dist/parsers/candidate-search.js +39 -0
- package/dist/parsers/candidate-search.js.map +1 -0
- package/dist/parsers/csv-export.d.ts +5 -0
- package/dist/parsers/csv-export.js +81 -0
- package/dist/parsers/csv-export.js.map +1 -0
- package/dist/tools/campaign.d.ts +2 -0
- package/dist/tools/campaign.js +191 -0
- package/dist/tools/campaign.js.map +1 -0
- package/dist/tools/cross-search.d.ts +26 -0
- package/dist/tools/cross-search.js +219 -0
- package/dist/tools/cross-search.js.map +1 -0
- package/dist/tools/overlap.d.ts +25 -0
- package/dist/tools/overlap.js +201 -0
- package/dist/tools/overlap.js.map +1 -0
- package/dist/tools/search.d.ts +2 -0
- package/dist/tools/search.js +146 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/sei.d.ts +2 -0
- package/dist/tools/sei.js +99 -0
- package/dist/tools/sei.js.map +1 -0
- package/dist/tools/vrems.d.ts +2 -0
- package/dist/tools/vrems.js +138 -0
- package/dist/tools/vrems.js.map +1 -0
- package/dist/types.d.ts +430 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getCampaignSummary, getCampaignReportDetails, cachedGetCampaignSummary, resolveCampaignContext, resolveCandidateName, getContributions, getExpenditures, getCampaignReports, buildContributionSummary, buildExpenditureSummary, } from '../api/ethics-client.js';
|
|
3
|
+
function formatHeader(context, count, totalLabel, totalAmount) {
|
|
4
|
+
return `Campaign: ${context.candidateName} — ${context.officeName} (campaignId: ${context.campaignId}, candidateFilerId: ${context.candidateFilerId}, status: ${context.campaignStatus})\n${count} ${totalLabel} totaling $${totalAmount.toFixed(2)}\n---`;
|
|
5
|
+
}
|
|
6
|
+
export function registerCampaignTools(server) {
|
|
7
|
+
server.tool('get_campaign_summary', 'Get campaign report summary for a candidate showing open/closed offices, balances, and contribution totals. Use candidateFilerId from search_filers. Note: an open campaign does not necessarily mean the person currently holds that office.', {
|
|
8
|
+
candidate_filer_id: z.number().describe('candidateFilerId from search_filers results'),
|
|
9
|
+
}, async ({ candidate_filer_id }) => {
|
|
10
|
+
try {
|
|
11
|
+
const summary = await getCampaignSummary(candidate_filer_id);
|
|
12
|
+
return {
|
|
13
|
+
content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
19
|
+
isError: true,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
server.tool('get_campaign_reports', 'List filed campaign disclosure reports. campaign_id is optional — auto-resolved for single-campaign candidates, or use office hint to disambiguate (e.g. office="County Council").', {
|
|
24
|
+
candidate_filer_id: z.number().describe('candidateFilerId from search_filers results'),
|
|
25
|
+
campaign_id: z.number().optional().describe('campaignId — optional, auto-resolved if candidate has one campaign'),
|
|
26
|
+
office: z.string().optional().describe('Office hint to disambiguate when multiple campaigns exist (e.g. "County Council", "Governor")'),
|
|
27
|
+
}, async ({ candidate_filer_id, campaign_id, office }) => {
|
|
28
|
+
try {
|
|
29
|
+
const summary = await cachedGetCampaignSummary(candidate_filer_id);
|
|
30
|
+
const resolved = resolveCampaignContext(summary, candidate_filer_id, campaign_id, office);
|
|
31
|
+
if ('error' in resolved) {
|
|
32
|
+
return { content: [{ type: 'text', text: resolved.error }], isError: true };
|
|
33
|
+
}
|
|
34
|
+
const reports = await getCampaignReports(resolved.resolvedCampaignId, candidate_filer_id);
|
|
35
|
+
const header = `Campaign: ${resolved.context.candidateName} — ${resolved.context.officeName} (campaignId: ${resolved.context.campaignId}, status: ${resolved.context.campaignStatus})`;
|
|
36
|
+
return {
|
|
37
|
+
content: [{
|
|
38
|
+
type: 'text',
|
|
39
|
+
text: reports.length === 0
|
|
40
|
+
? `${header}\nNo campaign reports found`
|
|
41
|
+
: `${header}\n${reports.length} report(s)\n---\n${JSON.stringify(reports, null, 2)}`,
|
|
42
|
+
}],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
48
|
+
isError: true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
server.tool('get_campaign_report_details', 'Get detailed breakdown of a single campaign report including income categories, expenditure totals, balance, and filing metadata. Use report_id from get_campaign_reports results.', {
|
|
53
|
+
report_id: z.number().describe('Report ID from get_campaign_reports'),
|
|
54
|
+
}, async ({ report_id }) => {
|
|
55
|
+
try {
|
|
56
|
+
const details = await getCampaignReportDetails(report_id);
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: 'text', text: JSON.stringify(details, null, 2) }],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
64
|
+
isError: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
server.tool('get_contributions', 'Get contributions for a candidate\'s campaign. Returns donor names, amounts, dates, types. Every response includes a metadata header identifying the candidate/office for verification.\n\ncampaign_id is optional: omit it for single-campaign candidates (auto-resolved). Provide it when a candidate has multiple campaigns, or use the office hint to disambiguate (e.g. office="County Council").\n\nUse summary=true for high-volume candidates — returns top 20 donors, totals by type, date range. Use year/min_amount to filter. Default limit is 200 records; pass limit=0 for all.', {
|
|
69
|
+
candidate_filer_id: z.number().describe('candidateFilerId from search_filers results'),
|
|
70
|
+
campaign_id: z.number().optional().describe('campaignId — optional, auto-resolved if candidate has one campaign'),
|
|
71
|
+
office: z.string().optional().describe('Office hint to disambiguate when multiple campaigns exist (e.g. "County Council", "Governor")'),
|
|
72
|
+
summary: z.boolean().optional().describe('If true, return aggregated top-20 view instead of full records'),
|
|
73
|
+
year: z.number().optional().describe('Filter to contributions in this year only'),
|
|
74
|
+
min_amount: z.number().optional().describe('Filter to contributions >= this amount'),
|
|
75
|
+
limit: z.number().optional().describe('Max records to return (default 200, 0 for all)'),
|
|
76
|
+
}, async ({ candidate_filer_id, campaign_id, office, summary: wantSummary, year, min_amount, limit }) => {
|
|
77
|
+
try {
|
|
78
|
+
const campaignSummary = await cachedGetCampaignSummary(candidate_filer_id);
|
|
79
|
+
// Resolve candidate name when summary.name is null
|
|
80
|
+
let candidateNameOverride;
|
|
81
|
+
if (!campaignSummary.name) {
|
|
82
|
+
candidateNameOverride = await resolveCandidateName(candidate_filer_id);
|
|
83
|
+
}
|
|
84
|
+
const resolved = resolveCampaignContext(campaignSummary, candidate_filer_id, campaign_id, office, candidateNameOverride);
|
|
85
|
+
if ('error' in resolved) {
|
|
86
|
+
return { content: [{ type: 'text', text: resolved.error }], isError: true };
|
|
87
|
+
}
|
|
88
|
+
let contributions = await getContributions(resolved.resolvedCampaignId, candidate_filer_id);
|
|
89
|
+
// Client-side filters
|
|
90
|
+
if (year) {
|
|
91
|
+
contributions = contributions.filter(c => {
|
|
92
|
+
if (!c.date)
|
|
93
|
+
return false;
|
|
94
|
+
return new Date(c.date).getFullYear() === year;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (min_amount) {
|
|
98
|
+
contributions = contributions.filter(c => c.credit >= min_amount);
|
|
99
|
+
}
|
|
100
|
+
if (wantSummary) {
|
|
101
|
+
const result = buildContributionSummary(contributions, resolved.context);
|
|
102
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
103
|
+
}
|
|
104
|
+
// Full records with limit
|
|
105
|
+
const totalCount = contributions.length;
|
|
106
|
+
const totalAmount = contributions.reduce((sum, c) => sum + c.credit, 0);
|
|
107
|
+
const effectiveLimit = limit === undefined ? 200 : limit;
|
|
108
|
+
const limited = effectiveLimit > 0 ? contributions.slice(0, effectiveLimit) : contributions;
|
|
109
|
+
const header = formatHeader(resolved.context, totalCount, 'contributions', totalAmount);
|
|
110
|
+
const limitNote = effectiveLimit > 0 && totalCount > effectiveLimit
|
|
111
|
+
? `\nShowing ${effectiveLimit} of ${totalCount}. Use limit=0 for all, or summary=true for aggregated view.`
|
|
112
|
+
: '';
|
|
113
|
+
return {
|
|
114
|
+
content: [{
|
|
115
|
+
type: 'text',
|
|
116
|
+
text: totalCount === 0
|
|
117
|
+
? `${header}\nNo contributions found`
|
|
118
|
+
: `${header}${limitNote}\n${JSON.stringify(limited, null, 2)}`,
|
|
119
|
+
}],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
125
|
+
isError: true,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
server.tool('get_expenditures', 'Get expenditures for a candidate\'s campaign. Returns vendor names, amounts, dates, types. Every response includes a metadata header identifying the candidate/office for verification.\n\ncampaign_id is optional: omit it for single-campaign candidates (auto-resolved). Provide it when a candidate has multiple campaigns, or use the office hint to disambiguate (e.g. office="County Council").\n\nUse summary=true for high-volume candidates — returns top 20 vendors, totals by type, date range. Use year/min_amount to filter. Default limit is 200 records; pass limit=0 for all.', {
|
|
130
|
+
candidate_filer_id: z.number().describe('candidateFilerId from search_filers results'),
|
|
131
|
+
campaign_id: z.number().optional().describe('campaignId — optional, auto-resolved if candidate has one campaign'),
|
|
132
|
+
office: z.string().optional().describe('Office hint to disambiguate when multiple campaigns exist (e.g. "County Council", "Governor")'),
|
|
133
|
+
summary: z.boolean().optional().describe('If true, return aggregated top-20 view instead of full records'),
|
|
134
|
+
year: z.number().optional().describe('Filter to expenditures in this year only'),
|
|
135
|
+
min_amount: z.number().optional().describe('Filter to expenditures >= this amount'),
|
|
136
|
+
limit: z.number().optional().describe('Max records to return (default 200, 0 for all)'),
|
|
137
|
+
}, async ({ candidate_filer_id, campaign_id, office, summary: wantSummary, year, min_amount, limit }) => {
|
|
138
|
+
try {
|
|
139
|
+
const campaignSummary = await cachedGetCampaignSummary(candidate_filer_id);
|
|
140
|
+
// Resolve candidate name when summary.name is null
|
|
141
|
+
let candidateNameOverride;
|
|
142
|
+
if (!campaignSummary.name) {
|
|
143
|
+
candidateNameOverride = await resolveCandidateName(candidate_filer_id);
|
|
144
|
+
}
|
|
145
|
+
const resolved = resolveCampaignContext(campaignSummary, candidate_filer_id, campaign_id, office, candidateNameOverride);
|
|
146
|
+
if ('error' in resolved) {
|
|
147
|
+
return { content: [{ type: 'text', text: resolved.error }], isError: true };
|
|
148
|
+
}
|
|
149
|
+
let expenditures = await getExpenditures(resolved.resolvedCampaignId, candidate_filer_id);
|
|
150
|
+
// Client-side filters
|
|
151
|
+
if (year) {
|
|
152
|
+
expenditures = expenditures.filter(e => {
|
|
153
|
+
if (!e.date)
|
|
154
|
+
return false;
|
|
155
|
+
return new Date(e.date).getFullYear() === year;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (min_amount) {
|
|
159
|
+
expenditures = expenditures.filter(e => e.debit >= min_amount);
|
|
160
|
+
}
|
|
161
|
+
if (wantSummary) {
|
|
162
|
+
const result = buildExpenditureSummary(expenditures, resolved.context);
|
|
163
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
164
|
+
}
|
|
165
|
+
// Full records with limit
|
|
166
|
+
const totalCount = expenditures.length;
|
|
167
|
+
const totalAmount = expenditures.reduce((sum, e) => sum + e.debit, 0);
|
|
168
|
+
const effectiveLimit = limit === undefined ? 200 : limit;
|
|
169
|
+
const limited = effectiveLimit > 0 ? expenditures.slice(0, effectiveLimit) : expenditures;
|
|
170
|
+
const header = formatHeader(resolved.context, totalCount, 'expenditures', totalAmount);
|
|
171
|
+
const limitNote = effectiveLimit > 0 && totalCount > effectiveLimit
|
|
172
|
+
? `\nShowing ${effectiveLimit} of ${totalCount}. Use limit=0 for all, or summary=true for aggregated view.`
|
|
173
|
+
: '';
|
|
174
|
+
return {
|
|
175
|
+
content: [{
|
|
176
|
+
type: 'text',
|
|
177
|
+
text: totalCount === 0
|
|
178
|
+
? `${header}\nNo expenditures found`
|
|
179
|
+
: `${header}${limitNote}\n${JSON.stringify(limited, null, 2)}`,
|
|
180
|
+
}],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
return {
|
|
185
|
+
content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
186
|
+
isError: true,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=campaign.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"campaign.js","sourceRoot":"","sources":["../../src/tools/campaign.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { CrossSearchContribution, CrossSearchExpenditure } from '../types.js';
|
|
3
|
+
/** @internal — exported for testing */
|
|
4
|
+
export declare function summarizeCrossSearchContributions(results: CrossSearchContribution[]): {
|
|
5
|
+
totalRecords: number;
|
|
6
|
+
grandTotal: number;
|
|
7
|
+
byCandidateTop20: {
|
|
8
|
+
candidateName: string;
|
|
9
|
+
officeName: string;
|
|
10
|
+
totalAmount: number;
|
|
11
|
+
count: number;
|
|
12
|
+
}[];
|
|
13
|
+
};
|
|
14
|
+
/** @internal — exported for testing */
|
|
15
|
+
export declare function summarizeCrossSearchExpenditures(results: CrossSearchExpenditure[]): {
|
|
16
|
+
totalRecords: number;
|
|
17
|
+
grandTotal: number;
|
|
18
|
+
byVendorTop20: {
|
|
19
|
+
vendorName: string;
|
|
20
|
+
totalAmount: number;
|
|
21
|
+
count: number;
|
|
22
|
+
candidateCount: number;
|
|
23
|
+
candidates: string[];
|
|
24
|
+
}[];
|
|
25
|
+
};
|
|
26
|
+
export declare function registerCrossSearchTools(server: McpServer): void;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { searchExpenditures, searchContributions, normalizeOfficeName } from '../api/ethics-client.js';
|
|
3
|
+
const CHAR_BUDGET = 60_000;
|
|
4
|
+
/** @internal — exported for testing */
|
|
5
|
+
export function summarizeCrossSearchContributions(results) {
|
|
6
|
+
const byCandidate = new Map();
|
|
7
|
+
let grandTotal = 0;
|
|
8
|
+
for (const r of results) {
|
|
9
|
+
grandTotal += r.amount;
|
|
10
|
+
const key = `${r.candidateId}-${r.officeRunId}`;
|
|
11
|
+
const existing = byCandidate.get(key);
|
|
12
|
+
if (existing) {
|
|
13
|
+
existing.totalAmount += r.amount;
|
|
14
|
+
existing.count++;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
byCandidate.set(key, {
|
|
18
|
+
candidateName: r.candidateName,
|
|
19
|
+
officeName: r.officeName,
|
|
20
|
+
totalAmount: r.amount,
|
|
21
|
+
count: 1,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
totalRecords: results.length,
|
|
27
|
+
grandTotal,
|
|
28
|
+
byCandidateTop20: [...byCandidate.values()]
|
|
29
|
+
.sort((a, b) => b.totalAmount - a.totalAmount)
|
|
30
|
+
.slice(0, 20),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** @internal — exported for testing */
|
|
34
|
+
export function summarizeCrossSearchExpenditures(results) {
|
|
35
|
+
const byVendor = new Map();
|
|
36
|
+
let grandTotal = 0;
|
|
37
|
+
for (const r of results) {
|
|
38
|
+
grandTotal += r.amount;
|
|
39
|
+
const key = r.vendorName.trim().toLowerCase();
|
|
40
|
+
const existing = byVendor.get(key);
|
|
41
|
+
if (existing) {
|
|
42
|
+
existing.totalAmount += r.amount;
|
|
43
|
+
existing.count++;
|
|
44
|
+
existing.candidates.add(r.candidateName);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
byVendor.set(key, {
|
|
48
|
+
vendorName: r.vendorName,
|
|
49
|
+
totalAmount: r.amount,
|
|
50
|
+
count: 1,
|
|
51
|
+
candidates: new Set([r.candidateName]),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
totalRecords: results.length,
|
|
57
|
+
grandTotal,
|
|
58
|
+
byVendorTop20: [...byVendor.values()]
|
|
59
|
+
.sort((a, b) => b.totalAmount - a.totalAmount)
|
|
60
|
+
.slice(0, 20)
|
|
61
|
+
.map(v => ({ vendorName: v.vendorName, totalAmount: v.totalAmount, count: v.count, candidateCount: v.candidates.size, candidates: [...v.candidates].slice(0, 5) })),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function applyCharBudget(output) {
|
|
65
|
+
const rawText = JSON.stringify(output, null, 2);
|
|
66
|
+
if (rawText.length <= CHAR_BUDGET) {
|
|
67
|
+
return { finalText: rawText, budgetWarning: '' };
|
|
68
|
+
}
|
|
69
|
+
// Linear scan: find how many records fit in the budget
|
|
70
|
+
let kept = 0;
|
|
71
|
+
let runningLength = 2; // opening "[\n"
|
|
72
|
+
for (let i = 0; i < output.length; i++) {
|
|
73
|
+
const itemStr = JSON.stringify(output[i], null, 2);
|
|
74
|
+
const addedLength = itemStr.length + (i > 0 ? 2 : 0); // comma + newline
|
|
75
|
+
if (runningLength + addedLength + 2 > CHAR_BUDGET)
|
|
76
|
+
break; // +2 for closing "\n]"
|
|
77
|
+
runningLength += addedLength;
|
|
78
|
+
kept++;
|
|
79
|
+
}
|
|
80
|
+
if (kept === 0)
|
|
81
|
+
kept = 1; // Always include at least one record
|
|
82
|
+
const budgetWarning = `\nWARNING: Response truncated to ${kept} of ${output.length} records (60K char budget). Use slim=true to strip address fields, summary=true for aggregated view, or add more filters.`;
|
|
83
|
+
return { finalText: JSON.stringify(output.slice(0, kept), null, 2), budgetWarning };
|
|
84
|
+
}
|
|
85
|
+
export function registerCrossSearchTools(server) {
|
|
86
|
+
server.tool('search_expenditures', 'Search expenditures across ALL candidates statewide. At least one filter required. Default limit 200 (0 for all). Use slim=true to strip address fields, summary=true for aggregated view. Responses auto-truncate at 60K chars. IMPORTANT: Office names in results are inconsistent — use broad office filters or omit office and filter by candidate name instead.', {
|
|
87
|
+
candidate: z.string().optional().describe('Candidate name filter (partial match)'),
|
|
88
|
+
office: z.string().optional().describe('Office name filter (partial match)'),
|
|
89
|
+
vendor_name: z.string().optional().describe('Vendor/payee name filter (partial match)'),
|
|
90
|
+
year: z.number().optional().describe('Election year filter (e.g. 2024). Omit for all years.'),
|
|
91
|
+
amount: z.number().optional().describe('Minimum amount filter'),
|
|
92
|
+
description: z.string().optional().describe('Expenditure description filter'),
|
|
93
|
+
limit: z.number().optional().describe('Max results to return (default 200, 0 for all).'),
|
|
94
|
+
slim: z.boolean().optional().describe('If true, strip address fields to reduce response size.'),
|
|
95
|
+
summary: z.boolean().optional().describe('If true, group by vendor and return totals instead of individual records.'),
|
|
96
|
+
}, async ({ candidate, office, vendor_name, year, amount, description, limit, slim, summary }) => {
|
|
97
|
+
try {
|
|
98
|
+
// Guard: all-empty queries return the entire expenditure database
|
|
99
|
+
const hasAnyFilter = candidate || office || vendor_name || year || amount || description;
|
|
100
|
+
if (!hasAnyFilter) {
|
|
101
|
+
return {
|
|
102
|
+
content: [{
|
|
103
|
+
type: 'text',
|
|
104
|
+
text: 'At least one filter is required for search_expenditures. Provide candidate, office, vendor_name, year, amount, or description.',
|
|
105
|
+
}],
|
|
106
|
+
isError: true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const results = await searchExpenditures({
|
|
110
|
+
candidate,
|
|
111
|
+
office,
|
|
112
|
+
vendorName: vendor_name,
|
|
113
|
+
expenditureYear: year,
|
|
114
|
+
amount,
|
|
115
|
+
expDesc: description,
|
|
116
|
+
});
|
|
117
|
+
const enriched = results.map(r => ({
|
|
118
|
+
...r,
|
|
119
|
+
normalizedOffice: normalizeOfficeName(r.office),
|
|
120
|
+
}));
|
|
121
|
+
if (summary) {
|
|
122
|
+
const summaryData = summarizeCrossSearchExpenditures(results);
|
|
123
|
+
return { content: [{ type: 'text', text: JSON.stringify(summaryData, null, 2) }] };
|
|
124
|
+
}
|
|
125
|
+
const effectiveLimit = limit === undefined ? 200 : limit;
|
|
126
|
+
const totalCount = enriched.length;
|
|
127
|
+
const limited = effectiveLimit > 0 ? enriched.slice(0, effectiveLimit) : enriched;
|
|
128
|
+
// Apply slim mode
|
|
129
|
+
const output = slim
|
|
130
|
+
? limited.map(({ address, ...rest }) => rest)
|
|
131
|
+
: limited;
|
|
132
|
+
const limitNote = effectiveLimit > 0 && totalCount > effectiveLimit
|
|
133
|
+
? `\nShowing ${effectiveLimit} of ${totalCount}. Use limit=0 for all.`
|
|
134
|
+
: '';
|
|
135
|
+
// Apply char budget
|
|
136
|
+
const { finalText, budgetWarning } = applyCharBudget(output);
|
|
137
|
+
return {
|
|
138
|
+
content: [{
|
|
139
|
+
type: 'text',
|
|
140
|
+
text: enriched.length === 0
|
|
141
|
+
? 'No expenditures found matching filters'
|
|
142
|
+
: `${totalCount} expenditure(s) found:${limitNote}${budgetWarning}\n${finalText}`,
|
|
143
|
+
}],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
return {
|
|
148
|
+
content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
149
|
+
isError: true,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
server.tool('search_contributions', 'Search contributions across ALL candidates statewide. At least one filter besides office required (office filter is broken server-side). Default limit 200 (0 for all). Use slim=true to strip address/occupation fields, summary=true for aggregated view. Responses auto-truncate at 60K chars.', {
|
|
154
|
+
candidate: z.string().optional().describe('Candidate name filter (partial match)'),
|
|
155
|
+
office: z.string().optional().describe('Office name filter (partial match)'),
|
|
156
|
+
contributor_name: z.string().optional().describe('Contributor/donor name filter (partial match)'),
|
|
157
|
+
year: z.number().optional().describe('Election year filter (e.g. 2024). Omit for all years.'),
|
|
158
|
+
amount: z.number().optional().describe('Minimum amount filter'),
|
|
159
|
+
limit: z.number().optional().describe('Max results to return (default 200, 0 for all).'),
|
|
160
|
+
slim: z.boolean().optional().describe('If true, strip address/occupation/group fields to reduce response size.'),
|
|
161
|
+
summary: z.boolean().optional().describe('If true, group by candidate and return totals instead of individual records.'),
|
|
162
|
+
}, async ({ candidate, office, contributor_name, year, amount, limit, slim, summary }) => {
|
|
163
|
+
try {
|
|
164
|
+
// Guard: office-only queries return ~79K unfiltered results (server ignores the filter)
|
|
165
|
+
const hasOtherFilter = candidate || contributor_name || year || amount;
|
|
166
|
+
if (office && !hasOtherFilter) {
|
|
167
|
+
return {
|
|
168
|
+
content: [{
|
|
169
|
+
type: 'text',
|
|
170
|
+
text: 'The office filter on search_contributions is broken server-side — the API ignores it and returns ~79K unfiltered results. Use list_filers_by_office to find candidates for that office, then get_contributions for each candidate individually.',
|
|
171
|
+
}],
|
|
172
|
+
isError: true,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const results = await searchContributions({
|
|
176
|
+
candidate,
|
|
177
|
+
office,
|
|
178
|
+
contributorName: contributor_name,
|
|
179
|
+
contributionYear: year,
|
|
180
|
+
amount,
|
|
181
|
+
});
|
|
182
|
+
const enriched = results.map(r => ({
|
|
183
|
+
...r,
|
|
184
|
+
normalizedOffice: normalizeOfficeName(r.officeName),
|
|
185
|
+
}));
|
|
186
|
+
if (summary) {
|
|
187
|
+
const summaryData = summarizeCrossSearchContributions(results);
|
|
188
|
+
return { content: [{ type: 'text', text: JSON.stringify(summaryData, null, 2) }] };
|
|
189
|
+
}
|
|
190
|
+
const effectiveLimit = limit === undefined ? 200 : limit;
|
|
191
|
+
const totalCount = enriched.length;
|
|
192
|
+
const limited = effectiveLimit > 0 ? enriched.slice(0, effectiveLimit) : enriched;
|
|
193
|
+
// Apply slim mode
|
|
194
|
+
const output = slim
|
|
195
|
+
? limited.map(({ contributorAddress, contributorOccupation, group, ...rest }) => rest)
|
|
196
|
+
: limited;
|
|
197
|
+
const limitNote = effectiveLimit > 0 && totalCount > effectiveLimit
|
|
198
|
+
? `\nShowing ${effectiveLimit} of ${totalCount}. Use limit=0 for all.`
|
|
199
|
+
: '';
|
|
200
|
+
// Apply char budget
|
|
201
|
+
const { finalText, budgetWarning } = applyCharBudget(output);
|
|
202
|
+
return {
|
|
203
|
+
content: [{
|
|
204
|
+
type: 'text',
|
|
205
|
+
text: enriched.length === 0
|
|
206
|
+
? 'No contributions found matching filters'
|
|
207
|
+
: `${totalCount} contribution(s) found:${limitNote}${budgetWarning}\n${finalText}`,
|
|
208
|
+
}],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
214
|
+
isError: true,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=cross-search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-search.js","sourceRoot":"","sources":["../../src/tools/cross-search.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { CampaignContribution } from '../types.js';
|
|
3
|
+
/** @internal — exported for testing */
|
|
4
|
+
export interface CandidateContributions {
|
|
5
|
+
candidateFilerId: number;
|
|
6
|
+
candidateName: string;
|
|
7
|
+
officeName: string;
|
|
8
|
+
contributions: CampaignContribution[];
|
|
9
|
+
error?: string;
|
|
10
|
+
}
|
|
11
|
+
interface DonorOverlapEntry {
|
|
12
|
+
donorName: string;
|
|
13
|
+
totalGiven: number;
|
|
14
|
+
candidateBreakdown: {
|
|
15
|
+
candidateName: string;
|
|
16
|
+
officeName: string;
|
|
17
|
+
amount: number;
|
|
18
|
+
count: number;
|
|
19
|
+
}[];
|
|
20
|
+
}
|
|
21
|
+
export declare function normalizeDonorName(name: string): string;
|
|
22
|
+
/** @internal — exported for testing */
|
|
23
|
+
export declare function computeOverlap(primary: CandidateContributions, comparisons: CandidateContributions[], exactMatch?: boolean): DonorOverlapEntry[];
|
|
24
|
+
export declare function registerOverlapTools(server: McpServer): void;
|
|
25
|
+
export {};
|