Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ The Browserbase MCP server accepts the following command-line flags:
| `--browserWidth <width>` | Browser viewport width (default: 1024) |
| `--browserHeight <height>` | Browser viewport height (default: 768) |
| `--modelName <model>` | The model to use for Stagehand (default: google/gemini-2.5-flash-lite) |
| `--modelApiKey <key>` | API key for the custom model provider (required when using custom models) |
| `--modelApiKey <key>` | Optional API key for a custom model provider |
| `--experimental` | Enable experimental features (default: false) |

These flags can be passed directly to the CLI or configured in your MCP configuration file.
Expand All @@ -180,7 +180,7 @@ These flags can be passed directly to the CLI or configured in your MCP configur

Stagehand defaults to using Google's Gemini 2.5 Flash Lite model, but you can configure it to use other models like GPT-4o, Claude, or other providers.

**Important**: When using any custom model (non-default), you must provide your own API key for that model provider using the `--modelApiKey` flag.
When `--modelApiKey` is omitted, Browserbase routes supported provider/model names through the Browserbase model gateway using your Browserbase API key. Provide `--modelApiKey` only when you want to bring your own provider key.

```json
{
Expand All @@ -190,9 +190,7 @@ Stagehand defaults to using Google's Gemini 2.5 Flash Lite model, but you can co
"args": [
"@browserbasehq/mcp",
"--modelName",
"anthropic/claude-sonnet-4.5",
"--modelApiKey",
"your-anthropic-api-key"
"anthropic/claude-sonnet-4.5"
],
"env": {
"BROWSERBASE_API_KEY": "",
Expand Down
6 changes: 3 additions & 3 deletions config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,12 @@ export type Config = {
* The Model that Stagehand uses
* Available models: OpenAI, Claude, Gemini, Cerebras, Groq, and other providers
*
* @default "gemini-2.0-flash"
* @default "google/gemini-2.5-flash-lite"
*/
modelName?: z.infer<typeof AvailableModelSchema>;
/**
* API key for the custom model provider
* Required when using a model other than the default gemini-2.0-flash
* Optional API key for a custom model provider.
* When omitted, Browserbase routes supported provider/model names through the model gateway.
*/
modelApiKey?: string;
/**
Expand Down
58 changes: 57 additions & 1 deletion src/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { describe, expect, it } from "vitest";

import { configFromCLIOptions, normalizeVerifiedConfig } from "./config.js";
import {
configFromCLIOptions,
normalizeVerifiedConfig,
resolveConfig,
} from "./config.js";
import { configSchema } from "./index.js";

describe("verified config compatibility", () => {
Expand Down Expand Up @@ -31,3 +35,55 @@ describe("verified config compatibility", () => {
expect(config.advancedStealth).toBe(true);
});
});

describe("model gateway config", () => {
it("accepts a provider/model modelName without modelApiKey", () => {
const config = configSchema.parse({
browserbaseApiKey: "test-key",
browserbaseProjectId: "test-project",
modelName: "openai/gpt-4.1",
});

expect(config.modelName).toBe("openai/gpt-4.1");
expect(config.modelApiKey).toBeUndefined();
});

it("keeps modelApiKey undefined when no provider key is configured", async () => {
const originalGeminiApiKey = process.env.GEMINI_API_KEY;
const originalGoogleApiKey = process.env.GOOGLE_API_KEY;
const originalBrowserbaseApiKey = process.env.BROWSERBASE_API_KEY;
const originalBrowserbaseProjectId = process.env.BROWSERBASE_PROJECT_ID;

delete process.env.GEMINI_API_KEY;
delete process.env.GOOGLE_API_KEY;
process.env.BROWSERBASE_API_KEY = "test-browserbase-key";
process.env.BROWSERBASE_PROJECT_ID = "test-project";

try {
const config = await resolveConfig({
modelName: "openai/gpt-4.1",
});

expect(config.modelName).toBe("openai/gpt-4.1");
expect(config.modelApiKey).toBeUndefined();
} finally {
if (originalGeminiApiKey === undefined) delete process.env.GEMINI_API_KEY;
else process.env.GEMINI_API_KEY = originalGeminiApiKey;

if (originalGoogleApiKey === undefined) delete process.env.GOOGLE_API_KEY;
else process.env.GOOGLE_API_KEY = originalGoogleApiKey;

if (originalBrowserbaseApiKey === undefined) {
delete process.env.BROWSERBASE_API_KEY;
} else {
process.env.BROWSERBASE_API_KEY = originalBrowserbaseApiKey;
}

if (originalBrowserbaseProjectId === undefined) {
delete process.env.BROWSERBASE_PROJECT_ID;
} else {
process.env.BROWSERBASE_PROJECT_ID = originalBrowserbaseProjectId;
}
}
});
});
7 changes: 0 additions & 7 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,6 @@ export async function resolveConfig(cliOptions: CLIOptions): Promise<Config> {
mergedConfig.browserbaseProjectId = "dummy-browserbase-project-id";
}

if (!mergedConfig.modelApiKey) {
console.warn(
"Warning: MODEL_API_KEY environment variable not set. Using dummy value.",
);
mergedConfig.modelApiKey = "dummy-api-key";
}

return mergedConfig;
}

Expand Down
168 changes: 72 additions & 96 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,102 +20,78 @@ import {
} from "@modelcontextprotocol/sdk/types.js";

// Configuration schema - matches existing Config interface
export const configSchema = z
.object({
browserbaseApiKey: z.string().describe("The Browserbase API Key to use"),
browserbaseProjectId: z
.string()
.describe("The Browserbase Project ID to use"),
proxies: z
.boolean()
.optional()
.describe("Whether or not to use Browserbase proxies"),
verified: z
.boolean()
.optional()
.describe(
"Use Browserbase Verified Identity. Only available to Browserbase Scale Plan users",
),
advancedStealth: z
.boolean()
.optional()
.describe("Deprecated alias for verified"),
keepAlive: z
.boolean()
.optional()
.describe("Whether or not to keep the Browserbase session alive"),
context: z
.object({
contextId: z
.string()
.optional()
.describe("The ID of the context to use"),
persist: z
.boolean()
.optional()
.describe("Whether or not to persist the context"),
})
.optional(),
viewPort: z
.object({
browserWidth: z
.number()
.optional()
.describe("The width of the browser"),
browserHeight: z
.number()
.optional()
.describe("The height of the browser"),
})
.optional(),
server: z
.object({
port: z
.number()
.optional()
.describe("The port to listen on for SHTTP or MCP transport"),
host: z
.string()
.optional()
.describe(
"The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces",
),
})
.optional(),
modelName: z
.string()
.optional()
.describe(
"The model to use for Stagehand (default: google/gemini-2.5-flash-lite)",
),
modelApiKey: z
.string()
.optional()
.describe(
"API key for the custom model provider. Required when using a model other than the default google/gemini-2.5-flash-lite",
),
experimental: z
.boolean()
.optional()
.describe("Enable experimental Stagehand features"),
})
.refine(
(data) => {
// If a non-default model is explicitly specified, API key is required
if (data.modelName && data.modelName !== "google/gemini-2.5-flash-lite") {
return (
data.modelApiKey !== undefined &&
typeof data.modelApiKey === "string" &&
data.modelApiKey.length > 0
);
}
return true;
},
{
message: "modelApiKey is required when specifying a custom model",
path: ["modelApiKey"],
},
);
export const configSchema = z.object({
browserbaseApiKey: z.string().describe("The Browserbase API Key to use"),
browserbaseProjectId: z
.string()
.describe("The Browserbase Project ID to use"),
proxies: z
.boolean()
.optional()
.describe("Whether or not to use Browserbase proxies"),
verified: z
.boolean()
.optional()
.describe(
"Use Browserbase Verified Identity. Only available to Browserbase Scale Plan users",
),
advancedStealth: z
.boolean()
.optional()
.describe("Deprecated alias for verified"),
keepAlive: z
.boolean()
.optional()
.describe("Whether or not to keep the Browserbase session alive"),
context: z
.object({
contextId: z.string().optional().describe("The ID of the context to use"),
persist: z
.boolean()
.optional()
.describe("Whether or not to persist the context"),
})
.optional(),
viewPort: z
.object({
browserWidth: z.number().optional().describe("The width of the browser"),
browserHeight: z
.number()
.optional()
.describe("The height of the browser"),
})
.optional(),
server: z
.object({
port: z
.number()
.optional()
.describe("The port to listen on for SHTTP or MCP transport"),
host: z
.string()
.optional()
.describe(
"The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces",
),
})
.optional(),
modelName: z
.string()
.optional()
.describe(
"The model to use for Stagehand (default: google/gemini-2.5-flash-lite)",
),
modelApiKey: z
.string()
.optional()
.describe(
"Optional API key for a custom model provider. When omitted, Browserbase routes supported provider/model names through the model gateway.",
),
experimental: z
.boolean()
.optional()
.describe("Enable experimental Stagehand features"),
});

// Default function for creating MCP server instance
export default function ({ config }: { config: z.infer<typeof configSchema> }) {
Expand Down
6 changes: 3 additions & 3 deletions src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ program
.option("--browserHeight <height>", "Browser height to use for the browser.")
.option(
"--modelName <model>",
"The model to use for Stagehand (default: gemini-2.0-flash)",
"The model to use for Stagehand (default: google/gemini-2.5-flash-lite)",
)
.option(
"--modelApiKey <key>",
"API key for the custom model provider (required when using custom models)",
"Optional API key for a custom model provider. When omitted, Browserbase routes supported provider/model names through the model gateway.",
)
.option("--keepAlive", "Enable Browserbase Keep Alive Session")
.option("--experimental", "Enable experimental features")
Expand All @@ -74,7 +74,7 @@ program

if (options.port)
startHttpTransport(+options.port, options.host, serverList);
else await startStdioTransport(serverList, config);
else await startStdioTransport(serverList);
});

function setupExitWatchdog(serverList: ServerList) {
Expand Down
Loading
Loading