import { chromium, Browser, BrowserContext, Page } from "playwright";
import { existsSync, mkdirSync } from "fs";
import { homedir } from "os";
import { join } from "path";
export const SESSION_DIR = join(homedir(), ".config", "ddb-mcp");
export const SESSION_PATH = join(SESSION_DIR, "session.json");
let browserInstance: Browser | null = null;
let contextInstance: BrowserContext | null = null;
export async function getBrowser(): Promise<Browser> {
if (browserInstance) return browserInstance;
browserInstance = await chromium.launch({
headless: false,
args: [
"--no-sandbox",
"--disable-blink-features=AutomationControlled",
],
});
return browserInstance;
}
export async function getContext(browser: Browser): Promise<BrowserContext> {
if (contextInstance) return contextInstance;
if (!existsSync(SESSION_DIR)) {
mkdirSync(SESSION_DIR, { recursive: true });
}
if (existsSync(SESSION_PATH)) {
contextInstance = await browser.newContext({
storageState: SESSION_PATH,
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
viewport: { width: 1280, height: 800 },
});
} else {
contextInstance = await browser.newContext({
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
viewport: { width: 1280, height: 800 },
});
}
return contextInstance;
}
export async function saveSession(context: BrowserContext): Promise<void> {
if (!existsSync(SESSION_DIR)) {
mkdirSync(SESSION_DIR, { recursive: true });
}
await context.storageState({ path: SESSION_PATH });
}
export async function isLoggedIn(page: Page): Promise<boolean> {
try {
await page.goto("https://www.dndbeyond.com", { waitUntil: "domcontentloaded", timeout: 15000 });
await page.waitForTimeout(2000);
// If we got redirected off dndbeyond.com (e.g. to Wizards login), we're not logged in
const currentUrl = page.url();
if (!currentUrl.includes("dndbeyond.com") || currentUrl.includes("/login") || currentUrl.includes("/sign-in")) {
return false;
}
// Check for visible "Sign In" / "Log In" text — these only appear when NOT logged in
return await page.evaluate(() => {
const allElements = Array.from(document.querySelectorAll("a, button"));
const signInEl = allElements.find((el) => {
const text = (el.textContent || "").trim().toLowerCase();
return text === "sign in" || text === "log in";
});
return !signInEl;
});
} catch {
return false;
}
}
export async function getPage(context: BrowserContext): Promise<Page> {
const pages = context.pages();
if (pages.length > 0) return pages[0];
return context.newPage();
}
export async function closeBrowser(): Promise<void> {
if (contextInstance) {
await contextInstance.close();
contextInstance = null;
}
if (browserInstance) {
await browserInstance.close();
browserInstance = null;
}
}