Skip to main content
Glama

save_auth

Authenticate with a web application by executing login steps and save the session cookies and localStorage to a JSON file for later analysis.

Instructions

Authenticate with a web application and save the session for subsequent analysis. Navigates to the URL, executes login steps (click a button, fill a form, etc.), waits for the authenticated page to load, then saves cookies and localStorage to a JSON file. Overwrites the output file if it already exists.

Side effects: Writes a storageState JSON file to disk at outputPath. Launches a headed browser that interacts with the page (clicks, fills inputs). Not needed for public pages — only use when content is behind authentication.

Pass the output file path as storageState to analyze_url, trace_path, or analyze_pages to analyze authenticated content.

Steps format: Array of actions to perform in order. Each step is an object:

  • { click: 'button text or selector' } — click a button/link

  • { fill: ['input selector', 'value'] } — fill an input field

  • { wait: 2000 } — wait N milliseconds

  • { waitForUrl: '/dashboard' } — wait until URL contains this string

Example for a dev login: steps: [{ click: 'Dev Login' }, { waitForUrl: '/workspace' }] Example for form login: steps: [{ fill: ['#email', 'user@test.com'] }, { fill: ['#password', 'pass'] }, { click: 'Sign In' }, { waitForUrl: '/dashboard' }]

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
urlYesLogin page URL
stepsYesLogin steps to execute (see description for format)
outputPathNoFile path to save the storageState JSON (must be within cwd)tactual-auth.json
timeoutNoTimeout per step in ms

