Skip to main content
Glama
config.ts10.9 kB
import * as fs from "node:fs"; import * as path from "node:path"; import * as dotenv from "dotenv"; import { z } from "zod"; // ============================================================================= // Environment Loading with Optional Isolation // ============================================================================= // Load .env file if it exists const envPath = path.resolve(process.cwd(), ".env"); const envFileExists = fs.existsSync(envPath); let dotenvConfig: Record<string, string> = {}; if (envFileExists) { // Use dotenv.parse() instead of dotenv.config() to avoid mutating process.env. // This preserves test isolation where tests set process.env values before import. const envContent = fs.readFileSync(envPath, "utf-8"); dotenvConfig = dotenv.parse(envContent); } // Determine if we should ignore system env vars. // Check process.env FIRST so tests can override .env settings. const ignoreSystemEnv = ( process.env.IGNORE_SYSTEM_ENV ?? dotenvConfig.IGNORE_SYSTEM_ENV )?.toLowerCase() === "true"; if (ignoreSystemEnv && !envFileExists) { // Warn but continue - env vars may be passed via MCP client config console.error( "[grammarly-mcp:warn] IGNORE_SYSTEM_ENV=true but no .env file found. Using process.env only.", ); } // Create the effective environment: when ignoring system envs, use .env only; // otherwise merge .env (if present) with process.env so .env values fill gaps // without overwriting explicit system envs (mirrors dotenv.config semantics). const effectiveEnv = ignoreSystemEnv && envFileExists ? dotenvConfig : { ...dotenvConfig, ...process.env }; // ============================================================================= // Type Definitions // ============================================================================= export type LogLevel = "debug" | "info" | "warn" | "error"; export type LLMProvider = "claude-code" | "openai" | "google" | "anthropic"; export type ClaudeModel = "auto" | "haiku" | "sonnet" | "opus"; export interface AppConfig { // Environment isolation ignoreSystemEnv: boolean; // Browser provider selection browserProvider: "stagehand" | "browser-use"; // Browser Use Cloud (fallback provider) browserUseApiKey: string | undefined; browserUseProfileId: string | undefined; // Browserbase + Stagehand (primary provider) browserbaseApiKey: string | undefined; browserbaseProjectId: string | undefined; browserbaseSessionId: string | undefined; browserbaseContextId: string | undefined; stagehandModel: string | undefined; stagehandCacheDir: string | undefined; // Separate LLM provider controls stagehandLlmProvider: LLMProvider | undefined; rewriteLlmProvider: LLMProvider | undefined; // Claude model selection (when using claude-code provider) claudeModel: ClaudeModel; // Non-Claude model selection openaiModel: string; googleModel: string; anthropicModel: string; // API keys for LLM provider detection claudeApiKey: string | undefined; openaiApiKey: string | undefined; googleApiKey: string | undefined; anthropicApiKey: string | undefined; // General settings llmRequestTimeoutMs: number; connectTimeoutMs: number; logLevel: LogLevel; browserUseDefaultTimeoutMs: number; defaultMaxAiPercent: number; defaultMaxPlagiarismPercent: number; defaultMaxIterations: number; } // ============================================================================= // Zod Schema // ============================================================================= const EnvSchema = z.object({ // Environment isolation IGNORE_SYSTEM_ENV: z .preprocess( (val) => val === "true" || val === true, z.boolean().default(false), ) .default(false), // Provider selection: "stagehand" (default) or "browser-use" (fallback) BROWSER_PROVIDER: z.enum(["stagehand", "browser-use"]).default("stagehand"), // Browser Use Cloud (required when BROWSER_PROVIDER=browser-use) BROWSER_USE_API_KEY: z.string().optional(), BROWSER_USE_PROFILE_ID: z.string().optional(), // Browserbase + Stagehand (required when BROWSER_PROVIDER=stagehand) BROWSERBASE_API_KEY: z.string().optional(), BROWSERBASE_PROJECT_ID: z.string().optional(), BROWSERBASE_SESSION_ID: z.string().optional(), BROWSERBASE_CONTEXT_ID: z.string().optional(), STAGEHAND_MODEL: z.string().default("gemini-2.5-flash"), STAGEHAND_CACHE_DIR: z.string().optional(), // Separate LLM provider controls STAGEHAND_LLM_PROVIDER: z .enum(["claude-code", "openai", "google", "anthropic"]) .optional(), REWRITE_LLM_PROVIDER: z .enum(["claude-code", "openai", "google", "anthropic"]) .optional(), // Claude model selection (when using claude-code provider) CLAUDE_MODEL: z.enum(["auto", "haiku", "sonnet", "opus"]).default("auto"), // Non-Claude model selection OPENAI_MODEL: z.string().default("gpt-4o"), GOOGLE_MODEL: z.string().default("gemini-2.5-flash"), ANTHROPIC_MODEL: z.string().default("claude-sonnet-4-20250514"), // API keys CLAUDE_API_KEY: z.string().optional(), OPENAI_API_KEY: z.string().optional(), GOOGLE_GENERATIVE_AI_API_KEY: z.string().optional(), GEMINI_API_KEY: z.string().optional(), ANTHROPIC_API_KEY: z.string().optional(), // General settings LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"), CLAUDE_REQUEST_TIMEOUT_MS: z.preprocess((value) => { if (typeof value === "string" && value.trim() !== "") { return Number(value); } return undefined; }, z.number().positive().optional()), LLM_REQUEST_TIMEOUT_MS: z.preprocess((value) => { if (typeof value === "string" && value.trim() !== "") { return Number(value); } return undefined; }, z.number().positive().optional()), CONNECT_TIMEOUT_MS: z.preprocess((value) => { if (typeof value === "string" && value.trim() !== "") { return Number(value); } return undefined; }, z.number().positive().optional()), }); // ============================================================================= // Validation and Config Export // ============================================================================= const parsed = EnvSchema.safeParse(effectiveEnv); if (!parsed.success) { // Log to stderr; MCP hosts expect stdout to be protocol-only. // Exiting early avoids a half-configured server. console.error( "[grammarly-mcp:error] Invalid environment configuration", JSON.stringify(parsed.error.format(), null, 2), ); process.exit(1); } const env = parsed.data; // Validate provider-specific required variables if (env.BROWSER_PROVIDER === "stagehand") { if (!env.BROWSERBASE_API_KEY || !env.BROWSERBASE_PROJECT_ID) { console.error( "[grammarly-mcp:error] BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID are required when BROWSER_PROVIDER=stagehand", ); process.exit(1); } } else if (env.BROWSER_PROVIDER === "browser-use") { if (!env.BROWSER_USE_API_KEY || !env.BROWSER_USE_PROFILE_ID) { console.error( "[grammarly-mcp:error] BROWSER_USE_API_KEY and BROWSER_USE_PROFILE_ID are required when BROWSER_PROVIDER=browser-use", ); process.exit(1); } } // Claude SDK reads API keys from environment variables at call time. // If an API key is provided, set it for downstream SDK calls. // If not provided, Claude Code uses CLI authentication ('claude login'). if (env.CLAUDE_API_KEY) { process.env.CLAUDE_API_KEY ??= env.CLAUDE_API_KEY; process.env.ANTHROPIC_API_KEY ??= env.CLAUDE_API_KEY; } // Also propagate other API keys to process.env for SDK compatibility if (env.OPENAI_API_KEY) { process.env.OPENAI_API_KEY ??= env.OPENAI_API_KEY; } if (env.GOOGLE_GENERATIVE_AI_API_KEY || env.GEMINI_API_KEY) { process.env.GOOGLE_GENERATIVE_AI_API_KEY ??= env.GOOGLE_GENERATIVE_AI_API_KEY ?? env.GEMINI_API_KEY; } if (env.ANTHROPIC_API_KEY) { process.env.ANTHROPIC_API_KEY ??= env.ANTHROPIC_API_KEY; } // Default thresholds; can be overridden per-tool call via args. export const config: AppConfig = { // Environment isolation ignoreSystemEnv: env.IGNORE_SYSTEM_ENV, // Provider selection browserProvider: env.BROWSER_PROVIDER, // Browser Use Cloud (fallback) browserUseApiKey: env.BROWSER_USE_API_KEY, browserUseProfileId: env.BROWSER_USE_PROFILE_ID, // Browserbase + Stagehand (primary) browserbaseApiKey: env.BROWSERBASE_API_KEY, browserbaseProjectId: env.BROWSERBASE_PROJECT_ID, browserbaseSessionId: env.BROWSERBASE_SESSION_ID, browserbaseContextId: env.BROWSERBASE_CONTEXT_ID, stagehandModel: env.STAGEHAND_MODEL, stagehandCacheDir: env.STAGEHAND_CACHE_DIR, // Separate LLM provider controls stagehandLlmProvider: env.STAGEHAND_LLM_PROVIDER, rewriteLlmProvider: env.REWRITE_LLM_PROVIDER, // Claude model selection claudeModel: env.CLAUDE_MODEL, // Non-Claude model selection openaiModel: env.OPENAI_MODEL, googleModel: env.GOOGLE_MODEL, anthropicModel: env.ANTHROPIC_MODEL, // API keys for LLM provider detection claudeApiKey: env.CLAUDE_API_KEY, openaiApiKey: env.OPENAI_API_KEY, googleApiKey: env.GOOGLE_GENERATIVE_AI_API_KEY ?? env.GEMINI_API_KEY, anthropicApiKey: env.ANTHROPIC_API_KEY, // General settings llmRequestTimeoutMs: env.LLM_REQUEST_TIMEOUT_MS ?? env.CLAUDE_REQUEST_TIMEOUT_MS ?? 2 * 60 * 1000, connectTimeoutMs: env.CONNECT_TIMEOUT_MS ?? 30_000, logLevel: env.LOG_LEVEL, browserUseDefaultTimeoutMs: 5 * 60 * 1000, defaultMaxAiPercent: 10, defaultMaxPlagiarismPercent: 5, defaultMaxIterations: 5, }; /** * Shared helper to choose an LLM provider based on available API keys. * Priority: OpenAI > Google > Anthropic > Claude Code (CLI auth). */ export function detectProviderFromApiKeys( configLike: Pick< AppConfig, "openaiApiKey" | "googleApiKey" | "anthropicApiKey" | "claudeApiKey" >, ): LLMProvider { if (configLike.openaiApiKey) { return "openai"; } if (configLike.googleApiKey) { return "google"; } if (configLike.anthropicApiKey || configLike.claudeApiKey) { return "anthropic"; } return "claude-code"; } // ============================================================================= // Logging // ============================================================================= const LOG_LEVELS: LogLevel[] = ["debug", "info", "warn", "error"]; /** * Minimal logger that always writes to stderr. * * MCP JSON-RPC frames must go to stdout only. */ export function log(level: LogLevel, message: string, extra?: unknown): void { const configuredIndex = LOG_LEVELS.indexOf(config.logLevel); const levelIndex = LOG_LEVELS.indexOf(level); if (levelIndex < configuredIndex) { return; } const prefix = `[grammarly-mcp:${level}]`; if (typeof extra !== "undefined") { console.error(prefix, message, extra); } else { console.error(prefix, message); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/BjornMelin/grammarly-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server