Skip to main content
Glama

validate_url

Validates Tactual's predicted navigation paths by simulating a screen reader on the page. Returns an accuracy ratio comparing predicted vs actual step counts for each target.

Instructions

Validate Tactual's predicted navigation paths against a virtual screen reader. Runs analyze_url internally, then for each worst finding drives @guidepup/virtual-screen-reader over the captured DOM (via jsdom) to check: (a) is the target reachable at all, and (b) how many virtual SR announcements does it take to reach it? Compares to Tactual's predicted step count. Returns an accuracy ratio per target and a mean across all validated targets — closer to 1.0 means Tactual's predictions match this virtual-screen-reader run, not a guarantee of full real-AT fidelity.

Requires (optional deps): jsdom + @guidepup/virtual-screen-reader. Installed with tactual if optionalDependencies were honored; otherwise run npm install jsdom @guidepup/virtual-screen-reader in your project.

When to use: closing the predicted-vs-actual loop. If Tactual's predictions diverge a lot from the virtual SR, either the profile weights need calibration or the page has structural patterns the analyzer doesn't model. Use sparingly — this adds the analyze_url cost plus jsdom parsing + virtual SR navigation time.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
urlYesURL to analyze and validate
profileNoAT profile ID (default: nvda-desktop-v0). Use list_profiles to see options.
maxTargetsNoMaximum findings to validate (worst-first). Higher = slower but more signal.
strategyNoNavigation strategy for the virtual SR. 'linear' uses Tab/Shift-Tab (keyboard flow); 'semantic' uses heading/landmark skip commands (screen-reader flow). Semantic is more representative for NVDA/JAWS/VoiceOver users.semantic
timeoutNoPage load timeout in ms
waitTimeNoAdditional wait after load (ms)
channelNoBrowser channel: chrome, chrome-beta, msedge
stealthNoApply anti-bot-detection defaults
storageStateNoPath to a Playwright storageState JSON (for authenticated pages). Must be within the current working directory.

