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/src/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * gov-data-mcp — MCP server exposing 95 published US government open-data tools.
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 95 actors; handing
8
- * an agent 95 tool definitions degrades tool selection and blows up the context
9
- * of every request. These are the highest-signal ones (revenue-proven plus the
10
- * flagships); the remaining 80 stay reachable through search/describe/run.
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: 'search_gov_data_tools',
29
- DESCRIBE: 'describe_gov_data_tool',
30
- RUN: 'run_gov_data_tool',
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. Billed to your own Apify account at the tool's published rate.`,
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: 'Input object matching the schema returned by describe_gov_data_tool.' },
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
- return { ok: false, error: `Unknown tool "${toolName}".` };
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. */