Skip to main content
Glama

find_by_text

Read-only

Locate DOM elements by their text content. Supports exact match, case-sensitive search, and regex patterns for finding elements with poor selectors.

Instructions

Find elements by their text content. Essential for finding elements without good selectors, especially in poorly structured DOM. Returns elements with position, visibility, and interaction state. Supports exact match, case-sensitive search, and NEW: regex pattern matching for advanced text searching (e.g., '/\d+ items?/' to find elements with numbers).

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
textYesText to search for in elements. If regex=true, this can be a regex pattern in /pattern/flags format (e.g., '/\d+/i' for case-insensitive numbers) or a raw pattern string.
exactNoWhether to match text exactly (default: false, allows partial matches). Ignored if regex=true.
caseSensitiveNoWhether search should be case-sensitive (default: false). Ignored if regex=true (use regex flags instead).
regexNoWhether to treat 'text' as a regex pattern (default: false). If true, supports /pattern/flags format or raw pattern. Examples: '/sign.*/i' (case-insensitive), '/\d+ items?/' (numbers + optional 's').
limitNoMaximum number of elements to return (default: 10)

Implementation Reference

  • The execute method that runs the tool logic: builds a Playwright text selector (regex/exact/partial), finds matching elements, retrieves bounding box, visibility, interactivity, and formats the output.
      async execute(args: FindByTextArgs, context: ToolContext): Promise<ToolResponse> {
        return this.safeExecute(context, async (page) => {
          const { text, exact = false, caseSensitive = false, regex = false, limit = 10 } = args;
    
          // Build the text selector based on exact, caseSensitive, and regex options
          let selector: string;
    
          if (regex) {
            // Validate and use user-provided regex pattern
            try {
              // Extract pattern and flags from /pattern/flags format
              const regexMatch = text.match(/^\/(.+?)\/([gimuy]*)$/);
              if (regexMatch) {
                const [, pattern, flags] = regexMatch;
                // Validate the regex pattern
                new RegExp(pattern, flags);
                selector = `text=/${pattern}/${flags}`;
              } else {
                // If not in /pattern/flags format, treat as raw pattern
                new RegExp(text);
                selector = `text=/${text}/`;
              }
            } catch (error) {
              return {
                content: [
                  {
                    type: 'text',
                    text: `✗ Invalid regex pattern: ${(error as Error).message}`
                  }
                ],
                isError: true
              };
            }
          } else if (exact) {
            selector = `text="${text}"`;
          } else {
            // Use regex for partial match with case sensitivity
            const flags = caseSensitive ? '' : 'i';
            const escapedText = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            selector = `text=/${escapedText}/${flags}`;
          }
    
          // Find all matching elements
          const locator = page.locator(selector);
          const count = await locator.count();
    
          if (count === 0) {
            // Build "not found" message based on search type
            let notFoundMsg: string;
            if (regex) {
              notFoundMsg = `✗ No elements found matching regex ${text}`;
            } else if (exact) {
              notFoundMsg = `✗ No elements found with exact text "${text}"`;
            } else {
              notFoundMsg = `✗ No elements found containing "${text}"`;
            }
    
            return {
              content: [
                {
                  type: 'text',
                  text: notFoundMsg
                }
              ],
              isError: false
            };
          }
    
          // Limit results
          const elementsToShow = Math.min(count, limit);
          const elements: string[] = [];
    
          for (let i = 0; i < elementsToShow; i++) {
            const element = locator.nth(i);
    
            // Get element info
            const [tagName, boundingBox, textContent, isVisible, isEnabled] = await Promise.all([
              element.evaluate((el) => el.tagName.toLowerCase()),
              element.boundingBox().catch(() => null),
              element.textContent().catch(() => ''),
              element.isVisible().catch(() => false),
              element.isEnabled().catch(() => true)
            ]);
    
            // Get selector attributes for better identification
            const selectorInfo = await element.evaluate((el) => {
              const attrs: Record<string, string> = {};
              if (el.id) attrs.id = el.id;
              if (el.className && typeof el.className === 'string') {
                attrs.class = el.className.split(' ').slice(0, 2).join(' ');
              }
              const testId = el.getAttribute('data-testid') || el.getAttribute('data-test') || el.getAttribute('data-cy');
              if (testId) attrs.testid = testId;
              if (el.getAttribute('href')) attrs.href = el.getAttribute('href') || '';
              if (el.getAttribute('name')) attrs.name = el.getAttribute('name') || '';
              if (el.getAttribute('type')) attrs.type = el.getAttribute('type') || '';
              if (el.getAttribute('aria-label')) attrs['aria-label'] = el.getAttribute('aria-label') || '';
              return attrs;
            });
    
            // Build selector string
            let selectorStr = `<${tagName}`;
            if (selectorInfo.id) selectorStr += `#${selectorInfo.id}`;
            if (selectorInfo.class) selectorStr += ` class="${selectorInfo.class}"`;
            if (selectorInfo.testid) selectorStr += ` data-testid="${selectorInfo.testid}"`;
            if (selectorInfo.name) selectorStr += ` name="${selectorInfo.name}"`;
            if (selectorInfo.type) selectorStr += ` type="${selectorInfo.type}"`;
            if (selectorInfo.href) selectorStr += ` href="${selectorInfo.href.slice(0, 30)}${selectorInfo.href.length > 30 ? '...' : ''}"`;
            if (selectorInfo['aria-label']) selectorStr += ` aria-label="${selectorInfo['aria-label']}"`;
            selectorStr += '>';
    
            // Format position
            let positionStr = '';
            if (boundingBox) {
              const x = Math.round(boundingBox.x);
              const y = Math.round(boundingBox.y);
              const w = Math.round(boundingBox.width);
              const h = Math.round(boundingBox.height);
              positionStr = `    @ (${x},${y}) ${w}x${h}px`;
            } else {
              positionStr = `    @ (no bounding box)`;
            }
    
            // Format text content (truncate if too long)
            const truncatedText = textContent && textContent.length > 100
              ? textContent.slice(0, 100) + '...'
              : textContent;
            const textStr = `    "${truncatedText}"`;
    
            // Format state
            let stateStr = '    ';
            if (isVisible) {
              stateStr += '✓ visible';
            } else {
              stateStr += '✗ hidden';
              // Try to detect why it's hidden
              const hiddenReason = await element.evaluate((el) => {
                const style = window.getComputedStyle(el);
                if (style.display === 'none') return '(display: none)';
                if (style.visibility === 'hidden') return '(visibility: hidden)';
                if (style.opacity === '0') return '(opacity: 0)';
                const rect = el.getBoundingClientRect();
                if (rect.width === 0 || rect.height === 0) return '(zero size)';
                return '';
              }).catch(() => '');
              if (hiddenReason) stateStr += ` ${hiddenReason}`;
            }
    
            // Check if interactive
            const isInteractive = await element.evaluate((el) => {
              const tag = el.tagName.toLowerCase();
              if (['a', 'button', 'input', 'select', 'textarea'].includes(tag)) return true;
              if (el.getAttribute('onclick')) return true;
              if (el.getAttribute('role') === 'button') return true;
              return false;
            }).catch(() => false);
    
            if (isVisible && isInteractive && isEnabled) {
              stateStr += ', ⚡ interactive';
            } else if (isVisible && isInteractive && !isEnabled) {
              stateStr += ', ✗ disabled';
            }
    
            const nthSelector = `${selector} >> nth=${i}`;
            elements.push(
              `[${i}] ${selectorStr}\n${positionStr}\n${textStr}\n${stateStr}\n    selector: ${nthSelector}`
            );
          }
    
          // Build header message based on search type
          let searchDesc: string;
          if (regex) {
            searchDesc = `matching regex ${text}`;
          } else if (exact) {
            searchDesc = `with exact text "${text}"`;
          } else {
            searchDesc = `containing "${text}"`;
          }
    
          const header = count > limit
            ? `Found ${count} elements ${searchDesc} (showing first ${limit}):\n`
            : `Found ${count} element${count > 1 ? 's' : ''} ${searchDesc}:\n`;
    
          return {
            content: [
              {
                type: 'text',
                text: header + '\n' + elements.join('\n\n')
              }
            ],
            isError: false
          };
        });
      }
    }
  • Input argument interface for the find_by_text tool with text (required), exact, caseSensitive, regex, and limit (optional) fields.
    export interface FindByTextArgs {
      text: string;
      exact?: boolean;
      caseSensitive?: boolean;
      regex?: boolean;
      limit?: number;
    }
  • Static getMetadata defines name, description, input/output schemas, priority, examples, and example outputs for the find_by_text tool.
    static getMetadata(sessionConfig?: SessionConfig): ToolMetadata {
      return {
        name: "find_by_text",
        description: "Find elements by their text content. Essential for finding elements without good selectors, especially in poorly structured DOM. Returns elements with position, visibility, and interaction state. Supports exact match, case-sensitive search, and NEW: regex pattern matching for advanced text searching (e.g., '/\\d+ items?/' to find elements with numbers).",
        annotations: ANNOTATIONS.readOnly,
        priority: 8,
        outputs: [
          "Header showing 'No elements found ...' or 'Found N elements ...'",
          "Up to limit results, each with:",
          "- <tag id/class/testid ...> line with key attributes",
          "- Position line: @ (x,y) widthxheight px",
          "- Trimmed text content (if any)",
          "- Visibility and interactability status",
          "Footer shows how many are displayed vs omitted and how to increase limit",
        ],
        examples: [
          "find_by_text({ text: 'Sign in' })",
          "find_by_text({ text: '/^Next \\d+$/', regex: true })",
          "find_by_text({ text: 'Delete', exact: true, caseSensitive: true })",
        ],
        exampleOutputs: [
          {
            call: "find_by_text({ text: 'Sign in' })",
            output: `Found 3 elements containing "Sign in":\n\n[0] <button data-testid=\"primary-cta\">\n    @ (640,420) 120x40px\n    "Sign in"\n    ✓ visible\n\n[1] <a class=\"link\" href=\"/signin\">\n    @ (600,480) 68x20px\n    "Sign in"\n    ✓ visible, ⚡ interactive\n\n[2] <div class=\"menu-item\">\n    @ (40,360) 200x24px\n    "Sign in"\n    ✗ hidden\n\nShowing all 3 matches`
          }
        ],
        inputSchema: {
          type: "object",
          properties: {
            text: {
              type: "string",
              description: "Text to search for in elements. If regex=true, this can be a regex pattern in /pattern/flags format (e.g., '/\\d+/i' for case-insensitive numbers) or a raw pattern string."
            },
            exact: {
              type: "boolean",
              description: "Whether to match text exactly (default: false, allows partial matches). Ignored if regex=true."
            },
            caseSensitive: {
              type: "boolean",
              description: "Whether search should be case-sensitive (default: false). Ignored if regex=true (use regex flags instead)."
            },
            regex: {
              type: "boolean",
              description: "Whether to treat 'text' as a regex pattern (default: false). If true, supports /pattern/flags format or raw pattern. Examples: '/sign.*/i' (case-insensitive), '/\\d+ items?/' (numbers + optional 's')."
            },
            limit: {
              type: "number",
              description: "Maximum number of elements to return (default: 10)"
            }
          },
          required: ["text"],
        },
      };
  • FindByTextTool is registered in the BROWSER_TOOL_CLASSES array, making it an available tool.
    FindByTextTool,
  • Re-export of FindByTextTool from the inspection index barrel file.
    export { FindByTextTool } from './find_by_text.js';
Behavior4/5

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

The description discloses the tool's return information (position, visibility, interaction state) and mentions support for exact match, case-sensitive search, and regex. This adds behavioral context beyond the readOnlyHint annotation, which already indicates safety. No contradictions. Slight gap: no mention of error handling or behavior with no matches.

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 concise (4 sentences), each sentence adds value. It front-loads the core purpose, then provides usage guidance, return behavior, and supported features. No unnecessary words. Excellent structure.

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?

The description adequately covers the tool's purpose, return data, and matching options. Given the complexity of 5 parameters and no output schema, it is reasonably complete. However, it could mention edge cases like no matches or performance implications, which would make it more robust.

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

Parameters4/5

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

With 100% schema description coverage, the bar for parameter value is higher. The description adds extra context with examples and the 'NEW' regex capability, which enriches the schema's descriptions. It explains the interplay between exact, caseSensitive, and regex, going beyond what the schema states.

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 finds elements by text content, which is a specific verb and resource. It distinguishes itself from selector-based tools by noting its utility for poorly structured DOM, and mentions returns of position, visibility, and interaction state. This differentiates it from siblings like get_text or element_exists.

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?

The description explicitly advises using this tool when 'without good selectors' and in 'poorly structured DOM,' which provides clear usage context. However, it does not explicitly list when not to use it or name alternative tools, so it falls short of a perfect score.

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/antonzherdev/mcp-web-inspector'

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