Skip to content
Open
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ Then point your MCP client at `http://localhost:8080/mcp` using the same header/
| `BROWSERLESS_TIMEOUT` | No | `30000` | Request timeout in milliseconds |
| `BROWSERLESS_MAX_RETRIES` | No | `3` | Max retry attempts for failed requests |
| `BROWSERLESS_CACHE_TTL` | No | `60000` | Cache TTL in milliseconds (0 to disable) |
| `AMPLITUDE_API_KEY` | No | — | Amplitude project API key for MCP usage analytics |
| `MCP_COMPLIANCE_MODE` | No | unset (full surface) | Serve the reduced, directory-compliant surface. Fails closed: any set value except `false`/`0`/`no`/`off` enables it |

## MCP Resources
Expand Down
2 changes: 2 additions & 0 deletions llms-install.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ If the user needs a local-only / air-gapped install, use the npm package over st
}
```

To opt in to Amplitude analytics, add `AMPLITUDE_API_KEY` with a real Amplitude project key.

4. **Reload the MCP client.**

## Verify install
Expand Down
83 changes: 75 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@
"printWidth": 80
},
"dependencies": {
"@amplitude/analytics-node": "^1.5.68",
"@amplitude/mcp-analytics": "^0.3.0",
"@aws-sdk/client-sqs": "^3.1053.0",
"@modelcontextprotocol/sdk": "^1.24.3",
"fastmcp": "4.4.0",
"ioredis": "^5.10.1",
"ws": "^8.21.0",
Expand Down
3 changes: 3 additions & 0 deletions src/@types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export interface BrowserlessSession extends Record<string, unknown> {
* resulting id instead of letting the model open a `createProfile` session.
*/
attachSessionId?: string;
/** Verified account id for OAuth/Supabase-authenticated sessions. */
accountId?: string;
/** Origin tag from the `x-browserless-mcp-source` header; see resolveMcpSource. */
source?: string;
}
Expand All @@ -69,6 +71,7 @@ export interface McpConfig {
maxRetries: number;
cacheTtlMs: number;
analyticsEnabled: boolean;
amplitudeApiKey?: string;
// Required (not optional): the compliant surface is a security gate, so every
// McpConfig must choose explicitly — an omitted field must not default to the
// fuller/prohibited surface.
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export function getConfig(): McpConfig {
maxRetries: parseInt(process.env.BROWSERLESS_MAX_RETRIES ?? '3', 10),
cacheTtlMs: parseInt(process.env.BROWSERLESS_CACHE_TTL ?? '60000', 10),
analyticsEnabled: process.env.ANALYTICS_ENABLED === 'true',
amplitudeApiKey: process.env.AMPLITUDE_API_KEY,
// Per-process toggle for the compliant surface used by the OpenAI/Anthropic
// directory listings: registers fewer tools and de-fangs the agent (see
// tools/compliance.ts). Fails closed — see parseComplianceMode.
Expand Down
39 changes: 39 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import { resolveBrowserlessAuth } from './lib/http-auth.js';
import { BoundedEventStore } from './lib/bounded-event-store.js';
import { RedisOAuthProxy } from './lib/redis-oauth-proxy.js';
import { Redis } from 'ioredis';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
instrumentFastMcpTools,
initializeAmplitudeAnalytics,
shutdownAmplitudeAnalytics,
} from './lib/amplitude-analytics.js';

const pkg = JSON.parse(
readFileSync(
Expand All @@ -38,6 +44,10 @@ const analytics = new AnalyticsHelper(
config.sqsQueueUrl,
config.sqsRegion,
);
const amplitudeAnalytics = initializeAmplitudeAnalytics(
config.amplitudeApiKey,
pkg.version,
);

// Passthrough OAuth provider: disables FastMCP's token-swap mode so the MCP client
// receives the raw Supabase JWT directly.
Expand Down Expand Up @@ -122,6 +132,7 @@ const server = new FastMCP<BrowserlessSession>({
authenticate: hybridAuthenticate,
});

instrumentFastMcpTools(server, amplitudeAnalytics);
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
Expand All @@ -143,15 +154,43 @@ const complianceSurface = config.complianceMode
: 'full (explicit opt-out)';
console.error(`[browserless-mcp] Tool surface: ${complianceSurface}`);

let warnedAboutServerIdentity = false;
server.on('connect', (event) => {
const id = event.session.sessionId ?? 'stdio';
console.error(`[browserless-mcp] Client connected: ${id}`);
if (
amplitudeAnalytics &&
!warnedAboutServerIdentity &&
!(event.session.server instanceof Server)
) {
warnedAboutServerIdentity = true;
console.error(
'[browserless-mcp] WARNING: FastMCP session server is not an MCP SDK Server; Amplitude instrumentation may be disabled.',
);
}
// force the client to refresh its tool list on connect
void event.session.triggerListChangedNotification(
'notifications/tools/list_changed',
);
});

if (amplitudeAnalytics) {
let amplitudeShutdown = false;
const shutdown = (exitCode: number): void => {
if (amplitudeShutdown) return;
amplitudeShutdown = true;
void (async () => {
try {
await shutdownAmplitudeAnalytics(amplitudeAnalytics);
} finally {
process.exit(exitCode);
}
})();
};
process.once('SIGTERM', () => shutdown(143));
process.once('SIGINT', () => shutdown(130));
}

server.on('disconnect', (event) => {
const id = event.session.sessionId ?? 'stdio';
// Drop any files staged/captured for this session (TTL is the backstop).
Expand Down
2 changes: 2 additions & 0 deletions src/lib/account-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { SupabaseJwtPayload } from '../@types/types.js';
interface ResolvedAccount {
apiKey: string;
email: string;
accountId: string;
}

const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
Expand Down Expand Up @@ -149,6 +150,7 @@ export async function resolveApiKey(
const resolved: ResolvedAccount = {
apiKey: account.api_key,
email: account.email,
accountId,
};

cache.set(cacheKey, resolved);
Expand Down
Loading