Implementation Reference

  • MCP tool handler function that destructures the Zod-validated input, calls runValidateUrl(), and formats the result as MCP content. Catches ValidateUrlError and returns isError: true on failure.
      async ({ url, profile, maxTargets, strategy, timeout, waitTime, channel, stealth, storageState }) => {
        try {
          const result = await runValidateUrl({
            url,
            profileId: profile,
            maxTargets,
            strategy,
            timeout,
            waitTime,
            channel,
            stealth,
            storageState,
            restrictStorageStateToCwd: true,
            useSharedBrowserPool: true,
          });
          return {
            content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
          };
        } catch (err) {
          const text =
            err instanceof ValidateUrlError
              ? err.message
              : `validate_url failed: ${err instanceof Error ? err.message : String(err)}`;
          return {
            content: [{ type: "text" as const, text }],
            isError: true,
          };
        }
      },
    );
  • Zod input schema for validate_url tool, defining parameters: url, profile, maxTargets, strategy, timeout, waitTime, channel, stealth, storageState.
    inputSchema: {
      url: z.string().describe("URL to analyze and validate"),
      profile: z
        .string()
        .optional()
        .describe("AT profile ID (default: nvda-desktop-v0). Use list_profiles to see options."),
      maxTargets: z
        .number()
        .int()
        .min(1)
        .max(50)
        .default(10)
        .describe("Maximum findings to validate (worst-first). Higher = slower but more signal."),
      strategy: z
        .enum(["linear", "semantic"])
        .default("semantic")
        .describe(
          "Navigation strategy for the virtual SR. 'linear' uses Tab/Shift-Tab (keyboard flow); " +
          "'semantic' uses heading/landmark skip commands (screen-reader flow). Semantic is more " +
          "representative for NVDA/JAWS/VoiceOver users.",
        ),
      timeout: z.number().default(30000).describe("Page load timeout in ms"),
      waitTime: z.number().optional().describe("Additional wait after load (ms)"),
      channel: z.string().optional().describe("Browser channel: chrome, chrome-beta, msedge"),
      stealth: z.boolean().optional().describe("Apply anti-bot-detection defaults"),
      storageState: z
        .string()
        .optional()
        .describe(
          "Path to a Playwright storageState JSON (for authenticated pages). " +
          "Must be within the current working directory.",
        ),
    },
  • Registration function that calls server.registerTool('validate_url', ...) with schema and handler. Exported as registerValidateUrl.
    export function registerValidateUrl(server: McpServer): void {
      server.registerTool(
        "validate_url",
        {
          description:
            "Validate Tactual's predicted navigation paths against a virtual screen reader. " +
            "Runs analyze_url internally, then for each worst finding drives " +
            "@guidepup/virtual-screen-reader over the captured DOM (via jsdom) to check: " +
            "(a) is the target reachable at all, and (b) how many virtual SR announcements " +
            "does it take to reach it? Compares to Tactual's predicted step count. " +
            "Returns an accuracy ratio per target and a mean across all validated targets — " +
            "closer to 1.0 means Tactual's predictions match this virtual-screen-reader run, " +
            "not a guarantee of full real-AT fidelity.\n\n" +
            "**Requires** (optional deps): jsdom + @guidepup/virtual-screen-reader. " +
            "Installed with tactual if optionalDependencies were honored; otherwise run " +
            "`npm install jsdom @guidepup/virtual-screen-reader` in your project.\n\n" +
            "**When to use**: closing the predicted-vs-actual loop. If Tactual's predictions " +
            "diverge a lot from the virtual SR, either the profile weights need calibration " +
            "or the page has structural patterns the analyzer doesn't model. Use sparingly — " +
            "this adds the analyze_url cost plus jsdom parsing + virtual SR navigation time.",
          inputSchema: {
            url: z.string().describe("URL to analyze and validate"),
            profile: z
              .string()
              .optional()
              .describe("AT profile ID (default: nvda-desktop-v0). Use list_profiles to see options."),
            maxTargets: z
              .number()
              .int()
              .min(1)
              .max(50)
              .default(10)
              .describe("Maximum findings to validate (worst-first). Higher = slower but more signal."),
            strategy: z
              .enum(["linear", "semantic"])
              .default("semantic")
              .describe(
                "Navigation strategy for the virtual SR. 'linear' uses Tab/Shift-Tab (keyboard flow); " +
                "'semantic' uses heading/landmark skip commands (screen-reader flow). Semantic is more " +
                "representative for NVDA/JAWS/VoiceOver users.",
              ),
            timeout: z.number().default(30000).describe("Page load timeout in ms"),
            waitTime: z.number().optional().describe("Additional wait after load (ms)"),
            channel: z.string().optional().describe("Browser channel: chrome, chrome-beta, msedge"),
            stealth: z.boolean().optional().describe("Apply anti-bot-detection defaults"),
            storageState: z
              .string()
              .optional()
              .describe(
                "Path to a Playwright storageState JSON (for authenticated pages). " +
                "Must be within the current working directory.",
              ),
          },
        },
        async ({ url, profile, maxTargets, strategy, timeout, waitTime, channel, stealth, storageState }) => {
          try {
            const result = await runValidateUrl({
              url,
              profileId: profile,
              maxTargets,
              strategy,
              timeout,
              waitTime,
              channel,
              stealth,
              storageState,
              restrictStorageStateToCwd: true,
              useSharedBrowserPool: true,
            });
            return {
              content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
            };
          } catch (err) {
            const text =
              err instanceof ValidateUrlError
                ? err.message
                : `validate_url failed: ${err instanceof Error ? err.message : String(err)}`;
            return {
              content: [{ type: "text" as const, text }],
              isError: true,
            };
          }
        },
      );
    }
  • src/mcp/index.ts:34-35 (registration)
    Top-level registration call: registerValidateUrl(server) invoked in createMcpServer().
    registerAnalyzeUrl(server);
    registerValidateUrl(server);
  • Core URL validation logic used internally by the pipeline. Validates allowed protocols (http, https, file), blocks dangerous protocols (javascript, data, vbscript, blob), checks hostname, and prevents embedded credentials.
    export function validateUrl(input: string): ValidationResult {
      const trimmed = input.trim();
    
      if (!trimmed) {
        return { valid: false, error: "URL is empty" };
      }
    
      // Block obviously dangerous protocols before parsing
      const lower = trimmed.toLowerCase();
      for (const proto of BLOCKED_PROTOCOLS) {
        if (lower.startsWith(proto)) {
          return { valid: false, error: `Blocked protocol: ${proto}` };
        }
      }
    
      let parsed: URL;
      try {
        parsed = new URL(trimmed);
      } catch {
        return { valid: false, error: `Invalid URL: "${trimmed}"` };
      }
    
      if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) {
        return {
          valid: false,
          error: `Unsupported protocol "${parsed.protocol}" — only http:, https:, and file: are allowed`,
        };
      }
    
      // For http/https, require a hostname
      if ((parsed.protocol === "http:" || parsed.protocol === "https:") && !parsed.hostname) {
        return { valid: false, error: "URL is missing a hostname" };
      }
    
      // Block credentials in URLs (potential phishing)
      if (parsed.username || parsed.password) {
        return { valid: false, error: "URLs with embedded credentials are not allowed" };
      }
    
      return { valid: true, url: parsed.href };
    }
Behavior5/5

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

With no annotations, the description fully discloses behavior: it runs analyze_url internally, uses jsdom and a virtual screen reader, compares steps, and returns an accuracy ratio. It also notes limitations ('not a guarantee of full real-AT fidelity') and optional dependencies. Destructive behavior is not relevant; this is a read-intensive operation.

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?

The description is well-structured with separate paragraphs and a 'When to use' section. It front-loads the core purpose. While somewhat long, every part adds value. Could be slightly shorter, but efficiency is good.

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 9 parameters and no output schema, the description adequately explains what the tool does and what it returns (accuracy ratio per target and mean). It also covers prerequisites and cost. Missing perhaps a note on the return format structure, but overall sufficient for an agent.

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?

Schema coverage is 100%, so baseline is 3. The description adds extra context: it explains the 'strategy' parameter (linear vs semantic) and notes that 'maxTargets' higher is slower but more signal. This goes beyond the schema descriptions.

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?

Clearly states the tool validates Tactual's predicted navigation paths against a virtual screen reader. It describes the internal process (runs analyze_url, uses @guidepup/virtual-screen-reader) and the output (accuracy ratio). Distinguishes itself from sibling tools like analyze_url and trace_path by its specific validation role.

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?

Explicitly states 'When to use: closing the predicted-vs-actual loop' and advises to use sparingly due to cost. Provides context for when it is appropriate but does not explicitly list alternatives or conditions when not to use.

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