Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [Unreleased]

### Changed

- `firecrawl_gov_search` asks `/v2/search/gov` for `sources=law` and returns the `law` results. Each row in `structuredContent.results` keeps `url`, `title`, `description` and `position`, and also carries the index's legal labels when it has them: `jurisdiction`, `court`, `court_type`, `cites`, `cited_as` and `updated_at`. The text block, the tool description and the input schema are unchanged. Against an API that does not accept `sources` yet, which answers only `web`, the tool returns those results as before.

## [3.36.0] - 2026-10-27

### Changed
Expand Down
16 changes: 14 additions & 2 deletions src/gov.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@ interface GovHit {
position?: number;
}

/** A `law` result: a hit plus the index's legal labels, each absent when unknown. */
interface LawHit extends GovHit {
jurisdiction?: string;
court?: string;
court_type?: string;
cites?: string[];
cited_as?: string[];
updated_at?: number;
}

/** Render hits as `## <position>. <title>` / url / description blocks. */
function fmtGov(results?: GovHit[]): string {
if (!results || results.length === 0) return '(no results)';
Expand Down Expand Up @@ -150,10 +160,11 @@ Returns ranked results with a position, title, URL, and matched snippet. Results
for (const [key, value] of Object.entries(options)) {
if (value != null) params.append(key, String(value));
}
params.append('sources', 'law');
const client = getClient(session) as ClientLike;
const res = await client.http
.get<{
data?: { web?: GovHit[] };
data?: { law?: LawHit[]; web?: GovHit[] };
}>(
`${BASE}?${params.toString()}`,
originHeaders(requestOrigin(mcpClient, session))
Expand All @@ -170,7 +181,8 @@ Returns ranked results with a position, title, URL, and matched snippet. Results
}
throw new UserError(message, { status: 400, error: message });
});
const results = res.data?.data?.web ?? [];
// An API without `sources` answers only `web`.
const results = res.data?.data?.law ?? res.data?.data?.web ?? [];
return withStructured(fmtGov(results), { results });
},
});
Expand Down
45 changes: 43 additions & 2 deletions tests/mcp-search-profile.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -197,12 +197,16 @@ async function startFakeBackend(options = {}) {
JSON.stringify({
success: true,
data: {
web: [
[options.govGroup ?? 'law']: [
{
description: 'The matched snippet.',
position: 1,
title: '21 CFR Part 101 -- Food Labeling',
url: 'https://www.ecfr.gov/current/title-21/part-101',
jurisdiction: 'us',
cites: ['us:usc:21:343'],
cited_as: ['us:cfr:21:101'],
updated_at: 1700000000000,
},
],
},
Expand Down Expand Up @@ -801,6 +805,31 @@ test('search firecrawl_developer_search queries the developer index and returns
assert.equal(skillsQuery.get('query'), 'retry loop backoff');
});

test('search firecrawl_gov_search reads web results from an API without sources', async (t) => {
const backend = await startFakeBackend({ govGroup: 'web' });
t.after(() => backend.close());
const { searchPort } = await startHostedServer(t, {
FIRECRAWL_API_URL: backend.url,
});

const res = await jsonRpc(searchPort, SEARCH_ENDPOINT, {
id: 45,
method: 'tools/call',
params: {
arguments: { query: 'food labeling requirements', k: 1 },
name: 'firecrawl_gov_search',
},
headers: { 'x-api-key': 'fc-search-key' },
});
assert.equal(res.status, 200);
const message = parseSseJson(await res.text());
assert.notEqual(message.result?.isError, true, JSON.stringify(message));
assert.deepEqual(
message.result.structuredContent.results.map((result) => result.url),
['https://www.ecfr.gov/current/title-21/part-101']
);
});

test('search firecrawl_gov_search queries the Government Index', async (t) => {
const backend = await startFakeBackend();
t.after(() => backend.close());
Expand Down Expand Up @@ -828,12 +857,24 @@ test('search firecrawl_gov_search queries the Government Index', async (t) => {
const query = new URL(calls[0].url, 'http://localhost').searchParams;
assert.equal(query.get('query'), 'food labeling requirements');
assert.equal(query.get('k'), '1');
assert.deepEqual(query.getAll('sources'), ['law']);

assert.equal(
message.result.content[0].text,
'## 1. 21 CFR Part 101 -- Food Labeling\nhttps://www.ecfr.gov/current/title-21/part-101\nThe matched snippet.'
);
assert.equal(message.result.structuredContent.results.length, 1);
assert.deepEqual(message.result.structuredContent.results, [
{
description: 'The matched snippet.',
position: 1,
title: '21 CFR Part 101 -- Food Labeling',
url: 'https://www.ecfr.gov/current/title-21/part-101',
jurisdiction: 'us',
cites: ['us:usc:21:343'],
cited_as: ['us:cfr:21:101'],
updated_at: 1700000000000,
},
]);
assert.equal(backend.requests.some((request) => request.url === '/v2/search'), false);
});

Expand Down
Loading