Skip to main content
Glama

interceptor_browser_list_cookies

Retrieve cookies from a browser target with filtering, pagination, and value previews to inspect or debug stored session data.

Instructions

List cookies from the browser context with pagination and truncated value previews.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
target_idYesTarget ID from interceptor_browser_launch or interceptor_camoufox_launch
url_filterNoFilter cookies by domain/path substring
domain_filterNoFilter cookies by domain substring
name_filterNoFilter cookies by name substring
offsetNoOffset into results (default: 0)
limitNoMax cookies to return (default: 50, max: 500)
value_max_charsNoMax characters for cookie value previews (default: 256)
fullNoReturn full cookie values instead of previews (capped at 20000 chars). Overrides value_max_chars.
sortNoSort order (default: name)name

Implementation Reference

  • The handler function for interceptor_browser_list_cookies tool. Fetches cookies from the browser context, applies filters (url, domain, name), sorts, paginates, truncates values, and returns summaries with cookie_id for each cookie.
    async ({ target_id, url_filter, domain_filter, name_filter, offset, limit, value_max_chars, full, sort }) => {
      try {
        const context = await getContextForTarget(target_id);
        const cookies = await context.cookies();
    
        const urlNeedle = url_filter?.toLowerCase();
        const domainNeedle = domain_filter?.toLowerCase();
        const nameNeedle = name_filter?.toLowerCase();
    
        const filtered = cookies.filter((c) => {
          if (urlNeedle && !`${c.domain}${c.path}`.toLowerCase().includes(urlNeedle)) return false;
          if (domainNeedle && !c.domain.toLowerCase().includes(domainNeedle)) return false;
          if (nameNeedle && !c.name.toLowerCase().includes(nameNeedle)) return false;
          return true;
        });
    
        const sorted = filtered.sort((a, b) => {
          switch (sort) {
            case "domain": return a.domain.localeCompare(b.domain) || a.name.localeCompare(b.name);
            case "expires": return (a.expires ?? 0) - (b.expires ?? 0) || a.domain.localeCompare(b.domain) || a.name.localeCompare(b.name);
            case "name":
            default: return a.name.localeCompare(b.name) || a.domain.localeCompare(b.domain);
          }
        });
    
        const total = sorted.length;
        const o = normalizeOffset(offset);
        const l = normalizeLimit(limit);
        const page = sorted.slice(o, o + l);
    
        const valueCap = full
          ? HARD_VALUE_CAP_CHARS
          : Math.max(0, Math.min(HARD_VALUE_CAP_CHARS, Math.trunc(value_max_chars ?? DEFAULT_VALUE_MAX_CHARS)));
    
        const summaries = page.map((c) => {
          const capped = capValue(c.value, valueCap);
          const base = {
            cookie_id: cookieStableId(c),
            name: c.name,
            domain: c.domain,
            path: c.path,
            expires: c.expires ?? null,
            httpOnly: c.httpOnly,
            secure: c.secure,
            sameSite: c.sameSite ?? null,
            value_length: capped.valueLength,
            value_truncated: capped.truncated,
          };
          return full
            ? { ...base, value: capped.value }
            : { ...base, value_preview: capped.value };
        });
    
        return {
          content: [{
            type: "text",
            text: truncateResult({
              status: "success",
              target_id,
              total,
              offset: o,
              limit: l,
              showing: summaries.length,
              cookies: summaries,
            }),
          }],
        };
      } catch (e) {
        return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: errorToString(e) }) }] };
      }
    },
  • Zod schema / input validation for interceptor_browser_list_cookies. Defines parameters: target_id, url_filter, domain_filter, name_filter, offset, limit, value_max_chars, full, and sort with their types, defaults, and descriptions.
    {
      target_id: z.string().describe("Target ID from interceptor_browser_launch or interceptor_camoufox_launch"),
      url_filter: z.string().optional().describe("Filter cookies by domain/path substring"),
      domain_filter: z.string().optional().describe("Filter cookies by domain substring"),
      name_filter: z.string().optional().describe("Filter cookies by name substring"),
      offset: z.number().optional().default(0).describe("Offset into results (default: 0)"),
      limit: z.number().optional().default(DEFAULT_LIST_LIMIT).describe("Max cookies to return (default: 50, max: 500)"),
      value_max_chars: z.number().optional().default(DEFAULT_VALUE_MAX_CHARS)
        .describe("Max characters for cookie value previews (default: 256)"),
      full: z.boolean().optional().default(false)
        .describe(`Return full cookie values instead of previews (capped at ${HARD_VALUE_CAP_CHARS} chars). Overrides value_max_chars.`),
      sort: z.enum(["name", "domain", "expires"]).optional().default("name").describe("Sort order (default: name)"),
    },
  • Registration of the tool with MCP server via server.tool('interceptor_browser_list_cookies', ...). This is the call that registers the tool name, description, schema, and handler with the McpServer instance.
    server.tool(
  • The registerDevToolsTools function is exported and called from src/index.ts:69 to register all devtools tools including interceptor_browser_list_cookies.
    export function registerDevToolsTools(server: McpServer): void {
      // ── snapshot ──────────────────────────────────────────────────
    
      server.tool(
        "interceptor_browser_snapshot",
        "Take an ARIA accessibility snapshot of the bound page (YAML-formatted role tree). " +
        "Great for LLM-driven page understanding without parsing HTML.",
        {
          target_id: z.string().describe("Target ID from interceptor_browser_launch or interceptor_camoufox_launch"),
          selector: z.string().optional().default("body").describe("Root selector to snapshot (default: 'body')"),
          mode: z.enum(["default", "ai"]).optional().default("default").describe("Snapshot mode — 'ai' adds ref attributes for locator reuse"),
        },
        async ({ target_id, selector, mode }) => {
          try {
            const page = await getPageForTarget(target_id);
            const snapshot = await page.locator(selector).ariaSnapshot({ mode });
            return {
              content: [{
                type: "text",
                text: truncateResult({
                  status: "success",
                  target_id,
                  url: page.url(),
                  title: await page.title().catch(() => ""),
                  root: selector,
                  snapshot,
                }),
              }],
            };
          } catch (e) {
            return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: errorToString(e) }) }] };
          }
        },
      );
    
      // ── screenshot ────────────────────────────────────────────────
    
      server.tool(
        "interceptor_browser_screenshot",
        "Take a screenshot of the bound page. Saves to file_path if provided; otherwise reports byte count without embedding the image.",
        {
          target_id: z.string().describe("Target ID from interceptor_browser_launch or interceptor_camoufox_launch"),
          file_path: z.string().optional().describe("Optional path to save screenshot"),
          format: z.enum(["png", "jpeg"]).optional().default("png").describe("Image format (default: png)"),
          full_page: z.boolean().optional().default(false).describe("Capture the full scrollable page"),
          quality: z.number().optional().describe("JPEG quality 0-100 (ignored for png)"),
        },
        async ({ target_id, file_path, format, full_page, quality }) => {
          try {
            const page = await getPageForTarget(target_id);
            const buffer = await page.screenshot({
              type: format,
              fullPage: full_page,
              ...(format === "jpeg" && quality !== undefined ? { quality } : {}),
            });
    
            let saved = false;
            if (file_path) {
              await mkdir(dirname(file_path), { recursive: true });
              await writeFile(file_path, buffer);
              saved = true;
            }
    
            return {
              content: [{
                type: "text",
                text: truncateResult({
                  status: "success",
                  target_id,
                  format,
                  full_page,
                  bytes: buffer.length,
                  ...(file_path ? { file_path, saved } : {}),
                }),
              }],
            };
          } catch (e) {
            return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: errorToString(e) }) }] };
          }
        },
      );
    
      // ── console ───────────────────────────────────────────────────
    
      server.tool(
        "interceptor_browser_list_console",
        "List console messages buffered since the browser was launched. Types: log, info, warning, error, debug, etc.",
        {
          target_id: z.string().describe("Target ID from interceptor_browser_launch or interceptor_camoufox_launch"),
          types: z.array(z.string()).optional().describe("Filter by console message types"),
          text_filter: z.string().optional().describe("Filter by text substring"),
          offset: z.number().optional().default(0).describe("Offset into results (default: 0)"),
          limit: z.number().optional().default(DEFAULT_LIST_LIMIT).describe("Max messages to return (default: 50, max: 500)"),
        },
        async ({ target_id, types, text_filter, offset, limit }) => {
          try {
            let msgs = await getConsoleBufferForTarget(target_id);
            if (types && types.length > 0) {
              const set = new Set(types.map((t) => t.toLowerCase()));
              msgs = msgs.filter((m) => set.has(m.type.toLowerCase()));
            }
            if (text_filter) {
              const needle = text_filter.toLowerCase();
              msgs = msgs.filter((m) => m.text.toLowerCase().includes(needle));
            }
            const total = msgs.length;
            const o = normalizeOffset(offset);
            const l = normalizeLimit(limit);
            const page = msgs.slice(o, o + l);
    
            return {
              content: [{
                type: "text",
                text: truncateResult({
                  status: "success",
                  target_id,
                  total,
                  offset: o,
                  limit: l,
                  showing: page.length,
                  messages: page,
                }),
              }],
            };
          } catch (e) {
            return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: errorToString(e) }) }] };
          }
        },
      );
    
      // ── cookies ───────────────────────────────────────────────────
    
      server.tool(
        "interceptor_browser_list_cookies",
        "List cookies from the browser context with pagination and truncated value previews.",
        {
          target_id: z.string().describe("Target ID from interceptor_browser_launch or interceptor_camoufox_launch"),
          url_filter: z.string().optional().describe("Filter cookies by domain/path substring"),
          domain_filter: z.string().optional().describe("Filter cookies by domain substring"),
          name_filter: z.string().optional().describe("Filter cookies by name substring"),
          offset: z.number().optional().default(0).describe("Offset into results (default: 0)"),
          limit: z.number().optional().default(DEFAULT_LIST_LIMIT).describe("Max cookies to return (default: 50, max: 500)"),
          value_max_chars: z.number().optional().default(DEFAULT_VALUE_MAX_CHARS)
            .describe("Max characters for cookie value previews (default: 256)"),
          full: z.boolean().optional().default(false)
            .describe(`Return full cookie values instead of previews (capped at ${HARD_VALUE_CAP_CHARS} chars). Overrides value_max_chars.`),
          sort: z.enum(["name", "domain", "expires"]).optional().default("name").describe("Sort order (default: name)"),
        },
        async ({ target_id, url_filter, domain_filter, name_filter, offset, limit, value_max_chars, full, sort }) => {
          try {
            const context = await getContextForTarget(target_id);
            const cookies = await context.cookies();
    
            const urlNeedle = url_filter?.toLowerCase();
            const domainNeedle = domain_filter?.toLowerCase();
            const nameNeedle = name_filter?.toLowerCase();
    
            const filtered = cookies.filter((c) => {
              if (urlNeedle && !`${c.domain}${c.path}`.toLowerCase().includes(urlNeedle)) return false;
              if (domainNeedle && !c.domain.toLowerCase().includes(domainNeedle)) return false;
              if (nameNeedle && !c.name.toLowerCase().includes(nameNeedle)) return false;
              return true;
            });
    
            const sorted = filtered.sort((a, b) => {
              switch (sort) {
                case "domain": return a.domain.localeCompare(b.domain) || a.name.localeCompare(b.name);
                case "expires": return (a.expires ?? 0) - (b.expires ?? 0) || a.domain.localeCompare(b.domain) || a.name.localeCompare(b.name);
                case "name":
                default: return a.name.localeCompare(b.name) || a.domain.localeCompare(b.domain);
              }
            });
    
            const total = sorted.length;
            const o = normalizeOffset(offset);
            const l = normalizeLimit(limit);
            const page = sorted.slice(o, o + l);
    
            const valueCap = full
              ? HARD_VALUE_CAP_CHARS
              : Math.max(0, Math.min(HARD_VALUE_CAP_CHARS, Math.trunc(value_max_chars ?? DEFAULT_VALUE_MAX_CHARS)));
    
            const summaries = page.map((c) => {
              const capped = capValue(c.value, valueCap);
              const base = {
                cookie_id: cookieStableId(c),
                name: c.name,
                domain: c.domain,
                path: c.path,
                expires: c.expires ?? null,
                httpOnly: c.httpOnly,
                secure: c.secure,
                sameSite: c.sameSite ?? null,
                value_length: capped.valueLength,
                value_truncated: capped.truncated,
              };
              return full
                ? { ...base, value: capped.value }
                : { ...base, value_preview: capped.value };
            });
    
            return {
              content: [{
                type: "text",
                text: truncateResult({
                  status: "success",
                  target_id,
                  total,
                  offset: o,
                  limit: l,
                  showing: summaries.length,
                  cookies: summaries,
                }),
              }],
            };
          } catch (e) {
            return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: errorToString(e) }) }] };
          }
        },
      );
    
      server.tool(
        "interceptor_browser_get_cookie",
        "Get one cookie by cookie_id with full value (subject to a hard cap to keep output bounded).",
        {
          target_id: z.string().describe("Target ID from interceptor_browser_launch or interceptor_camoufox_launch"),
          cookie_id: z.string().describe("cookie_id from interceptor_browser_list_cookies"),
          value_max_chars: z.number().optional().default(HARD_VALUE_CAP_CHARS)
            .describe(`Max characters for cookie value (default: ${HARD_VALUE_CAP_CHARS})`),
        },
        async ({ target_id, cookie_id, value_max_chars }) => {
          try {
            const context = await getContextForTarget(target_id);
            const cookies = await context.cookies();
            const found = cookies.find((c) => cookieStableId(c) === cookie_id) ?? null;
            if (!found) {
              return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: `Cookie '${cookie_id}' not found. Re-run list tool.` }) }] };
            }
            const capped = capValue(found.value, Math.max(0, Math.min(HARD_VALUE_CAP_CHARS, Math.trunc(value_max_chars ?? HARD_VALUE_CAP_CHARS))));
            return {
              content: [{
                type: "text",
                text: truncateResult({
                  status: "success",
                  target_id,
                  cookie_id,
                  cookie: { ...found, value: capped.value },
                  value_length: capped.valueLength,
                  value_truncated: capped.truncated,
                  value_max_chars: capped.maxChars,
                }),
              }],
            };
          } catch (e) {
            return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: errorToString(e) }) }] };
          }
        },
      );
    
      // ── storage ───────────────────────────────────────────────────
    
      server.tool(
        "interceptor_browser_list_storage_keys",
        "List localStorage/sessionStorage keys for the current origin with pagination and truncated value previews.",
        {
          target_id: z.string().describe("Target ID from interceptor_browser_launch or interceptor_camoufox_launch"),
          storage_type: z.enum(["local", "session"]).describe("Storage type"),
          origin: z.string().optional().describe("Optional origin override (must match current page origin)"),
          key_filter: z.string().optional().describe("Filter by key substring"),
          offset: z.number().optional().default(0).describe("Offset into results (default: 0)"),
          limit: z.number().optional().default(DEFAULT_LIST_LIMIT).describe("Max items to return (default: 50, max: 500)"),
          value_max_chars: z.number().optional().default(DEFAULT_VALUE_MAX_CHARS)
            .describe("Max characters for storage value previews (default: 256)"),
        },
        async ({ target_id, storage_type, origin, key_filter, offset, limit, value_max_chars }) => {
          try {
            const page = await getPageForTarget(target_id);
            const pageUrl = page.url();
            const currentOrigin = getOriginFromUrl(pageUrl);
            if (!currentOrigin) {
              return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: `No http(s) origin available for current page URL: '${pageUrl}'` }) }] };
            }
            if (origin && origin !== currentOrigin) {
              return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: `origin '${origin}' does not match current origin '${currentOrigin}'. Navigate first.` }) }] };
            }
    
            const previewLen = Math.max(0, Math.min(HARD_VALUE_CAP_CHARS, Math.trunc(value_max_chars ?? DEFAULT_VALUE_MAX_CHARS)));
            const keyNeedle = (key_filter ?? "").toLowerCase();
    
            const items = await page.evaluate(
              ({ stType, keyFilter, maxChars }) => {
                const storage = stType === "local" ? localStorage : sessionStorage;
                const out: { key: string; valuePreview: string; valueLength: number }[] = [];
                for (let i = 0; i < storage.length; i++) {
                  const k = storage.key(i);
                  if (typeof k !== "string") continue;
                  if (keyFilter && !k.toLowerCase().includes(keyFilter)) continue;
                  const raw = storage.getItem(k);
                  const v = typeof raw === "string" ? raw : "";
                  out.push({ key: k, valuePreview: maxChars > 0 ? v.slice(0, maxChars) : "", valueLength: v.length });
                }
                out.sort((a, b) => a.key.localeCompare(b.key));
                return out;
              },
              { stType: storage_type, keyFilter: keyNeedle, maxChars: previewLen },
            );
    
            const total = items.length;
            const o = normalizeOffset(offset);
            const l = normalizeLimit(limit);
            const pageItems = items.slice(o, o + l);
    
            const summaries = pageItems.map((x) => ({
              item_id: `st.${storage_type}.${toBase64UrlUtf8(currentOrigin)}.${toBase64UrlUtf8(x.key)}`,
              key: x.key,
              value_preview: x.valuePreview,
              value_length: x.valueLength,
              value_truncated: previewLen > 0 ? x.valueLength > previewLen : (x.valueLength > 0),
            }));
    
            return {
              content: [{
                type: "text",
                text: truncateResult({
                  status: "success",
                  target_id,
                  origin: currentOrigin,
                  storage_type,
                  total,
                  offset: o,
                  limit: l,
                  showing: summaries.length,
                  items: summaries,
                }),
              }],
            };
          } catch (e) {
            return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: errorToString(e) }) }] };
          }
        },
      );
    
      server.tool(
        "interceptor_browser_get_storage_value",
        "Get one localStorage/sessionStorage value by item_id.",
        {
          target_id: z.string().describe("Target ID from interceptor_browser_launch or interceptor_camoufox_launch"),
          storage_type: z.enum(["local", "session"]).describe("Storage type"),
          item_id: z.string().describe("item_id from interceptor_browser_list_storage_keys"),
          origin: z.string().optional().describe("Optional origin override (must match current page origin)"),
          value_max_chars: z.number().optional().default(HARD_VALUE_CAP_CHARS)
            .describe(`Max characters for returned value (default: ${HARD_VALUE_CAP_CHARS})`),
        },
        async ({ target_id, storage_type, item_id, origin, value_max_chars }) => {
          try {
            const parts = item_id.split(".");
            if (parts.length !== 4 || parts[0] !== "st") {
              return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: `Invalid item_id '${item_id}'` }) }] };
            }
            const itemType = parts[1];
            const itemOrigin = fromBase64UrlUtf8(parts[2]);
            const itemKey = fromBase64UrlUtf8(parts[3]);
            if (itemType !== storage_type) {
              return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: `item_id storage_type '${itemType}' does not match requested '${storage_type}'` }) }] };
            }
    
            const page = await getPageForTarget(target_id);
            const pageUrl = page.url();
            const currentOrigin = getOriginFromUrl(pageUrl);
            if (!currentOrigin) {
              return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: `No http(s) origin available for current page URL: '${pageUrl}'` }) }] };
            }
            if (origin && origin !== currentOrigin) {
              return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: `origin '${origin}' does not match current origin '${currentOrigin}'. Navigate first.` }) }] };
            }
            if (itemOrigin !== currentOrigin) {
              return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: `item_id origin '${itemOrigin}' does not match current origin '${currentOrigin}'. Navigate first.` }) }] };
            }
    
            const maxChars = Math.max(0, Math.min(HARD_VALUE_CAP_CHARS, Math.trunc(value_max_chars ?? HARD_VALUE_CAP_CHARS)));
            const result = await page.evaluate(
              ({ stType, key, maxChars: mc }) => {
                const storage = stType === "local" ? localStorage : sessionStorage;
                const raw = storage.getItem(key);
                const v = typeof raw === "string" ? raw : "";
                const valueLength = v.length;
                const truncated = mc > 0 && valueLength > mc;
                const value = mc > 0 ? (truncated ? v.slice(0, mc) : v) : v;
                return { key, value, valueLength, truncated };
              },
              { stType: storage_type, key: itemKey, maxChars },
            );
    
            return {
              content: [{
                type: "text",
                text: truncateResult({
                  status: "success",
                  target_id,
                  origin: currentOrigin,
                  storage_type,
                  item_id,
                  key: result.key,
                  value: result.value,
                  value_length: result.valueLength,
                  value_truncated: result.truncated,
                  value_max_chars: maxChars,
                }),
              }],
            };
          } catch (e) {
            return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: errorToString(e) }) }] };
          }
        },
      );
    
      // ── network fields (from MITM proxy capture) ───────────────────
    
      server.tool(
        "interceptor_browser_list_network_fields",
        "List request/response header fields from proxy-captured traffic since the browser was launched, with pagination and truncation.",
        {
          target_id: z.string().describe("Target ID from interceptor_browser_launch or interceptor_camoufox_launch"),
          direction: z.enum(["request", "response", "both"]).optional().default("both").describe("Header direction (default: both)"),
          header_name_filter: z.string().optional().describe("Filter by header name substring"),
          method_filter: z.string().optional().describe("Filter by HTTP method"),
          url_filter: z.string().optional().describe("Filter by URL substring"),
          status_filter: z.number().optional().describe("Filter by response status code"),
          hostname_filter: z.string().optional().describe("Filter by hostname substring"),
          offset: z.number().optional().default(0).describe("Offset into results (default: 0)"),
          limit: z.number().optional().default(DEFAULT_LIST_LIMIT).describe("Max fields to return (default: 50, max: 500)"),
          value_max_chars: z.number().optional().default(DEFAULT_VALUE_MAX_CHARS)
            .describe("Max characters for header value previews (default: 256)"),
        },
        async ({ target_id, direction, header_name_filter, method_filter, url_filter, status_filter, hostname_filter, offset, limit, value_max_chars }) => {
          try {
            const entry = getEntry(target_id);
            const since = entry.target.activatedAt;
    
            let traffic = proxyManager.getTraffic().filter((t) => t.timestamp >= since);
    
            if (method_filter) {
              const m = method_filter.toUpperCase();
              traffic = traffic.filter((t) => t.request.method === m);
            }
            if (url_filter) {
              const u = url_filter.toLowerCase();
              traffic = traffic.filter((t) => t.request.url.toLowerCase().includes(u));
            }
            if (status_filter !== undefined) {
              traffic = traffic.filter((t) => t.response?.statusCode === status_filter);
            }
            if (hostname_filter) {
              const h = hostname_filter.toLowerCase();
              traffic = traffic.filter((t) => t.request.hostname.toLowerCase().includes(h));
            }
    
            const nameNeedle = header_name_filter?.toLowerCase();
            const valueCap = Math.max(0, Math.min(HARD_VALUE_CAP_CHARS, Math.trunc(value_max_chars ?? DEFAULT_VALUE_MAX_CHARS)));
    
            const rows: Array<Record<string, unknown>> = [];
            const wantReq = direction === "request" || direction === "both";
            const wantRes = direction === "response" || direction === "both";
    
            for (const ex of traffic) {
              if (wantReq) {
                for (const [k, v] of Object.entries(ex.request.headers)) {
                  if (nameNeedle && !k.toLowerCase().includes(nameNeedle)) continue;
                  const capped = capValue(v, valueCap);
                  rows.push({
                    field_id: `nf.${ex.id}.request.${toBase64UrlUtf8(k.toLowerCase())}`,
                    exchange_id: ex.id,
                    direction: "request",
                    header_name: k,
                    value_preview: capped.value,
                    value_length: capped.valueLength,
                    value_truncated: capped.truncated,
                    method: ex.request.method,
                    url: ex.request.url,
                    hostname: ex.request.hostname,
                    status: ex.response?.statusCode ?? null,
                    timestamp: ex.timestamp,
                  });
                }
              }
              if (wantRes && ex.response) {
                for (const [k, v] of Object.entries(ex.response.headers)) {
                  if (nameNeedle && !k.toLowerCase().includes(nameNeedle)) continue;
                  const capped = capValue(v, valueCap);
                  rows.push({
                    field_id: `nf.${ex.id}.response.${toBase64UrlUtf8(k.toLowerCase())}`,
                    exchange_id: ex.id,
                    direction: "response",
                    header_name: k,
                    value_preview: capped.value,
                    value_length: capped.valueLength,
                    value_truncated: capped.truncated,
                    method: ex.request.method,
                    url: ex.request.url,
                    hostname: ex.request.hostname,
                    status: ex.response.statusCode,
                    timestamp: ex.timestamp,
                  });
                }
              }
            }
    
            const total = rows.length;
            const o = normalizeOffset(offset);
            const l = normalizeLimit(limit);
            const pageRows = rows.slice(o, o + l);
    
            return {
              content: [{
                type: "text",
                text: truncateResult({
                  status: "success",
                  target_id,
                  since_ts: since,
                  total,
                  offset: o,
                  limit: l,
                  showing: pageRows.length,
                  fields: pageRows,
                }),
              }],
            };
          } catch (e) {
            return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: errorToString(e) }) }] };
          }
        },
      );
    
      server.tool(
        "interceptor_browser_get_network_field",
        "Get one full header field value from proxy-captured traffic by field_id.",
        {
          target_id: z.string().describe("Target ID from interceptor_browser_launch or interceptor_camoufox_launch"),
          field_id: z.string().describe("field_id from interceptor_browser_list_network_fields"),
          value_max_chars: z.number().optional().default(HARD_VALUE_CAP_CHARS)
            .describe(`Max characters for returned value (default: ${HARD_VALUE_CAP_CHARS})`),
        },
        async ({ target_id, field_id, value_max_chars }) => {
          try {
            const entry = getEntry(target_id);
            const since = entry.target.activatedAt;
    
            const parts = field_id.split(".");
            if (parts.length !== 4 || parts[0] !== "nf") {
              return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: `Invalid field_id '${field_id}'` }) }] };
            }
            const exchangeId = parts[1];
            const dir = parts[2];
            const headerName = fromBase64UrlUtf8(parts[3]);
    
            const exchange = proxyManager.getExchange(exchangeId);
            if (!exchange) {
              return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: `Exchange '${exchangeId}' not found in capture buffer.` }) }] };
            }
            if (exchange.timestamp < since) {
              return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: "field_id refers to an exchange older than this browser session." }) }] };
            }
    
            let value: string | null = null;
            if (dir === "request") {
              value = exchange.request.headers[headerName.toLowerCase()] ?? null;
            } else if (dir === "response") {
              value = exchange.response?.headers?.[headerName.toLowerCase()] ?? null;
            } else {
              return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: `Invalid field direction '${dir}'` }) }] };
            }
            if (value === null) {
              return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: `Header '${headerName}' not found on ${dir}.` }) }] };
            }
    
            const capped = capValue(value, Math.max(0, Math.min(HARD_VALUE_CAP_CHARS, Math.trunc(value_max_chars ?? HARD_VALUE_CAP_CHARS))));
    
            return {
              content: [{
                type: "text",
                text: truncateResult({
                  status: "success",
                  target_id,
                  field_id,
                  exchange_id: exchangeId,
                  direction: dir,
                  header_name: headerName,
                  value: capped.value,
                  value_length: capped.valueLength,
                  value_truncated: capped.truncated,
                  value_max_chars: capped.maxChars,
                }),
              }],
            };
          } catch (e) {
            return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: errorToString(e) }) }] };
          }
        },
      );
    
      // ── JS execution / injection ───────────────────────────────────
    
      server.tool(
        "interceptor_browser_evaluate",
        "Execute a JS file in the page and return its result. " +
        "Source is loaded from `script_path` (absolute path). The file body is wrapped in an arrow " +
        "function receiving `__args` (so the file may `return value;` directly and access the optional " +
        "args object). " +
        "Worlds: `isolated` (default) or `main` (camoufox-only, requires `main_world_eval: true` at launch). " +
        "Cloakbrowser (Chromium): `isolated` runs in Playwright's utility world (different `window`, same DOM). " +
        "`main` is rejected — use `interceptor_browser_inject_init_script` for main-world patching there. " +
        "Camoufox (cloverlabs/FF150): there is no separate isolated world — both permitted modes run in the page's main world. " +
        "Reads are invisible to the page; mutations (`window.x = …`, `Object.defineProperty`, prototype patches) are observable by page scripts. " +
        "Earlier daijro/FF135 had a Juggler scope that made `isolated` invisible to the page; that scope was removed in cloverlabs. " +
        "Verify on your installed build with `scripts/camoufox-world-probe.ts`. " +
        "Rate-limit on cloakbrowser before reCAPTCHA: each call emits CDP traffic that behavioural scorers count.",
        {
          target_id: z.string().describe("Target ID from interceptor_browser_launch or interceptor_camoufox_launch"),
          script_path: z.string().describe("Absolute path to a .js file. File body is the function body; use `return` to send a value back."),
          args: z.record(z.unknown()).optional().describe("Optional JSON-serialisable args object, available inside the script as `__args`."),
          world: z.enum(["isolated", "main"]).optional().default("isolated")
            .describe("`isolated` (default) or `main`. On current camoufox build (cloverlabs/FF150) both run in the page's main world — arg is accepted but has no observable effect."),
          value_max_chars: z.number().optional().default(HARD_VALUE_CAP_CHARS)
            .describe(`Max characters of the JSON-stringified return value (default: ${HARD_VALUE_CAP_CHARS}).`),
        },
        async ({ target_id, script_path, args, world, value_max_chars }) => {
          try {
            if (!isAbsolute(script_path)) {
              return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: `script_path must be absolute: '${script_path}'` }) }] };
            }
            const source = await readFile(script_path, "utf-8");
            const page = await getPageForTarget(target_id);
            const isCamoufox = isCamoufoxTarget(target_id);
    
            if (world === "main") {
              if (!isCamoufox) {
                return { content: [{ type: "text", text: JSON.stringify({
                  status: "error",
                  error: "world: 'main' is only supported on camoufox targets. On cloakbrowser, use interceptor_browser_inject_init_script for main-world patching.",
                }) }] };
              }
              const entry = getEntry(target_id);
              const mwEnabled = Boolean((entry.target.details as { main_world_eval?: boolean } | undefined)?.main_world_eval);
              if (!mwEnabled) {
                return { content: [{ type: "text", text: JSON.stringify({
                  status: "error",
                  error: "Camoufox target launched without main_world_eval=true; main-world evaluate is disabled. Relaunch with `main_world_eval: true`.",
                }) }] };
              }
            }
    
            const argsLiteral = JSON.stringify(args ?? {});
            const fnExpr = `((__args) => { ${source}\n })(${argsLiteral})`;
            const pageFunction = world === "main" && isCamoufox ? `mw:${fnExpr}` : fnExpr;
    
            const result = await page.evaluate(pageFunction);
            const serialised = result === undefined ? "" : JSON.stringify(result);
            const capped = capValue(serialised, Math.max(0, Math.min(HARD_VALUE_CAP_CHARS, Math.trunc(value_max_chars ?? HARD_VALUE_CAP_CHARS))));
    
            return {
              content: [{
                type: "text",
                text: truncateResult({
                  status: "success",
                  target_id,
                  world,
                  backend: isCamoufox ? "camoufox" : "cloakbrowser",
                  value: capped.value,
                  value_length: capped.valueLength,
                  value_truncated: capped.truncated,
                  value_max_chars: capped.maxChars,
                }),
              }],
            };
          } catch (e) {
            return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: errorToString(e) }) }] };
          }
        },
      );
    
      server.tool(
        "interceptor_browser_inject_init_script",
        "Inject a JS file as an init script (Playwright `page.addInitScript`). " +
        "Runs before any page script on every subsequent navigation/frame. " +
        "Cloakbrowser (Chromium): runs in the isolated utility world — no DOM artifact; patches to shared prototypes/globals reach the page main world via utility-world sharing. " +
        "Camoufox (cloverlabs/FF150): runs directly in the page's main world. Patches (e.g. `Object.defineProperty(navigator, 'webdriver', ...)`) DO apply to the page, but are observable by anti-bot code on the page (`Function.prototype.toString` leak applies). " +
        "For Camoufox stealth, prefer source-level fingerprint config at launch (`os`, `webgl_config`, `fonts`, `humanize`, …) over JS injection. " +
        "Earlier daijro/FF135 ran init scripts in a Juggler scope that did NOT reach the page (camoufox#48); cloverlabs/FF150 removed that scope. " +
        "Does NOT affect the currently loaded document — navigate again to apply.",
        {
          target_id: z.string().describe("Target ID from interceptor_browser_launch or interceptor_camoufox_launch"),
          script_path: z.string().describe("Absolute path to a .js file to inject before page scripts on every load."),
        },
        async ({ target_id, script_path }) => {
          try {
            if (!isAbsolute(script_path)) {
              return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: `script_path must be absolute: '${script_path}'` }) }] };
            }
            const source = await readFile(script_path, "utf-8");
            const page = await getPageForTarget(target_id);
            await page.addInitScript({ content: source });
            const isCamoufox = isCamoufoxTarget(target_id);
            return {
              content: [{
                type: "text",
                text: JSON.stringify({
                  status: "success",
                  target_id,
                  backend: isCamoufox ? "camoufox" : "cloakbrowser",
                  bytes: source.length,
                  note: isCamoufox
                    ? "Camoufox (cloverlabs/FF150): init script runs in the page's main world — patches reach the page but are observable by page scripts. Apply on next navigation."
                    : "Applies on next navigation/frame, not the current document.",
                }),
              }],
            };
          } catch (e) {
            return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: errorToString(e) }) }] };
          }
        },
      );
    
      server.tool(
        "interceptor_browser_add_script_tag",
        "Append a <script> element to the current page (Playwright `page.addScriptTag`). " +
        "WARNING: injects a real DOM node visible to MutationObserver, document.scripts, and CSP. " +
        "Avoid for anti-bot stealth — prefer interceptor_browser_inject_init_script (no DOM node) when you need page-scope execution.",
        {
          target_id: z.string().describe("Target ID from interceptor_browser_launch or interceptor_camoufox_launch"),
          script_path: z.string().describe("Absolute path to a .js file to inject as <script>."),
          script_type: z.enum(["classic", "module"]).optional().default("classic")
            .describe("`classic` (default) or `module`."),
        },
        async ({ target_id, script_path, script_type }) => {
          try {
            if (!isAbsolute(script_path)) {
              return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: `script_path must be absolute: '${script_path}'` }) }] };
            }
            const source = await readFile(script_path, "utf-8");
            const page = await getPageForTarget(target_id);
            await page.addScriptTag({ content: source, type: script_type === "module" ? "module" : undefined });
            return {
              content: [{
                type: "text",
                text: JSON.stringify({
                  status: "success",
                  target_id,
                  backend: isCamoufoxTarget(target_id) ? "camoufox" : "cloakbrowser",
                  bytes: source.length,
                  script_type,
                  warning: "DOM-visible injection. Detectable by MutationObserver/document.scripts/CSP.",
                }),
              }],
            };
          } catch (e) {
            return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: errorToString(e) }) }] };
          }
        },
      );
    }
  • Helper function cookieStableId used by the handler to generate stable unique IDs for cookies based on name, domain, path, secure, httpOnly, sameSite, and partitionKey.
    function cookieStableId(cookie: { name?: string; domain?: string; path?: string; secure?: boolean; httpOnly?: boolean; sameSite?: string; partitionKey?: string }): string {
      const parts = [
        cookie.name ?? "",
        cookie.domain ?? "",
        cookie.path ?? "",
        String(!!cookie.secure),
        String(!!cookie.httpOnly),
        cookie.sameSite ?? "",
        cookie.partitionKey ?? "",
      ];
      return `ck_${createHash("sha1").update(parts.join("|"), "utf8").digest("hex")}`;
    }
Behavior2/5

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

No annotations are provided, and the description lacks disclosure of behavioral traits such as read-only nature, side effects, or lifecycle requirements. It mentions pagination and previews but does not fully compensate for missing annotations.

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

Conciseness5/5

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

The description is a single sentence, front-loaded with purpose, and contains no unnecessary words.

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

Completeness4/5

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

Given the number of parameters and absence of output schema, the description covers the essential aspects (pagination, previews) and is comprehensive enough for the tool's purpose. It could mention return format, but not strictly required.

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

Parameters3/5

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

Schema coverage is 100%, and the schema descriptions are detailed. The description adds the concept of 'truncated value previews' but does not provide additional meaning beyond what the schema already conveys.

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 verb (list), resource (cookies), and key features (pagination and truncated value previews). It distinguishes from sibling tool 'interceptor_browser_get_cookie' which is for a single cookie.

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

Usage Guidelines3/5

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

The description does not explicitly state when to use this tool vs. alternatives, nor does it provide exclusions. The purpose is clear, but guidance on usage context is missing.

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/yfe404/proxy-mcp'

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