gov-data-mcp 1.2.5 → 1.2.6
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 +21 -1
- package/package.json +2 -2
- package/src/catalog.json +7038 -127
- package/src/index.js +3 -3
- package/src/tools.js +93 -5
package/src/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
18
18
|
import { dirname, join } from 'node:path';
|
|
19
19
|
|
|
20
20
|
import { createClient, ApifyError } from './apify.js';
|
|
21
|
-
import { indexCatalog, listTools, searchCatalog, describeTool, resolveCall, formatRunResult, META_TOOLS } from './tools.js';
|
|
21
|
+
import { indexCatalog, listTools, searchCatalog, describeTool, resolveCall, formatRunResult, serverInstructions, META_TOOLS } from './tools.js';
|
|
22
22
|
import { parseCliArgs, resolvePreset, describePresets, PRESET_ENV } from './presets.js';
|
|
23
23
|
|
|
24
24
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -51,7 +51,7 @@ const identity = chosen.preset
|
|
|
51
51
|
|
|
52
52
|
const server = new Server(
|
|
53
53
|
identity,
|
|
54
|
-
{ capabilities: { tools: {} } },
|
|
54
|
+
{ capabilities: { tools: {} }, instructions: serverInstructions(index) },
|
|
55
55
|
);
|
|
56
56
|
|
|
57
57
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: listTools(index) }));
|
|
@@ -82,7 +82,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
82
82
|
const result = await client.runActor(`${catalog.owner}/${call.slug}`, call.input, {
|
|
83
83
|
maxItems: call.maxItems ?? 200,
|
|
84
84
|
});
|
|
85
|
-
const formatted = formatRunResult(call.slug, result);
|
|
85
|
+
const formatted = formatRunResult(call.slug, result, index.bySlug.get(call.slug));
|
|
86
86
|
return text(formatted.text, formatted.isError);
|
|
87
87
|
} catch (err) {
|
|
88
88
|
if (err instanceof ApifyError) return text(`${err.message}`, true);
|
package/src/tools.js
CHANGED
|
@@ -54,6 +54,78 @@ export const RENAMED_IN_1_1 = {
|
|
|
54
54
|
run_gov_data_tool: 'run-gov-data-tool',
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Server-level guide, sent in the MCP `initialize` response as `instructions`.
|
|
59
|
+
*
|
|
60
|
+
* Written because the measured failure mode is agents GUESSING inputs: an agent
|
|
61
|
+
* that calls a screener with no state, no coordinates and no name starts a
|
|
62
|
+
* metered run that refuses the question (or matches nothing) and costs the
|
|
63
|
+
* caller an actor-start fee for no answer. Every rule below targets that.
|
|
64
|
+
*/
|
|
65
|
+
export function serverInstructions(index) {
|
|
66
|
+
const n = index.actors.length;
|
|
67
|
+
return [
|
|
68
|
+
`gov-data-mcp: ${n} tools over official US government open data (EPA, FEMA, USGS, FDIC, SEC, state licensing boards and registries, ...).${index.scopeNote || ''}`,
|
|
69
|
+
'',
|
|
70
|
+
'HOW TO CALL THESE TOOLS WELL',
|
|
71
|
+
`1. Find the tool: ${META_TOOLS.SEARCH} (free). 2. Read it: ${META_TOOLS.DESCRIBE} (free) - it returns mustSupply, exampleInput and run-verified examples. 3. Run it.`,
|
|
72
|
+
'4. Start from exampleInput or one of the examples and change only the values you need. These inputs are verified to return rows; a hand-built input usually is not.',
|
|
73
|
+
'5. Always SCOPE the question: a state, county, coordinates, a name, an NPI, a date window. Unscoped calls are refused by most tools or bill for whole national lists.',
|
|
74
|
+
'6. Use real values the source knows: two-letter state codes unless the schema says otherwise, decimal-degree lat/lon, ISO dates (YYYY-MM-DD). Never invent enum values - use the enum in the schema.',
|
|
75
|
+
'7. Keep the first call small (low maxResults / few assets) and widen once the shape works. Each call bills per row (see the price in the tool description).',
|
|
76
|
+
'',
|
|
77
|
+
'READING RESULTS',
|
|
78
|
+
'- run_status other than SUCCEEDED means the question was NOT answered. Read `note` (the tool explains what was wrong), fix the input, and do not retry the identical input.',
|
|
79
|
+
'- SUCCEEDED with zero rows means the source was reached and matched nothing for exactly that input. Before concluding "none exist", check the scope (radius, date window, spelling, state).',
|
|
80
|
+
'- null in a field means "not checked / not published", never "no". Many rows carry per-source status fields; read them before stating a negative.',
|
|
81
|
+
].join('\n');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Free, local pre-flight check run BEFORE a metered call: required fields the
|
|
86
|
+
* caller must supply (required with no server default) and closed vocabularies.
|
|
87
|
+
* Apify would reject these too, but only after the agent has learned nothing
|
|
88
|
+
* useful; answering here returns the example to copy instead.
|
|
89
|
+
*/
|
|
90
|
+
export function preflight(actor, input) {
|
|
91
|
+
const problems = [];
|
|
92
|
+
const props = (actor.inputSchema && actor.inputSchema.properties) || {};
|
|
93
|
+
const given = input && typeof input === 'object' ? input : {};
|
|
94
|
+
for (const k of actor.mustSupply || []) {
|
|
95
|
+
const v = given[k];
|
|
96
|
+
if (v === undefined || v === null || v === '' || (Array.isArray(v) && v.length === 0)) problems.push(`"${k}" is required and was not supplied`);
|
|
97
|
+
}
|
|
98
|
+
for (const [k, v] of Object.entries(given)) {
|
|
99
|
+
const p = props[k];
|
|
100
|
+
if (!p) { problems.push(`"${k}" is not an input of this tool (known inputs: ${Object.keys(props).join(', ')})`); continue; }
|
|
101
|
+
if (Array.isArray(p.enum) && v !== undefined && v !== null && !p.enum.includes(v)) problems.push(`"${k}" must be one of ${JSON.stringify(p.enum)}, got ${JSON.stringify(v)}`);
|
|
102
|
+
const itemEnum = p.items && Array.isArray(p.items.enum) ? p.items.enum : null;
|
|
103
|
+
if (itemEnum && Array.isArray(v)) {
|
|
104
|
+
const bad = v.filter(x => !itemEnum.includes(x));
|
|
105
|
+
if (bad.length) problems.push(`"${k}" items must be from ${JSON.stringify(itemEnum)}, got ${JSON.stringify(bad)}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return problems;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** What an agent should do next, attached to every non-SUCCEEDED or zero-row result. */
|
|
112
|
+
export function guidanceFor(actor, kind) {
|
|
113
|
+
const ex = actor && actor.exampleInput ? actor.exampleInput : null;
|
|
114
|
+
const examples = actor && Array.isArray(actor.examples) ? actor.examples : [];
|
|
115
|
+
const base = kind === 'failed'
|
|
116
|
+
? 'The question was not answered and no result rows were billed. Read `note` above for the reason, fix the input, and do not resend the same input.'
|
|
117
|
+
: kind === 'preflight'
|
|
118
|
+
? 'Nothing was run and nothing was charged. Fix the input and call again.'
|
|
119
|
+
: 'Zero rows is an answer only for exactly this input. Before concluding nothing exists, check the scope: spelling, state code, radius, date window, mode.';
|
|
120
|
+
return {
|
|
121
|
+
next_step: `${base} Compare your input with example_input (verified to return rows) and change only the values you need.`,
|
|
122
|
+
must_supply: actor ? (actor.mustSupply || []) : [],
|
|
123
|
+
example_input: ex,
|
|
124
|
+
examples: examples.length ? examples : undefined,
|
|
125
|
+
describe: actor ? `${META_TOOLS.DESCRIBE} {"tool":"${actor.slug}"}` : undefined,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
57
129
|
/** MCP tool names must match ^[a-zA-Z0-9_-]{1,64}$. Slugs already do. */
|
|
58
130
|
export function toolNameFor(slug) {
|
|
59
131
|
return slug;
|
|
@@ -204,7 +276,7 @@ export function featuredToolDefinitions(index) {
|
|
|
204
276
|
const routing = routingNoteFor(index, slug);
|
|
205
277
|
return {
|
|
206
278
|
name: toolNameFor(slug),
|
|
207
|
-
description: `${a.title}. ${truncate(a.description, 400)}${routing} Reads live from the official government source. ${costNote(a.pricing)} Store page: ${a.storeUrl}`,
|
|
279
|
+
description: `${a.title}. ${truncate(a.description, 400)}${routing} Reads live from the official government source. Call ${META_TOOLS.DESCRIBE} (free) for a verified example input. ${costNote(a.pricing)} Store page: ${a.storeUrl}`,
|
|
208
280
|
inputSchema: a.inputSchema,
|
|
209
281
|
annotations: { title: a.title, ...ANNOTATIONS.BILLED_LIVE_READ },
|
|
210
282
|
};
|
|
@@ -242,7 +314,7 @@ export function metaToolDefinitions(index) {
|
|
|
242
314
|
{
|
|
243
315
|
name: META_TOOLS.RUN,
|
|
244
316
|
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. `
|
|
245
|
-
+ `${COST_NOTE} A run that FAILS returns an error and no rows rather than an empty result, so a zero-row answer
|
|
317
|
+
+ `${COST_NOTE} A run that FAILS returns an error and no rows rather than an empty result, so a zero-row answer means the source was reached and matched nothing for exactly that input. Failed and zero-row results carry next_step, must_supply and a verified example_input to copy. Inputs are checked locally first: a missing required field or an invalid enum value is reported free, without starting a run.`,
|
|
246
318
|
annotations: { title: 'Run any government data tool', ...ANNOTATIONS.BILLED_LIVE_READ },
|
|
247
319
|
inputSchema: {
|
|
248
320
|
type: 'object',
|
|
@@ -308,13 +380,25 @@ export function describeTool(index, name) {
|
|
|
308
380
|
? { ...actor.pricing, summary: priceLine(actor.pricing) }
|
|
309
381
|
: null,
|
|
310
382
|
storeUrl: actor.storeUrl,
|
|
383
|
+
must_supply: actor.mustSupply || [],
|
|
384
|
+
example_input: actor.exampleInput || null,
|
|
385
|
+
examples: actor.examples || [],
|
|
386
|
+
how_to_call: 'Start from example_input (or one of examples): these inputs are verified to return rows. Change only the values you need, keep the call scoped (state / coordinates / name / date window) and keep the first call small. '
|
|
387
|
+
+ `Run it with ${META_TOOLS.RUN} {"tool":"${actor.slug}","input":{...}}${index.featured && index.featured.includes(actor.slug) ? ` or call the dedicated "${actor.slug}" tool directly` : ''}.`,
|
|
311
388
|
inputSchema: actor.inputSchema,
|
|
312
389
|
};
|
|
313
390
|
}
|
|
314
391
|
|
|
315
392
|
/** Resolve a tool call to { slug, input } or an error, for both featured and meta RUN calls. */
|
|
316
393
|
export function resolveCall(index, toolName, args) {
|
|
317
|
-
|
|
394
|
+
const checked = (slug, input, maxItems) => {
|
|
395
|
+
const problems = preflight(index.bySlug.get(slug), input);
|
|
396
|
+
if (problems.length) {
|
|
397
|
+
return { ok: false, error: JSON.stringify({ tool: slug, run_status: 'NOT_RUN_INPUT_INVALID', problems, ...guidanceFor(index.bySlug.get(slug), 'preflight') }, null, 2) };
|
|
398
|
+
}
|
|
399
|
+
return { ok: true, slug, input, maxItems };
|
|
400
|
+
};
|
|
401
|
+
if (index.bySlug.has(toolName)) return checked(toolName, args || {}, undefined);
|
|
318
402
|
if (toolName === META_TOOLS.RUN) {
|
|
319
403
|
const slug = args && args.tool;
|
|
320
404
|
if (!slug) return { ok: false, error: `${META_TOOLS.RUN} requires a "tool" argument naming which catalog tool to run.` };
|
|
@@ -322,7 +406,7 @@ export function resolveCall(index, toolName, args) {
|
|
|
322
406
|
const near = searchCatalog(index, String(slug).replace(/[-_]/g, ' '), 5).map(r => r.tool);
|
|
323
407
|
return { ok: false, error: `No tool named "${slug}" in this catalog.${near.length ? ` Closest matches: ${near.join(', ')}.` : ''}${index.scopeNote || ''}` };
|
|
324
408
|
}
|
|
325
|
-
return
|
|
409
|
+
return checked(slug, (args && args.input) || {}, args && args.maxItems);
|
|
326
410
|
}
|
|
327
411
|
// A caller with a hardcoded pre-1.1 name gets told what happened rather than
|
|
328
412
|
// silently aliased, so the hardcode gets fixed instead of quietly persisting.
|
|
@@ -333,7 +417,7 @@ export function resolveCall(index, toolName, args) {
|
|
|
333
417
|
}
|
|
334
418
|
|
|
335
419
|
/** Shape a run result into MCP text content. Never collapses a failure into an empty answer. */
|
|
336
|
-
export function formatRunResult(slug, result) {
|
|
420
|
+
export function formatRunResult(slug, result, actor = null) {
|
|
337
421
|
const header = {
|
|
338
422
|
tool: slug,
|
|
339
423
|
run_status: result.status,
|
|
@@ -343,6 +427,10 @@ export function formatRunResult(slug, result) {
|
|
|
343
427
|
};
|
|
344
428
|
if (result.statusMessage) header.note = result.statusMessage;
|
|
345
429
|
const failed = result.status !== 'SUCCEEDED';
|
|
430
|
+
// A run still in flight on Apify is not a wrong input; do not tell the agent to change it.
|
|
431
|
+
const pending = result.status === 'CLIENT_TIMEOUT' || result.status === 'DATASET_UNSETTLED';
|
|
432
|
+
if (actor && failed && !pending) Object.assign(header, guidanceFor(actor, 'failed'));
|
|
433
|
+
if (actor && !failed && result.itemCount === 0) Object.assign(header, guidanceFor(actor, 'zero'));
|
|
346
434
|
return {
|
|
347
435
|
isError: failed,
|
|
348
436
|
text: JSON.stringify(failed ? header : { ...header, rows: result.items }, null, 2),
|