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..d7816ad 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,11 @@ export function registerSurface( surface: 'full', register: () => registerCrawlTool(server, config, analytics), }, + // Full-only: the compliant surface has no profile capability (agent rejects `profile`). + { + 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..715d402 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,37 @@ 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'); + // 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]); + expect( + directCalls, + 'register 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);