From aa557e27649570190f049e2c7f5ae3d4b961b6d6 Mon Sep 17 00:00:00 2001 From: erikengervall <5392475+erikengervall@users.noreply.github.com> Date: Thu, 22 Oct 2026 23:15:52 +0000 Subject: [PATCH 1/3] feat(gov): return law results with legal labels from firecrawl_gov_search --- CHANGELOG.md | 6 ++++++ src/gov.ts | 15 +++++++++++++-- tests/mcp-search-profile.test.mjs | 20 ++++++++++++++++++-- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91c00b2c..65cdb44e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. Needs an API that accepts `sources` on `/v2/search/gov`. + ## [3.34.1] - 2026-10-22 ### Changed diff --git a/src/gov.ts b/src/gov.ts index f088901c..82c353f3 100644 --- a/src/gov.ts +++ b/src/gov.ts @@ -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 `## . ` / url / description blocks. */ function fmtGov(results?: GovHit[]): string { if (!results || results.length === 0) return '(no results)'; @@ -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[] }; }>( `${BASE}?${params.toString()}`, originHeaders(requestOrigin(mcpClient, session)) @@ -170,7 +181,7 @@ 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 ?? []; + const results = res.data?.data?.law ?? []; return withStructured(fmtGov(results), { results }); }, }); diff --git a/tests/mcp-search-profile.test.mjs b/tests/mcp-search-profile.test.mjs index 52a6cf82..66822b31 100644 --- a/tests/mcp-search-profile.test.mjs +++ b/tests/mcp-search-profile.test.mjs @@ -197,12 +197,16 @@ async function startFakeBackend(options = {}) { JSON.stringify({ success: true, data: { - web: [ + 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, }, ], }, @@ -825,12 +829,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); }); From 56c3a4523648b032262b337e3572f32c201dac2d Mon Sep 17 00:00:00 2001 From: erikengervall <5392475+erikengervall@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:31:10 +0000 Subject: [PATCH 2/3] docs(changelog): keep the gov_search law entry under Unreleased --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad1c9c64..e71a8695 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,15 @@ # Changelog -## [3.36.0] - 2026-10-27 +## [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. Needs an API that accepts `sources` on `/v2/search/gov`. + +## [3.36.0] - 2026-10-27 + +### Changed + - Local HTTP mode (`HTTP_STREAMABLE_SERVER=true` or `SSE_LOCAL=true` without `CLOUD_SERVICE=true`) checks the `Host` and `Origin` headers. It answers `localhost`, `127.0.0.1` and `[::1]` on any port, the address in `HOST` unless it is `0.0.0.0` or `::`, and the hosts listed in the new `FIRECRAWL_MCP_ALLOWED_HOSTS` (comma-separated). Any other `Host`, or an `Origin` naming any other host (including `null`), gets `403` with a JSON-RPC error, and the server logs a warning that names `FIRECRAWL_MCP_ALLOWED_HOSTS`, so a web page can't reach the server through DNS rebinding or a cross-origin request and spend the configured `FIRECRAWL_API_KEY`. CORS headers echo an allowed origin, with `Vary: Origin`, instead of `Access-Control-Allow-Origin: *`. `GET` and `HEAD` requests to `/health` are answered for any host, so health checks that address the server by IP keep working; they get no CORS allow for other origins. Clients that reach the server under another name, such as a LAN address or a reverse-proxy hostname, need that name in `FIRECRAWL_MCP_ALLOWED_HOSTS`; `FIRECRAWL_MCP_ALLOWED_HOSTS=*` restores the previous behaviour. `CLOUD_SERVICE=true` and stdio are unchanged. - The `firecrawl_scrape` description's Alexandria contract guidance says "a key field under response" instead of `response.key`. Some MCP gateways scan tool descriptions for sensitive file names, read `response.key` as a private key file, and flagged the tool. From d27129bb9cd53768e3950a644b35566215ebf3ab Mon Sep 17 00:00:00 2001 From: erikengervall <5392475+erikengervall@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:51:29 +0000 Subject: [PATCH 3/3] fix(gov): fall back to web results when the API returns no law group --- CHANGELOG.md | 2 +- src/gov.ts | 5 +++-- tests/mcp-search-profile.test.mjs | 27 ++++++++++++++++++++++++++- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e71a8695..d441075d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### 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. Needs an API that accepts `sources` on `/v2/search/gov`. +- `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 diff --git a/src/gov.ts b/src/gov.ts index bd19d033..42e30f5d 100644 --- a/src/gov.ts +++ b/src/gov.ts @@ -164,7 +164,7 @@ Returns ranked results with a position, title, URL, and matched snippet. Results const client = getClient(session) as ClientLike; const res = await client.http .get<{ - data?: { law?: LawHit[] }; + data?: { law?: LawHit[]; web?: GovHit[] }; }>( `${BASE}?${params.toString()}`, originHeaders(requestOrigin(mcpClient, session)) @@ -181,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?.law ?? []; + // An API without `sources` answers only `web`. + const results = res.data?.data?.law ?? res.data?.data?.web ?? []; return withStructured(fmtGov(results), { results }); }, }); diff --git a/tests/mcp-search-profile.test.mjs b/tests/mcp-search-profile.test.mjs index 3d403da8..8fea744d 100644 --- a/tests/mcp-search-profile.test.mjs +++ b/tests/mcp-search-profile.test.mjs @@ -197,7 +197,7 @@ async function startFakeBackend(options = {}) { JSON.stringify({ success: true, data: { - law: [ + [options.govGroup ?? 'law']: [ { description: 'The matched snippet.', position: 1, @@ -805,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());