opensecrets-mcp 1.0.0 → 1.0.2
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/dist/api.d.ts +11 -0
- package/dist/api.js +96 -0
- package/dist/server.js +36 -1
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -10,3 +10,14 @@ export interface Candidate {
|
|
|
10
10
|
export declare function searchCandidates(query: string, cycle?: string): Promise<Candidate[]>;
|
|
11
11
|
export declare function candidateSummary(cid: string, cycle?: string): Promise<string>;
|
|
12
12
|
export declare function formatCandidate(c: Candidate, index?: number): string;
|
|
13
|
+
export declare function fecCandidateSearch(args: {
|
|
14
|
+
query: string;
|
|
15
|
+
office?: string;
|
|
16
|
+
state?: string;
|
|
17
|
+
party?: string;
|
|
18
|
+
limit?: number;
|
|
19
|
+
}): Promise<string>;
|
|
20
|
+
export declare function fecCommitteeSearch(args: {
|
|
21
|
+
query: string;
|
|
22
|
+
limit?: number;
|
|
23
|
+
}): Promise<string>;
|
package/dist/api.js
CHANGED
|
@@ -61,3 +61,99 @@ export function formatCandidate(c, index) {
|
|
|
61
61
|
const prefix = index !== undefined ? `${index + 1}. ` : "";
|
|
62
62
|
return `${prefix}[${c.id ?? "?"}] ${c.name ?? "(unnamed)"}${c.party ? ` (${c.party}${c.state ? `-${c.state}` : ""})` : ""}${c.chamber ? ` — ${c.chamber}` : ""}`;
|
|
63
63
|
}
|
|
64
|
+
// ---- muxB tools:
|
|
65
|
+
// OpenFEC, the FEC's own open API. DEMO_KEY is the placeholder the FEC
|
|
66
|
+
// documents for public use and reaches the candidate and committee indexes.
|
|
67
|
+
const FEC_BASE = "https://api.open.fec.gov/v1";
|
|
68
|
+
const FEC_DEMO_KEY = "DEMO_KEY";
|
|
69
|
+
async function fecGet(path, params = []) {
|
|
70
|
+
const qs = new URLSearchParams([...params, ["api_key", FEC_DEMO_KEY]]);
|
|
71
|
+
const res = await fetch(`${FEC_BASE}${path}?${qs.toString()}`, {
|
|
72
|
+
headers: { "User-Agent": "opensecrets-mcp/1.0", Accept: "application/json" },
|
|
73
|
+
signal: AbortSignal.timeout(25000),
|
|
74
|
+
});
|
|
75
|
+
if (res.status === 429)
|
|
76
|
+
throw new OpenSecretsError("OpenFEC DEMO_KEY is limited to 40 calls an hour; wait or register a key at api.data.gov.");
|
|
77
|
+
if (res.status === 422) {
|
|
78
|
+
const detail = (await res.json().catch(() => ({}))).message;
|
|
79
|
+
throw new OpenSecretsError(`OpenFEC rejected the query${detail ? `: ${String(detail).slice(0, 200)}` : "."}`);
|
|
80
|
+
}
|
|
81
|
+
if (!res.ok)
|
|
82
|
+
throw new OpenSecretsError(`OpenFEC error ${res.status}`);
|
|
83
|
+
return (await res.json());
|
|
84
|
+
}
|
|
85
|
+
function fecCycles(row) {
|
|
86
|
+
const list = Array.isArray(row.cycles) ? row.cycles : [];
|
|
87
|
+
if (list.length === 0)
|
|
88
|
+
return "cycles not reported";
|
|
89
|
+
const shown = list.slice(0, 6).map(String);
|
|
90
|
+
return shown.join(", ") + (list.length > 6 ? `, +${list.length - 6} more` : "");
|
|
91
|
+
}
|
|
92
|
+
function fecCommitteeLine(c) {
|
|
93
|
+
const designation = c.designation_full ?? c.designation ?? "";
|
|
94
|
+
return ` ${c.committee_id ?? "?"} ${c.name ?? "(unnamed)"}${designation ? ` [${designation}]` : ""}${c.committee_type_full ? ` (${c.committee_type_full})` : ""}`;
|
|
95
|
+
}
|
|
96
|
+
export async function fecCandidateSearch(args) {
|
|
97
|
+
const q = (args.query ?? "").trim();
|
|
98
|
+
if (!q)
|
|
99
|
+
throw new OpenSecretsError("Query is empty.");
|
|
100
|
+
const limit = Math.min(Math.max(args.limit ?? 5, 1), 20);
|
|
101
|
+
const params = [["q", q], ["per_page", String(limit)], ["sort", "-receipts"]];
|
|
102
|
+
const office = (args.office ?? "").trim().toUpperCase();
|
|
103
|
+
if (office)
|
|
104
|
+
params.push(["office", office]);
|
|
105
|
+
const state = (args.state ?? "").trim().toUpperCase();
|
|
106
|
+
if (state)
|
|
107
|
+
params.push(["state", state]);
|
|
108
|
+
const party = (args.party ?? "").trim().toUpperCase();
|
|
109
|
+
if (party)
|
|
110
|
+
params.push(["party", party]);
|
|
111
|
+
const data = await fecGet("/candidates/search/", params);
|
|
112
|
+
const rows = Array.isArray(data.results) ? data.results : [];
|
|
113
|
+
if (rows.length === 0)
|
|
114
|
+
return `No FEC candidate records match "${q}".`;
|
|
115
|
+
const total = (data.pagination ?? {}).count;
|
|
116
|
+
return [
|
|
117
|
+
`FEC candidate index for "${q}"${office ? `, office ${office}` : ""}${state ? `, ${state}` : ""}${party ? `, ${party}` : ""} - ${Number(total ?? 0).toLocaleString()} match, showing ${rows.length}:`,
|
|
118
|
+
...rows.map((r, i) => {
|
|
119
|
+
const committees = Array.isArray(r.principal_committees) ? r.principal_committees : [];
|
|
120
|
+
return [
|
|
121
|
+
`${i + 1}. ${r.name ?? "(unnamed)"} [${r.candidate_id ?? "?"}]`,
|
|
122
|
+
` ${r.office_full ?? r.office ?? "office not reported"}${r.district && r.district !== "00" ? `-${r.district}` : ""} · ${r.state ?? "?"} · ${r.party_full ?? r.party ?? "party not reported"}`,
|
|
123
|
+
` status ${r.candidate_status ?? "not reported"}${r.incumbent_challenge_full ? `, ${r.incumbent_challenge_full}` : ""} · raised funds: ${r.has_raised_funds === true ? "yes" : r.has_raised_funds === false ? "no" : "not reported"}`,
|
|
124
|
+
` cycles: ${fecCycles(r)}`,
|
|
125
|
+
committees.length ? ` principal committees:` : " principal committees: not reported",
|
|
126
|
+
...committees.slice(0, 3).map(fecCommitteeLine),
|
|
127
|
+
committees.length > 3 ? ` ...and ${committees.length - 3} more committees` : "",
|
|
128
|
+
].filter(Boolean).join("\n");
|
|
129
|
+
}),
|
|
130
|
+
].join("\n");
|
|
131
|
+
}
|
|
132
|
+
export async function fecCommitteeSearch(args) {
|
|
133
|
+
const q = (args.query ?? "").trim();
|
|
134
|
+
if (!q)
|
|
135
|
+
throw new OpenSecretsError("Query is empty.");
|
|
136
|
+
const limit = Math.min(Math.max(args.limit ?? 5, 1), 20);
|
|
137
|
+
const data = await fecGet("/committees/", [["q", q], ["per_page", String(limit)], ["sort", "-receipts"]]);
|
|
138
|
+
const rows = Array.isArray(data.results) ? data.results : [];
|
|
139
|
+
if (rows.length === 0)
|
|
140
|
+
return `No FEC committees match "${q}".`;
|
|
141
|
+
const total = (data.pagination ?? {}).count;
|
|
142
|
+
return [
|
|
143
|
+
`FEC committee index for "${q}" - ${Number(total ?? 0).toLocaleString()} match, showing ${rows.length}:`,
|
|
144
|
+
...rows.map((r, i) => {
|
|
145
|
+
const agent = [r.designated_agent_name, r.designated_agent_city, r.designated_agent_state].filter(Boolean).join(", ");
|
|
146
|
+
const sponsor = Array.isArray(r.sponsor_candidate_list) ? r.sponsor_candidate_list.map((s) => String(s.name ?? s.candidate_id ?? "?")) : [];
|
|
147
|
+
const candidates = Array.isArray(r.candidate_ids) ? r.candidate_ids.map(String) : [];
|
|
148
|
+
return [
|
|
149
|
+
`${i + 1}. ${r.name ?? "(unnamed committee)"} [${r.committee_id ?? "?"}]`,
|
|
150
|
+
` ${r.committee_type_full ?? "type not reported"}${r.designation_full ? ` · ${r.designation_full}` : ""}${r.party_full ? ` · ${r.party_full}` : ""}${r.state ? ` · ${r.state}` : ""}`,
|
|
151
|
+
` treasurer ${r.treasurer_name ?? "not reported"}${agent ? ` (designated agent ${agent})` : ""} · filing ${r.filing_frequency ?? "not reported"}`,
|
|
152
|
+
` cycles: ${fecCycles(r)} · first file ${r.first_file_date ?? "not reported"} · last file ${r.last_file_date ?? "not reported"}`,
|
|
153
|
+
sponsor.length ? ` sponsored by: ${sponsor.slice(0, 3).join(", ")}${sponsor.length > 3 ? `, +${sponsor.length - 3} more` : ""}` : "",
|
|
154
|
+
candidates.length ? ` linked candidate ids: ${candidates.slice(0, 4).join(", ")}${candidates.length > 4 ? `, +${candidates.length - 4} more` : ""}` : "",
|
|
155
|
+
r.affiliated_committee_name ? ` affiliated with: ${r.affiliated_committee_name}` : "",
|
|
156
|
+
].filter(Boolean).join("\n");
|
|
157
|
+
}),
|
|
158
|
+
].join("\n");
|
|
159
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { candidateSummary, formatCandidate, OpenSecretsError, searchCandidates } from "./api.js";
|
|
3
|
+
import { candidateSummary, formatCandidate, OpenSecretsError, searchCandidates, fecCandidateSearch, fecCommitteeSearch } from "./api.js";
|
|
4
4
|
const text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
5
5
|
const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
|
|
6
6
|
const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
|
|
@@ -43,6 +43,41 @@ export function createServer() {
|
|
|
43
43
|
return textError(errorMessage(e));
|
|
44
44
|
}
|
|
45
45
|
});
|
|
46
|
+
server.registerTool("fec_candidate_search", {
|
|
47
|
+
title: "Search the FEC candidate index",
|
|
48
|
+
description: "Federal campaign-finance candidate index by name fragment, with office, state and party filters, plus each candidate's principal committees and cycle history.",
|
|
49
|
+
inputSchema: z.object({
|
|
50
|
+
query: z.string().describe("Name fragment, e.g. 'Sanders'"),
|
|
51
|
+
office: z.string().default("").describe("Office code: H for House, S for Senate, P for President, empty = all"),
|
|
52
|
+
state: z.string().default("").describe("Two-letter state or 'US', empty = all"),
|
|
53
|
+
party: z.string().default("").describe("DEM, REP, IND or empty = all"),
|
|
54
|
+
limit: z.number().int().min(1).max(20).default(5),
|
|
55
|
+
}),
|
|
56
|
+
annotations: READ_ONLY,
|
|
57
|
+
}, async ({ query, office, state, party, limit }) => {
|
|
58
|
+
try {
|
|
59
|
+
return text(await fecCandidateSearch({ query, office, state, party, limit }));
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
return textError(errorMessage(e));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
server.registerTool("fec_committee_search", {
|
|
66
|
+
title: "Search the FEC committee index",
|
|
67
|
+
description: "Federal campaign committees by name keyword: type, designation, party, treasurer, filing frequency, cycle span and sponsored candidates.",
|
|
68
|
+
inputSchema: z.object({
|
|
69
|
+
query: z.string().describe("Committee name fragment, e.g. 'for Senate'"),
|
|
70
|
+
limit: z.number().int().min(1).max(20).default(5),
|
|
71
|
+
}),
|
|
72
|
+
annotations: READ_ONLY,
|
|
73
|
+
}, async ({ query, limit }) => {
|
|
74
|
+
try {
|
|
75
|
+
return text(await fecCommitteeSearch({ query, limit }));
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
return textError(errorMessage(e));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
46
81
|
return server;
|
|
47
82
|
}
|
|
48
83
|
function errorMessage(e) {
|
package/package.json
CHANGED