From 8b7575fc9893731ccece9812d20206a1b3aec5fb Mon Sep 17 00:00:00 2001 From: Nikolai Savelev Date: Fri, 17 Jul 2026 14:39:36 +0200 Subject: [PATCH 1/3] fix: register all MCP tools via registerSurface so /mcp/connector stays compliant --- src/index.ts | 22 ----------------- src/tools/register.ts | 7 ++++++ test/tools/compliance-mode.spec.ts | 38 +++++++++++++++++++++++++++++- 3 files changed, 44 insertions(+), 23 deletions(-) diff --git a/src/index.ts b/src/index.ts index ed72c3c..2daff21 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,17 +6,6 @@ import { FastMCP, OAuthProvider } from 'fastmcp'; import { OAuthProxy } from 'fastmcp/auth'; import { getConfig, classifyComplianceInput } from './config.js'; import type { BrowserlessSession } from './@types/types.js'; -import { registerSmartScraperTool } from './tools/smartscraper.js'; -import { registerFunctionTool } from './tools/function.js'; -import { registerExportTool } from './tools/export.js'; -import { registerAgentTools } from './tools/agent.js'; -import { registerSearchTool } from './tools/search.js'; -import { registerMapTool } from './tools/map.js'; -import { registerCrawlTool } from './tools/crawl.js'; -import { registerPerformanceTool } from './tools/performance.js'; -import { registerProfilesTool } from './tools/profiles.js'; -import { registerApiDocsResource } from './resources/api-docs.js'; -import { registerStatusResource } from './resources/status.js'; import { registerSurface } from './tools/register.js'; import { registerUploadRoute } from './resources/upload-route.js'; import { registerDownloadRoute } from './resources/download-route.js'; @@ -133,17 +122,6 @@ const server = new FastMCP({ authenticate: hybridAuthenticate, }); -registerSmartScraperTool(server, config, analytics); -registerFunctionTool(server, config, analytics); -registerExportTool(server, config, analytics); -registerAgentTools(server, config, analytics); -registerSearchTool(server, config, analytics); -registerMapTool(server, config, analytics); -registerCrawlTool(server, config, analytics); -registerPerformanceTool(server, config, analytics); -registerProfilesTool(server, config, analytics); -registerApiDocsResource(server, config); -registerStatusResource(server, config); registerSurface(server, config, analytics); // Log the active surface (both transports) so it's visible in the boot logs. // Fail-closed value lands on compliant; distinguish "unset" (dropped/wrong-scoped diff --git a/src/tools/register.ts b/src/tools/register.ts index 93d4b78..45c4fc9 100644 --- a/src/tools/register.ts +++ b/src/tools/register.ts @@ -9,6 +9,7 @@ import { registerPerformanceTool } from './performance.js'; import { registerFunctionTool } from './function.js'; import { registerMapTool } from './map.js'; import { registerCrawlTool } from './crawl.js'; +import { registerProfilesTool } from './profiles.js'; import { isCompliant } from './compliance.js'; import { registerApiDocsResource } from '../resources/api-docs.js'; import { registerStatusResource } from '../resources/status.js'; @@ -66,6 +67,12 @@ export function registerSurface( surface: 'full', register: () => registerCrawlTool(server, config, analytics), }, + // Auth-profile listing is full-only: the compliant surface exposes no profile + // capability (the agent rejects `profile`), so it must not advertise profiles. + { + surface: 'full', + register: () => registerProfilesTool(server, config, analytics), + }, { surface: 'full', register: () => registerApiDocsResource(server, config), diff --git a/test/tools/compliance-mode.spec.ts b/test/tools/compliance-mode.spec.ts index 65f6e9d..915899a 100644 --- a/test/tools/compliance-mode.spec.ts +++ b/test/tools/compliance-mode.spec.ts @@ -1,4 +1,6 @@ import { expect } from 'chai'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; import sinon from 'sinon'; import { FastMCP } from 'fastmcp'; import { registerSurface } from '../../src/tools/register.js'; @@ -89,7 +91,7 @@ describe('compliance mode — compliant tool surface', () => { ]); }); - it('full mode registers exactly the 9 tools (regression guard)', () => { + it('full mode registers exactly the 10 tools (regression guard)', () => { const { names } = captureTools(false); expect(names).to.deep.equal([ 'browserless_agent', @@ -98,12 +100,46 @@ describe('compliance mode — compliant tool surface', () => { 'browserless_function', 'browserless_map', 'browserless_performance', + 'browserless_profiles', 'browserless_search', 'browserless_skill', 'browserless_smartscraper', ]); }); + it('browserless_profiles is full-only (excluded from the compliant surface)', () => { + expect(captureTools(true).byName.has('browserless_profiles')).to.be.false; + expect(captureTools(false).byName.has('browserless_profiles')).to.be.true; + }); + + // #179 broke compliance by re-adding direct register*() calls in index.ts, + // bypassing the registerSurface gate (the register.ts-driven tests above can't + // see that). Lock index.ts to registerSurface as the sole registration path. + it('index.ts registers the surface only via registerSurface (no direct tool calls)', () => { + const src = readFileSync(join(process.cwd(), 'src', 'index.ts'), 'utf8'); + const forbidden = [ + 'registerSmartScraperTool(', + 'registerFunctionTool(', + 'registerExportTool(', + 'registerAgentTools(', + 'registerSearchTool(', + 'registerMapTool(', + 'registerCrawlTool(', + 'registerPerformanceTool(', + 'registerProfilesTool(', + 'registerApiDocsResource(', + 'registerStatusResource(', + ]; + const leaked = forbidden.filter((name) => src.includes(name)); + expect( + leaked, + 'these must be registered via registerSurface, not directly in index.ts', + ).to.deep.equal([]); + expect(src, 'index.ts must call registerSurface').to.include( + 'registerSurface(', + ); + }); + describe('non-tool surface (prompts + resources)', () => { it('compliant mode omits the smartscraper-centric prompts + api-docs resource', () => { const { promptNames, resourceUris } = captureTools(true); From f4c2b25e985049f382af655472a2d8dd6e93a120 Mon Sep 17 00:00:00 2001 From: Nikolai Savelev Date: Fri, 17 Jul 2026 16:44:23 +0200 Subject: [PATCH 2/3] test: make the index.ts registration guard generic so any direct register*() call fails --- test/tools/compliance-mode.spec.ts | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/test/tools/compliance-mode.spec.ts b/test/tools/compliance-mode.spec.ts index 915899a..6bb3285 100644 --- a/test/tools/compliance-mode.spec.ts +++ b/test/tools/compliance-mode.spec.ts @@ -117,23 +117,17 @@ describe('compliance mode — compliant tool surface', () => { // see that). Lock index.ts to registerSurface as the sole registration path. it('index.ts registers the surface only via registerSurface (no direct tool calls)', () => { const src = readFileSync(join(process.cwd(), 'src', 'index.ts'), 'utf8'); - const forbidden = [ - 'registerSmartScraperTool(', - 'registerFunctionTool(', - 'registerExportTool(', - 'registerAgentTools(', - 'registerSearchTool(', - 'registerMapTool(', - 'registerCrawlTool(', - 'registerPerformanceTool(', - 'registerProfilesTool(', - 'registerApiDocsResource(', - 'registerStatusResource(', - ]; - const leaked = forbidden.filter((name) => src.includes(name)); + // Any registerTool(s)/Resource/Prompt CALL in index.ts bypasses the + // registerSurface compliance gate — that is exactly how #179 regressed. Match + // generically (not a fixed denylist) so a brand-new tool registered directly + // here also fails. `registerSurface` and the route helpers + // (registerUploadRoute/registerDownloadRoute) are not surface registrars. + const directCalls = [ + ...src.matchAll(/\bregister\w+(?:Tools?|Resource|Prompt)\s*\(/g), + ].map((m) => m[0]); expect( - leaked, - 'these must be registered via registerSurface, not directly in index.ts', + directCalls, + 'register via registerSurface, not directly in index.ts', ).to.deep.equal([]); expect(src, 'index.ts must call registerSurface').to.include( 'registerSurface(', From 25a606191feb69c2449a9150544245269de2ed7a Mon Sep 17 00:00:00 2001 From: Nikolai Savelev Date: Fri, 17 Jul 2026 17:34:04 +0200 Subject: [PATCH 3/3] refactor: tighten compliance-guard comments to <=2 lines --- src/tools/register.ts | 3 +-- test/tools/compliance-mode.spec.ts | 7 ++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/tools/register.ts b/src/tools/register.ts index 45c4fc9..d7816ad 100644 --- a/src/tools/register.ts +++ b/src/tools/register.ts @@ -67,8 +67,7 @@ export function registerSurface( surface: 'full', register: () => registerCrawlTool(server, config, analytics), }, - // Auth-profile listing is full-only: the compliant surface exposes no profile - // capability (the agent rejects `profile`), so it must not advertise profiles. + // Full-only: the compliant surface has no profile capability (agent rejects `profile`). { surface: 'full', register: () => registerProfilesTool(server, config, analytics), diff --git a/test/tools/compliance-mode.spec.ts b/test/tools/compliance-mode.spec.ts index 6bb3285..715d402 100644 --- a/test/tools/compliance-mode.spec.ts +++ b/test/tools/compliance-mode.spec.ts @@ -117,11 +117,8 @@ describe('compliance mode — compliant tool surface', () => { // see that). Lock index.ts to registerSurface as the sole registration path. it('index.ts registers the surface only via registerSurface (no direct tool calls)', () => { const src = readFileSync(join(process.cwd(), 'src', 'index.ts'), 'utf8'); - // Any registerTool(s)/Resource/Prompt CALL in index.ts bypasses the - // registerSurface compliance gate — that is exactly how #179 regressed. Match - // generically (not a fixed denylist) so a brand-new tool registered directly - // here also fails. `registerSurface` and the route helpers - // (registerUploadRoute/registerDownloadRoute) are not surface registrars. + // A register*Tool/Resource/Prompt call in index.ts bypasses the registerSurface + // gate (how #179 regressed); generic match catches any new tool, not a fixed list. const directCalls = [ ...src.matchAll(/\bregister\w+(?:Tools?|Resource|Prompt)\s*\(/g), ].map((m) => m[0]);