@@ -7,6 +7,7 @@ import { readFile } from 'node:fs/promises';
77import path from 'node:path' ;
88import { z } from 'zod' ;
99import { registerMonitorTools } from './monitor.js' ;
10+ import { registerResearchTools } from './research.js' ;
1011
1112dotenv . config ( { debug : false , quiet : true } ) ;
1213
@@ -16,9 +17,37 @@ interface SessionData {
1617 * `Authorization: Bearer ...` to the Firecrawl API.
1718 */
1819 firecrawlApiKey ? : string ;
20+ /**
21+ * Whether the (experimental) research tools are exposed for this session.
22+ * Enabled locally via `FIRECRAWL_RESEARCH=true`, or per-request via the
23+ * `?research=true` query param on the MCP endpoint.
24+ */
25+ research ? : boolean ;
1926 [ key : string ] : unknown ;
2027}
2128
29+ /**
30+ * Decide whether the research tools should be visible for a session.
31+ * Local/stdio/self-hosted: gated by `FIRECRAWL_RESEARCH=true`.
32+ * Remote (HTTP): additionally enabled by a `?research=true` query param on the
33+ * incoming MCP request URL.
34+ */
35+ function isResearchEnabled ( request ?: { url ?: string } ) : boolean {
36+ if ( process . env . FIRECRAWL_RESEARCH === 'true' ) return true ;
37+ const url = request ?. url ;
38+ if ( url ) {
39+ try {
40+ const research = new URL ( url , 'http://localhost' ) . searchParams . get (
41+ 'research'
42+ ) ;
43+ if ( research === 'true' ) return true ;
44+ } catch {
45+ // malformed URL — fall through to disabled
46+ }
47+ }
48+ return false ;
49+ }
50+
2251function normalizeHeader (
2352 value : string | string [ ] | undefined
2453) : string | undefined {
@@ -253,7 +282,9 @@ const server = new FastMCP<SessionData>({
253282 },
254283 authenticate: async (request?: {
255284 headers: IncomingHttpHeaders;
285+ url?: string;
256286 }): Promise<SessionData> => {
287+ const research = isResearchEnabled(request);
257288 // FastMCP invokes ` authenticate ( undefined ) ` for the stdio transport
258289 // because there is no HTTP request context. Without this null guard,
259290 // accessing ` request . headers ` throws a TypeError, FastMCP silently
@@ -271,7 +302,7 @@ const server = new FastMCP<SessionData>({
271302 'Firecrawl credentials required: OAuth access token (Authorization: Bearer fco_...) or API key (x-firecrawl-api-key)'
272303 );
273304 }
274- return { firecrawlApiKey: headerCred };
305+ return { firecrawlApiKey: headerCred, research };
275306 }
276307
277308 const credential = headerCred ?? envCred;
@@ -296,7 +327,7 @@ const server = new FastMCP<SessionData>({
296327 process.exit(1);
297328 }
298329
299- return { firecrawlApiKey: credential };
330+ return { firecrawlApiKey: credential, research };
300331 },
301332 // Lightweight health endpoint for LB checks
302333 health: {
@@ -1808,4 +1839,20 @@ if (
18081839
18091840registerMonitorTools ( server ) ;
18101841
1842+ // Research tools gating. FastMCP's `canAccess` is only honored on the HTTP
1843+ // transport (the stdio path exposes every registered tool regardless), so we
1844+ // split the two cases:
1845+ // - HTTP (cloud / SSE_LOCAL / HTTP_STREAMABLE_SERVER): always register; each
1846+ // tool's `canAccess` hides it unless the session has research enabled
1847+ // (`FIRECRAWL_RESEARCH=true` env or `?research=true` on the request).
1848+ // - stdio (local): register only when `FIRECRAWL_RESEARCH=true`, since
1849+ // `canAccess` cannot hide them there.
1850+ const isHttpTransport =
1851+ process . env . CLOUD_SERVICE === 'true' ||
1852+ process . env . SSE_LOCAL === 'true' ||
1853+ process . env . HTTP_STREAMABLE_SERVER === 'true' ;
1854+ if ( isHttpTransport || process . env . FIRECRAWL_RESEARCH === 'true' ) {
1855+ registerResearchTools ( server , getClient ) ;
1856+ }
1857+
18111858await server . start ( args ) ;
0 commit comments