Implementation Reference

  • Core handler function `runSaveAuth` that authenticates with a web app and saves Playwright storageState. Validates URL, parses auth steps, acquires a browser, navigates to the login page, executes steps (click, fill, wait, waitForUrl), saves cookies/localStorage to a JSON file, and returns the result.
    export async function runSaveAuth(
      opts: SaveAuthOptions,
    ): Promise<SaveAuthResult> {
      const urlCheck = validateUrl(opts.url);
      if (!urlCheck.valid) {
        throw new SaveAuthError("invalid-url", `Invalid URL: ${urlCheck.error}`);
      }
    
      const outputPath = opts.outputPath ?? "tactual-auth.json";
      const resolved = resolvePath(outputPath);
      if (opts.restrictOutputToCwd) {
        const rel = relativePath(process.cwd(), resolved);
        if (rel.startsWith("..") || isAbsolute(rel)) {
          throw new SaveAuthError(
            "invalid-output-path",
            `Invalid outputPath: must be within the current working directory (${process.cwd()}). Resolved: ${resolved}`,
          );
        }
      }
    
      const timeout = opts.timeout ?? 30000;
      const steps = parseAuthSteps(opts.steps);
      const fs = await import("fs/promises");
    
      const { browser, owned } = await acquireBrowser(
        {},
        { useSharedPool: opts.useSharedBrowserPool === true },
      );
      let context: BrowserContext | undefined;
      try {
        context = await browser.newContext();
        const page = await context.newPage();
    
        await page.goto(urlCheck.url!, { waitUntil: "domcontentloaded", timeout });
        await page.waitForTimeout(2000);
    
        for (const step of steps) {
          if ("click" in step) {
            const target = step.click;
            const byRole = page
              .getByRole("button", { name: target })
              .or(page.getByRole("link", { name: target }))
              .or(page.getByText(target, { exact: false }));
            const exists = (await byRole.count()) > 0;
            if (exists) {
              await byRole.first().click({ timeout });
            } else {
              await page.click(target, { timeout });
            }
          } else if ("fill" in step) {
            await page.fill(step.fill[0], step.fill[1]);
          } else if ("wait" in step) {
            await page.waitForTimeout(Math.min(step.wait, 60000));
          } else if ("waitForUrl" in step) {
            await page.waitForURL(`**${step.waitForUrl}**`, { timeout });
          }
        }
    
        await page.waitForTimeout(2000);
    
        const state = await context.storageState();
        await fs.writeFile(resolved, JSON.stringify(state, null, 2), {
          mode: opts.fileMode ?? 0o600,
        });
    
        const cookieCount = state.cookies?.length ?? 0;
        const originCount = state.origins?.length ?? 0;
        return {
          saved: outputPath,
          cookies: cookieCount,
          origins: originCount,
          currentUrl: page.url(),
          message: `Auth state saved. Pass storageState="${outputPath}" to analyze-url/analyze_url.`,
        };
      } finally {
        await context?.close().catch(() => {});
        if (owned) await closeBrowser(browser);
      }
    }
  • MCP tool registration for 'save_auth'. Defines input schema (url, steps array, outputPath, timeout) and invokes the pipeline's `runSaveAuth`. Wraps result as MCP content response with error handling for SaveAuthError.
    export function registerSaveAuth(server: McpServer): void {
      server.registerTool(
        "save_auth",
        {
          description:
            "Authenticate with a web application and save the session for subsequent analysis. " +
            "Navigates to the URL, executes login steps (click a button, fill a form, etc.), " +
            "waits for the authenticated page to load, then saves cookies and localStorage " +
            "to a JSON file. Overwrites the output file if it already exists.\n\n" +
            "**Side effects**: Writes a storageState JSON file to disk at `outputPath`. " +
            "Launches a headed browser that interacts with the page (clicks, fills inputs). " +
            "Not needed for public pages — only use when content is behind authentication.\n\n" +
            "Pass the output file path as `storageState` to analyze_url, trace_path, " +
            "or analyze_pages to analyze authenticated content.\n\n" +
            "**Steps format**: Array of actions to perform in order. Each step is an object:\n" +
            "- `{ click: 'button text or selector' }` — click a button/link\n" +
            "- `{ fill: ['input selector', 'value'] }` — fill an input field\n" +
            "- `{ wait: 2000 }` — wait N milliseconds\n" +
            "- `{ waitForUrl: '/dashboard' }` — wait until URL contains this string\n\n" +
            "Example for a dev login: `steps: [{ click: 'Dev Login' }, { waitForUrl: '/workspace' }]`\n" +
            "Example for form login: `steps: [{ fill: ['#email', 'user@test.com'] }, { fill: ['#password', 'pass'] }, { click: 'Sign In' }, { waitForUrl: '/dashboard' }]`",
          inputSchema: {
            url: z.string().describe("Login page URL"),
            steps: z
              .array(AuthStepInputSchema)
              .describe("Login steps to execute (see description for format)"),
            outputPath: z
              .string()
              .default("tactual-auth.json")
              .describe("File path to save the storageState JSON (must be within cwd)"),
            timeout: z.number().default(30000).describe("Timeout per step in ms"),
          },
        },
        async ({ url, steps, outputPath, timeout }) => {
          try {
            const result = await runSaveAuth({
              url,
              steps: steps as Record<string, unknown>[],
              outputPath,
              timeout,
              restrictOutputToCwd: true,
              useSharedBrowserPool: true,
            });
            return {
              content: [
                { type: "text" as const, text: JSON.stringify(result, null, 2) },
              ],
            };
          } catch (err) {
            const text =
              err instanceof SaveAuthError
                ? err.message
                : `Auth failed: ${err instanceof Error ? err.message : String(err)}`;
            return { content: [{ type: "text" as const, text }], isError: true };
          }
        },
      );
    }
  • Type definitions: SaveAuthOptions (input config), SaveAuthResult (output shape with saved path, cookie/origin counts, currentUrl), AuthStep union type (click, fill, wait, waitForUrl), and Zod schemas (AuthStepSchema, AuthStepsSchema) for validation.
    export interface SaveAuthOptions {
      url: string;
      steps: AuthStep[] | Record<string, unknown>[];
      outputPath?: string;
      timeout?: number;
      /** MCP callers pass true; CLI passes false (explicit path choice). */
      restrictOutputToCwd?: boolean;
      /** File mode for the saved JSON. Defaults to 0o600. */
      fileMode?: number;
      /** Long-lived MCP/server callers can opt into the shared browser pool. */
      useSharedBrowserPool?: boolean;
    }
    
    export interface SaveAuthResult {
      saved: string;
      cookies: number;
      origins: number;
      currentUrl: string;
      message: string;
    }
  • Helper function `parseAuthSteps` that validates the steps array against the Zod schema and throws a SaveAuthError with details on invalid steps.
    function parseAuthSteps(steps: AuthStep[] | Record<string, unknown>[]): AuthStep[] {
      const parsed = AuthStepsSchema.safeParse(steps);
      if (parsed.success) return parsed.data;
    
      const firstIssue = parsed.error.issues[0];
      const path = firstIssue.path.length > 0 ? ` at steps.${firstIssue.path.join(".")}` : "";
      throw new SaveAuthError(
        "invalid-step",
        `${firstIssue.message}${path}. Valid step types: click, fill, wait, waitForUrl.`,
      );
    }
  • Helper function `stepsFromCliFlags` that builds an AuthStep array from CLI flag values (fill pairs as selector=value, click text, waitForUrl pattern).
    export function stepsFromCliFlags(opts: {
      fill?: string[];
      click?: string;
      waitForUrl?: string;
    }): AuthStep[] {
      const steps: AuthStep[] = [];
      if (opts.fill) {
        for (const pair of opts.fill) {
          const [selector, value] = pair.split("=", 2);
          if (selector && value !== undefined) {
            steps.push({ fill: [selector, value] });
          }
        }
      }
      if (opts.click) steps.push({ click: opts.click });
      if (opts.waitForUrl) steps.push({ waitForUrl: opts.waitForUrl });
      return steps;
    }
  • src/mcp/index.ts:40-40 (registration)
    Registration of save_auth tool in the MCP server via `registerSaveAuth(server)`.
    registerSaveAuth(server);
  • src/cli/index.ts:32-32 (registration)
    Registration of save-auth CLI command via `registerSaveAuth(program)`.
    registerSaveAuth(program);
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations provided, but the description thoroughly discloses side effects: file writing, browser launching, interaction, and overwriting behavior. This fully covers behavioral transparency.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Well-structured with sections and examples. Slightly lengthy due to detailed examples, but each part serves a purpose. Could be trimmed slightly without losing clarity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Complete coverage: explains operation, parameters, side effects, integration with sibling tools, and provides examples. No output schema needed as tool writes to disk.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, but description adds substantial value: explains steps format in detail, provides examples, constrains outputPath to cwd, and clarifies timeout usage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: authenticate and save session for subsequent analysis. It differentiates from siblings (analyze_pages, analyze_url, etc.) by focusing on login and session capture.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Description explicitly states when not to use ('Not needed for public pages') and how to use output with other tools. Though it could mention alternatives, it provides clear context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/tactual-dev/tactual'

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