gov-data-mcp 1.0.3 → 1.1.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 +11 -11
- package/package.json +3 -3
- package/src/catalog.json +2156 -17
- package/src/index.js +4 -1
- package/src/tools.js +134 -13
package/src/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* gov-data-mcp — MCP server exposing
|
|
3
|
+
* gov-data-mcp — MCP server exposing every published US government open-data
|
|
4
|
+
* Actor in the bundled catalog. The count is deliberately NOT written down
|
|
5
|
+
* here; it is read from src/catalog.json at runtime, because a number in a
|
|
6
|
+
* comment rots the moment a batch ships and nothing fails when it does.
|
|
4
7
|
*
|
|
5
8
|
* Wiring only. All decision logic lives in src/tools.js (pure, offline-tested)
|
|
6
9
|
* and src/apify.js (fetch injected). A test asserts this file holds no logic
|
package/src/tools.js
CHANGED
|
@@ -4,10 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Actors promoted to first-class MCP tools. The catalog holds
|
|
8
|
-
* an agent
|
|
9
|
-
* of every request. These are the
|
|
10
|
-
*
|
|
7
|
+
* Actors promoted to first-class MCP tools. The catalog holds well over a
|
|
8
|
+
* hundred actors; handing an agent that many tool definitions degrades tool
|
|
9
|
+
* selection and blows up the context of every request. These are the
|
|
10
|
+
* highest-signal ones (revenue-proven plus the flagships); every other actor
|
|
11
|
+
* stays reachable through search/describe/run.
|
|
12
|
+
*
|
|
13
|
+
* No count is written down in this file on purpose — the catalog is the only
|
|
14
|
+
* source of truth for how many there are, and prose counts here rotted from
|
|
15
|
+
* 95 to 109 to 114 to 116 without a single test going red.
|
|
11
16
|
*/
|
|
12
17
|
export const FEATURED = [
|
|
13
18
|
'site-due-diligence-bundle',
|
|
@@ -24,10 +29,29 @@ export const FEATURED = [
|
|
|
24
29
|
'license-verifier',
|
|
25
30
|
];
|
|
26
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Meta tools are hyphen-case for one reason: so that EVERY tool this server
|
|
34
|
+
* exposes follows a single rule, and for catalog tools the rule is stronger
|
|
35
|
+
* still — the tool name IS the Apify slug, which is also the value you pass to
|
|
36
|
+
* describe/run and the tail of the Store URL. The old snake_case meta names
|
|
37
|
+
* split the surface arbitrarily in two, which an independent review scored 2/5,
|
|
38
|
+
* and the alternative (snake_case everywhere) would have made 12 tool names
|
|
39
|
+
* differ from the slug they resolve to, trading one inconsistency for another.
|
|
40
|
+
*
|
|
41
|
+
* Renamed in v1.1.0. The pre-1.1 names are recognised in resolveCall and
|
|
42
|
+
* answered with an explicit rename notice rather than silently aliased — a
|
|
43
|
+
* caller with a hardcoded name deserves to be told, not quietly patched.
|
|
44
|
+
*/
|
|
27
45
|
export const META_TOOLS = {
|
|
28
|
-
SEARCH: '
|
|
29
|
-
DESCRIBE: '
|
|
30
|
-
RUN: '
|
|
46
|
+
SEARCH: 'search-gov-data-tools',
|
|
47
|
+
DESCRIBE: 'describe-gov-data-tool',
|
|
48
|
+
RUN: 'run-gov-data-tool',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const RENAMED_IN_1_1 = {
|
|
52
|
+
search_gov_data_tools: 'search-gov-data-tools',
|
|
53
|
+
describe_gov_data_tool: 'describe-gov-data-tool',
|
|
54
|
+
run_gov_data_tool: 'run-gov-data-tool',
|
|
31
55
|
};
|
|
32
56
|
|
|
33
57
|
/** MCP tool names must match ^[a-zA-Z0-9_-]{1,64}$. Slugs already do. */
|
|
@@ -47,13 +71,95 @@ function truncate(s, n) {
|
|
|
47
71
|
return s.length <= n ? s : s.slice(0, n - 1).trimEnd() + '…';
|
|
48
72
|
}
|
|
49
73
|
|
|
74
|
+
/**
|
|
75
|
+
* MCP tool annotations — the machine-readable half of "what does this do to the
|
|
76
|
+
* world before I call it".
|
|
77
|
+
*
|
|
78
|
+
* WHY `readOnlyHint: false` ON THE DATA TOOLS, despite them only ever reading:
|
|
79
|
+
* they never write to any government system, but every call starts a metered
|
|
80
|
+
* run on the caller's own Apify account and costs them money. `readOnlyHint`
|
|
81
|
+
* means "does not modify its environment", and an agent that reads `true`
|
|
82
|
+
* reasonably concludes the call is free to make speculatively — which is the
|
|
83
|
+
* one wrong conclusion that costs the caller real money. Billing is a side
|
|
84
|
+
* effect. We declare it rather than hide it behind a comfortable `true`, and
|
|
85
|
+
* `destructiveHint: false` carries the other half of the truth: nothing is
|
|
86
|
+
* destroyed, no external state is altered, and a call is always safe to make
|
|
87
|
+
* once you accept its cost.
|
|
88
|
+
*
|
|
89
|
+
* `idempotentHint: false` because these read live sources — the same input
|
|
90
|
+
* tomorrow can legitimately return different rows, and a caller must not cache
|
|
91
|
+
* a flood or sanctions answer as if it were fixed.
|
|
92
|
+
*/
|
|
93
|
+
export const ANNOTATIONS = {
|
|
94
|
+
/** Reads the bundled catalog. No network, no run, no charge. */
|
|
95
|
+
CATALOG_LOCAL: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
96
|
+
/** Starts a billed Apify run against a live government source. */
|
|
97
|
+
BILLED_LIVE_READ: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** Format a price so an agent can compare tools without doing arithmetic. */
|
|
101
|
+
export function priceLine(pricing) {
|
|
102
|
+
if (!pricing) return 'Price unavailable for this tool — check its Store page before calling.';
|
|
103
|
+
const per1k = pricing.usdPer1000;
|
|
104
|
+
const cheapest = pricing.tierDiscountsUsdPerUnit
|
|
105
|
+
? Math.min(...Object.values(pricing.tierDiscountsUsdPerUnit))
|
|
106
|
+
: pricing.usdPerUnit;
|
|
107
|
+
const discount = cheapest < pricing.usdPerUnit
|
|
108
|
+
? ` Lower on paid Apify plans, down to $${(cheapest * 1000).toFixed(2)} per 1,000.`
|
|
109
|
+
: '';
|
|
110
|
+
return `$${pricing.usdPerUnit} per ${pricing.unit} ($${per1k} per 1,000).${discount}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function costNote(pricing) {
|
|
114
|
+
return 'COST AND SIDE EFFECTS: read-only with respect to the government source — it never writes to any external system — '
|
|
115
|
+
+ `but each call starts a metered run on YOUR Apify account, billed ${priceLine(pricing)} `
|
|
116
|
+
+ 'Nothing is charged when a run fails.';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The generic, price-free wording used by the run tool, which can run anything. */
|
|
120
|
+
export const COST_NOTE =
|
|
121
|
+
'COST AND SIDE EFFECTS: read-only with respect to the government source — it never writes to any external system — '
|
|
122
|
+
+ 'but each call starts a metered run on YOUR Apify account, billed per result row at the rate this tool reports. '
|
|
123
|
+
+ 'Call describe-gov-data-tool first to see the exact price before running anything. '
|
|
124
|
+
+ 'Nothing is charged when a run fails.';
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Routing notes for tools whose scope genuinely overlaps.
|
|
128
|
+
*
|
|
129
|
+
* Four of the featured tools answer adjacent questions about the same
|
|
130
|
+
* coordinate, and an agent handed all four with no guidance picks by keyword
|
|
131
|
+
* overlap rather than by which regulatory question is actually being asked.
|
|
132
|
+
* These sentences say, in each tool's own description, when it is the wrong
|
|
133
|
+
* choice — which is the part a bare capability blurb never tells you.
|
|
134
|
+
*/
|
|
135
|
+
export const ROUTING = {
|
|
136
|
+
'site-due-diligence-bundle':
|
|
137
|
+
'CHOOSE THIS when you want one combined go / caution / no-go verdict for a coordinate across many unrelated layers. '
|
|
138
|
+
+ 'It is NOT an ASTM records review: its contamination layer reads RCRA and TRI through ECHO only and omits coordinate-less Superfund records. '
|
|
139
|
+
+ 'For a contamination-first question use epa-contaminated-site-screener.',
|
|
140
|
+
'epa-contaminated-site-screener':
|
|
141
|
+
'CHOOSE THIS for the ASTM E1527-21 Phase I records search at regulation distances around one or more properties. '
|
|
142
|
+
+ 'For a single combined verdict across twenty unrelated layers use site-due-diligence-bundle; for drinking-water quality use epa-drinking-water-quality-screener.',
|
|
143
|
+
'fws-wetlands-proximity-screener':
|
|
144
|
+
'CHOOSE THIS for National Wetlands Inventory polygons and their decode columns within a radius. '
|
|
145
|
+
+ 'It does NOT answer Clean Water Act §404 jurisdiction — for surface-water features and relative permanence use nhd-surface-water-404-screener. The two are usually needed together.',
|
|
146
|
+
'nhd-surface-water-404-screener':
|
|
147
|
+
'CHOOSE THIS for Clean Water Act §404 surface-water screening — streams, waterbodies and their relative permanence. '
|
|
148
|
+
+ 'For mapped wetland polygons use fws-wetlands-proximity-screener.',
|
|
149
|
+
'epa-drinking-water-quality-screener':
|
|
150
|
+
'CHOOSE THIS for public water-system quality: SDWA violations, lead 90th-percentile results and PFAS occurrence. '
|
|
151
|
+
+ 'It is NOT a property contamination screen — for soil and groundwater records at a site use epa-contaminated-site-screener.',
|
|
152
|
+
};
|
|
153
|
+
|
|
50
154
|
export function featuredToolDefinitions(index) {
|
|
51
155
|
return FEATURED.filter(s => index.bySlug.has(s)).map(slug => {
|
|
52
156
|
const a = index.bySlug.get(slug);
|
|
157
|
+
const routing = ROUTING[slug] ? ` ${ROUTING[slug]}` : '';
|
|
53
158
|
return {
|
|
54
159
|
name: toolNameFor(slug),
|
|
55
|
-
description: `${a.title}. ${truncate(a.description, 400)} Reads live from the official government source. Store page: ${a.storeUrl}`,
|
|
160
|
+
description: `${a.title}. ${truncate(a.description, 400)}${routing} Reads live from the official government source. ${costNote(a.pricing)} Store page: ${a.storeUrl}`,
|
|
56
161
|
inputSchema: a.inputSchema,
|
|
162
|
+
annotations: { title: a.title, ...ANNOTATIONS.BILLED_LIVE_READ },
|
|
57
163
|
};
|
|
58
164
|
});
|
|
59
165
|
}
|
|
@@ -63,7 +169,8 @@ export function metaToolDefinitions(index) {
|
|
|
63
169
|
return [
|
|
64
170
|
{
|
|
65
171
|
name: META_TOOLS.SEARCH,
|
|
66
|
-
description: `Search the full catalog of ${n} US government data tools by keyword, agency, or topic (e.g. "wetlands", "FDIC", "flood", "drone airspace", "business licenses"). Returns matching tool names with descriptions. Use this first when the task is not covered by one of the dedicated tools above.`,
|
|
172
|
+
description: `Search the full catalog of ${n} US government data tools by keyword, agency, or topic (e.g. "wetlands", "FDIC", "flood", "drone airspace", "business licenses"). Returns matching tool names with descriptions. Use this first when the task is not covered by one of the dedicated tools above. FREE: reads a catalog bundled with this server — no network call, no run, nothing charged.`,
|
|
173
|
+
annotations: { title: 'Search the government data catalog', ...ANNOTATIONS.CATALOG_LOCAL },
|
|
67
174
|
inputSchema: {
|
|
68
175
|
type: 'object',
|
|
69
176
|
properties: {
|
|
@@ -75,7 +182,8 @@ export function metaToolDefinitions(index) {
|
|
|
75
182
|
},
|
|
76
183
|
{
|
|
77
184
|
name: META_TOOLS.DESCRIBE,
|
|
78
|
-
description: `Return the full input schema and documentation for any one of the ${n} tools in the catalog. Call this before ${META_TOOLS.RUN} so the input is correctly shaped.`,
|
|
185
|
+
description: `Return the full input schema and documentation for any one of the ${n} tools in the catalog. Call this before ${META_TOOLS.RUN} so the input is correctly shaped. FREE: reads a catalog bundled with this server — no network call, no run, nothing charged.`,
|
|
186
|
+
annotations: { title: 'Describe one government data tool', ...ANNOTATIONS.CATALOG_LOCAL },
|
|
79
187
|
inputSchema: {
|
|
80
188
|
type: 'object',
|
|
81
189
|
properties: {
|
|
@@ -86,12 +194,14 @@ export function metaToolDefinitions(index) {
|
|
|
86
194
|
},
|
|
87
195
|
{
|
|
88
196
|
name: META_TOOLS.RUN,
|
|
89
|
-
description: `Run any one of the ${n} catalog tools with the given input and return its rows.
|
|
197
|
+
description: `Run any one of the ${n} catalog tools with the given input and return its rows. Call ${META_TOOLS.DESCRIBE} first to shape the input. `
|
|
198
|
+
+ `${COST_NOTE} A run that FAILS returns an error and no rows rather than an empty result, so a zero-row answer here always means the source was reached and genuinely matched nothing.`,
|
|
199
|
+
annotations: { title: 'Run any government data tool', ...ANNOTATIONS.BILLED_LIVE_READ },
|
|
90
200
|
inputSchema: {
|
|
91
201
|
type: 'object',
|
|
92
202
|
properties: {
|
|
93
203
|
tool: { type: 'string', description: 'The tool name to run, e.g. "usgs-seismic-design-screener".' },
|
|
94
|
-
input: { type: 'object', description:
|
|
204
|
+
input: { type: 'object', description: `Input object matching the schema returned by ${META_TOOLS.DESCRIBE}.` },
|
|
95
205
|
maxItems: { type: 'integer', description: 'Maximum rows to return. Default 200.', minimum: 1, maximum: 1000 },
|
|
96
206
|
},
|
|
97
207
|
required: ['tool', 'input'],
|
|
@@ -123,6 +233,9 @@ export function searchCatalog(index, query, limit = 10) {
|
|
|
123
233
|
title: x.actor.title,
|
|
124
234
|
description: truncate(x.actor.description, 300),
|
|
125
235
|
categories: x.actor.categories,
|
|
236
|
+
// Price rides along on every search hit so an agent can weigh cost while
|
|
237
|
+
// choosing, rather than discovering it only after it has already called.
|
|
238
|
+
usdPer1000Results: x.actor.pricing ? x.actor.pricing.usdPer1000 : null,
|
|
126
239
|
storeUrl: x.actor.storeUrl,
|
|
127
240
|
}));
|
|
128
241
|
}
|
|
@@ -142,6 +255,9 @@ export function describeTool(index, name) {
|
|
|
142
255
|
title: actor.title,
|
|
143
256
|
description: actor.description,
|
|
144
257
|
categories: actor.categories,
|
|
258
|
+
pricing: actor.pricing
|
|
259
|
+
? { ...actor.pricing, summary: priceLine(actor.pricing) }
|
|
260
|
+
: null,
|
|
145
261
|
storeUrl: actor.storeUrl,
|
|
146
262
|
inputSchema: actor.inputSchema,
|
|
147
263
|
};
|
|
@@ -159,7 +275,12 @@ export function resolveCall(index, toolName, args) {
|
|
|
159
275
|
}
|
|
160
276
|
return { ok: true, slug, input: (args && args.input) || {}, maxItems: args && args.maxItems };
|
|
161
277
|
}
|
|
162
|
-
|
|
278
|
+
// A caller with a hardcoded pre-1.1 name gets told what happened rather than
|
|
279
|
+
// silently aliased, so the hardcode gets fixed instead of quietly persisting.
|
|
280
|
+
if (RENAMED_IN_1_1[toolName]) {
|
|
281
|
+
return { ok: false, error: `"${toolName}" was renamed to "${RENAMED_IN_1_1[toolName]}" in gov-data-mcp v1.1.0, so that every tool on this server uses one naming convention. Call "${RENAMED_IN_1_1[toolName]}" instead — the arguments are unchanged.` };
|
|
282
|
+
}
|
|
283
|
+
return { ok: false, error: `Unknown tool "${toolName}". Call ${META_TOOLS.SEARCH} to list what this server offers.` };
|
|
163
284
|
}
|
|
164
285
|
|
|
165
286
|
/** Shape a run result into MCP text content. Never collapses a failure into an empty answer. */
|