fidelity_login
Login to Fidelity using credentials directly or via environment variables. Supports automatic TOTP 2FA or SMS 2FA with a follow-up step for verification.
Instructions
Log in to Fidelity. Supports TOTP 2FA (automatic) and SMS 2FA (requires follow-up with fidelity_submit_2fa). Credentials can be passed directly or via env vars FIDELITY_USERNAME, FIDELITY_PASSWORD, FIDELITY_TOTP_SECRET.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| username | No | Fidelity username. Falls back to FIDELITY_USERNAME env var. | |
| password | No | Fidelity password. Falls back to FIDELITY_PASSWORD env var. | |
| totp_secret | No | TOTP secret for authenticator app 2FA. Falls back to FIDELITY_TOTP_SECRET env var. |
Implementation Reference
- src/index.ts:34-97 (registration)Registration of the fidelity_login tool on the MCP server, defining its schema (username, password, totp_secret) and delegating to the login() function from ./auth.js.
server.tool( "fidelity_login", "Log in to Fidelity. Supports TOTP 2FA (automatic) and SMS 2FA (requires follow-up with fidelity_submit_2fa). Credentials can be passed directly or via env vars FIDELITY_USERNAME, FIDELITY_PASSWORD, FIDELITY_TOTP_SECRET.", { username: z .string() .optional() .describe( "Fidelity username. Falls back to FIDELITY_USERNAME env var." ), password: z .string() .optional() .describe( "Fidelity password. Falls back to FIDELITY_PASSWORD env var." ), totp_secret: z .string() .optional() .describe( "TOTP secret for authenticator app 2FA. Falls back to FIDELITY_TOTP_SECRET env var." ), }, async ({ username, password, totp_secret }) => { const user = username ?? process.env.FIDELITY_USERNAME; const pass = password ?? process.env.FIDELITY_PASSWORD; const totp = totp_secret ?? process.env.FIDELITY_TOTP_SECRET; if (!user || !pass) { return { content: [ { type: "text", text: "Error: Username and password are required. Provide them as arguments or set FIDELITY_USERNAME and FIDELITY_PASSWORD environment variables.", }, ], isError: true, }; } try { const result = await login(getConfig(), user, pass, totp); return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], isError: !result.success, }; } catch (e) { return { content: [ { type: "text", text: `Login failed: ${e instanceof Error ? e.message : String(e)}`, }, ], isError: true, }; } } ); - src/index.ts:37-56 (schema)Zod schema defining the input parameters for fidelity_login: username, password (both optional, fallback to env vars), and totp_secret (optional, fallback to FIDELITY_TOTP_SECRET env var).
{ username: z .string() .optional() .describe( "Fidelity username. Falls back to FIDELITY_USERNAME env var." ), password: z .string() .optional() .describe( "Fidelity password. Falls back to FIDELITY_PASSWORD env var." ), totp_secret: z .string() .optional() .describe( "TOTP secret for authenticator app 2FA. Falls back to FIDELITY_TOTP_SECRET env var." ), }, - src/auth.ts:22-74 (handler)Main login handler function. Initializes the browser via Playwright, navigates to Fidelity login page, fills credentials, clicks login, checks for 2FA requirement, and returns a LoginResult indicating success or need for SMS 2FA.
export async function login( config: FidelityConfig, username: string, password: string, totpSecret?: string ): Promise<LoginResult> { const page = await initBrowser(config); // Navigate to login page (double navigation to handle redirects) await page.goto(LOGIN_URLS[0], { waitUntil: "domcontentloaded" }); await page.waitForTimeout(3000); // If redirected to new signin URL, use that; otherwise retry original if (!isOnLoginPage(page.url())) { await page.goto(LOGIN_URLS[0], { waitUntil: "domcontentloaded" }); } await page.waitForTimeout(2000); // Fill credentials const usernameField = page.getByLabel("Username", { exact: true }); await usernameField.waitFor({ state: "visible", timeout: config.timeout }); await usernameField.fill(username); const passwordField = page.getByLabel("Password", { exact: true }); await passwordField.fill(password); // Click login await page.getByRole("button", { name: "Log in" }).click(); // Wait for loading await waitForLoadingCompleteDouble(page, config.timeout); // Check if we landed on summary (no 2FA needed) if (page.url().includes("summary")) { await saveSession(); return { success: true, needsSms2FA: false, message: "Login successful. No 2FA required.", }; } // Still on login/signin page - 2FA required if (isOnLoginPage(page.url())) { return await handle2FA(page, config, totpSecret); } return { success: false, needsSms2FA: false, message: `Unexpected URL after login: ${page.url()}`, }; } - src/auth.ts:76-213 (helper)handle2FA helper function that processes 2FA after login: either automatic TOTP code generation/submission (if totpSecret provided) or fallback to SMS code request via 'Text me the code' workflow.
async function handle2FA( page: Page, config: FidelityConfig, totpSecret?: string ): Promise<LoginResult> { // Wait for 2FA widget try { await page.waitForSelector("#dom-widget div", { state: "visible", timeout: 15000, }); } catch { return { success: false, needsSms2FA: false, message: "2FA widget did not appear. Login may have failed.", }; } // Check if TOTP authenticator code is requested const totpHeading = page.getByRole("heading", { name: "Enter the code from your", }); if (totpSecret) { try { await totpHeading.waitFor({ state: "visible", timeout: 5000 }); // Generate TOTP code const totp = new OTPAuth.TOTP({ secret: OTPAuth.Secret.fromBase32(totpSecret), digits: 6, period: 30, }); const code = totp.generate(); // Fill code const codeInput = page.getByPlaceholder("XXXXXX"); await codeInput.fill(code); // Check "Don't ask me again" try { const rememberLabel = page .locator("label") .filter({ hasText: "Don't ask me again on this" }); if (await rememberLabel.isVisible()) { await rememberLabel.click(); } } catch { // Optional checkbox } // Submit await page.getByRole("button", { name: "Continue" }).click(); // Wait for redirect to summary await page.waitForURL("**/portfolio/summary", { timeout: config.timeout, }); await waitForLoadingComplete(page); await saveSession(); return { success: true, needsSms2FA: false, message: "Login successful with TOTP authentication.", }; } catch (e) { return { success: false, needsSms2FA: false, message: `TOTP authentication failed: ${e instanceof Error ? e.message : String(e)}`, }; } } // No TOTP secret - try to fall back to SMS try { // Check for "Try another way" link (push notification page) const tryAnotherWay = page.getByRole("link", { name: "Try another way" }); if (await tryAnotherWay.isVisible({ timeout: 3000 })) { // Check "Don't ask me again" first try { const rememberLabel = page .locator("label") .filter({ hasText: "Don't ask me again on this" }); if (await rememberLabel.isVisible()) { await rememberLabel.click(); } } catch { // Optional } await tryAnotherWay.click(); await page.waitForTimeout(2000); } } catch { // Not on push notification page } // Try to click "Text me the code" try { const textMeBtn = page.getByRole("button", { name: "Text me the code" }); await textMeBtn.waitFor({ state: "visible", timeout: 5000 }); await textMeBtn.click(); await page.waitForTimeout(2000); // Click the code input to focus it const codeInput = page.getByPlaceholder("XXXXXX"); await codeInput.click(); return { success: true, needsSms2FA: true, message: "SMS code sent. Use fidelity_submit_2fa tool to enter the code.", }; } catch { // Check if TOTP is required but no secret was provided try { if (await totpHeading.isVisible()) { return { success: false, needsSms2FA: false, message: "Authenticator app code required but no TOTP secret provided. Set FIDELITY_TOTP_SECRET environment variable.", }; } } catch { // Ignore } return { success: false, needsSms2FA: false, message: "Could not initiate 2FA. Check your Fidelity security settings.", }; } } - src/browser.ts:23-78 (helper)initBrowser helper - initializes Playwright Firefox browser, loads saved session state, and returns a Page instance. Called by the login() handler.
export async function initBrowser(config: FidelityConfig): Promise<Page> { if (page && !page.isClosed()) { return page; } const sessionPath = getSessionPath(config); currentConfig = config; // Ensure session directory exists fs.mkdirSync(config.sessionDir, { recursive: true }); // Initialize session file if it doesn't exist if (!fs.existsSync(sessionPath)) { fs.writeFileSync(sessionPath, JSON.stringify({ cookies: [], origins: [] })); } browser = await firefox.launch({ headless: config.headless, args: ["--disable-webgl", "--disable-software-rasterizer"], }); // Load saved session state let storageState: string | undefined; try { const data = fs.readFileSync(sessionPath, "utf-8"); const parsed = JSON.parse(data); if (parsed.cookies || parsed.origins) { storageState = sessionPath; } } catch { // No valid session, start fresh } context = await browser.newContext({ storageState, userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0", viewport: { width: 1920, height: 1080 }, locale: "en-US", }); page = await context.newPage(); // Basic stealth: override navigator properties await page.addInitScript(() => { Object.defineProperty(navigator, "webdriver", { get: () => false }); Object.defineProperty(navigator, "plugins", { get: () => [1, 2, 3, 4, 5], }); Object.defineProperty(navigator, "languages", { get: () => ["en-US", "en"], }); }); return page; }