AutoWebMCP: Automatic MCP Tool Schema Generation from Arbitrary Web Pages

Forms, links, OpenAPI/Swagger sniffs, and SSRF-protected fetches — turning any URL into a runnable agent tool

Tab Levitas

Memagen™

2026-05-08 · Version v1.1 · ~7 pages ·Subsystem: AutoWebMCP
↓ Download PDF

Abstract

Every web application is, in principle, an agent tool: it accepts inputs, performs an action, and returns a response. In practice agents cannot use arbitrary websites because they lack a schema describing what to send and what comes back. The conventional answer — handwritten Model Context Protocol (MCP) servers per site — leaves the long tail of the web inaccessible. AutoWebMCP, the open-core (V1-lite, MIT) Memagen™ subsystem described in this paper, infers a runnable MCP tool surface from a URL alone. The pipeline has four stages: an SSRF-safe fetch through a single-source-of-truth URL validator (`core.security.ssrf.assert_public_url`); an HTML structural parse over forms, links, meta tags, and standalone inputs (`core.mcp.webmcp_generator._SiteParser`); a spec-sniff that probes well-known paths and link relations for OpenAPI or Swagger documents and prefers the spec when found; and a schema-synthesis step that maps form input names to argument names, input types to JSON-Schema types, and HTML5 validation attributes to schema constraints. Generation runs at first-visit time and the resulting tool definitions are cached: registered in an in-memory registry and stamped into the Pro-tier graph substrate so they survive process restarts. On the V1-lite test corpus (Section 7), DOM analysis completes in 18–96 ms per page (median 34 ms) on a 4 vCPU build host, and 41 of 43 generated argument names round-tripped to the target server on first invocation (95.3%). We describe the implementation, the SSRF defense-in-depth, the runtime executor that translates an agent's tool call into a real HTTP request with redirect re-validation, the limitations on single-page-app sites and multi-step workflows, and the comparison to vision-based browser agents. AutoWebMCP is fastest and cheapest on sites whose structural surface is meaningful; vision-based agents remain the right tool for the rest.

Keywords — webmcp, tool synthesis, openapi sniffing, browser automation, ssrf protection

1. Introduction

The Model Context Protocol gave agent platforms a common shape for tool surfaces: a JSON object with a name, a description, and an input schema. A hand-authored MCP server is the wrong unit for the web. There are a few hundred million reachable web applications and somewhere between two and three thousand published MCP servers at the time of writing. The long tail is unreachable not because it lacks an interface, but because nobody has translated each interface into the agent’s idiom.

The translation is mechanical for a large class of sites. A contact form has, in HTML, the names and types of every field, often with required and pattern attributes that constrain valid input. A page that publishes an OpenAPI document at /openapi.json declares every endpoint, parameter, and response shape in a format that already maps cleanly to JSON Schema. The site is already self-describing; it simply isn’t using the protocol the agent expects.

AutoWebMCP closes the gap. Given a URL, the generator fetches the page through an SSRF-safe path, parses the HTML structural surface, sniffs for OpenAPI or Swagger specs at conventional locations, and emits one or more MCP tool definitions. A user-approved tool becomes a first-class catalog entry: the agent calls webmcp_invoke(tool_name="example_com_contact", params={...}), the executor in core/tools/webmcp_executor.py revalidates the destination against the SSRF guard, issues the HTTP request with manual redirect handling, and returns the response. The pipeline is open-source under MIT in the Memagen™ Lite distribution and is described in the master paper at section 9.2.

The contributions of this paper are:

  1. A four-stage pipeline (SSRF-safe fetch → structural parse → spec sniff → schema synthesis) for translating arbitrary web pages into MCP tool definitions, executed at first-visit time and cached afterward.
  2. An argument-inference table that maps HTML form input types and validation attributes to JSON-Schema fragments, with documented collision and anonymous-field behavior.
  3. A runtime executor with redirect re-validation, manual redirect handling, and a pentest-mode escape hatch that still refuses cloud-metadata exfil targets.
  4. An evaluation describing what works (well-structured forms, OpenAPI-publishing sites), what partially works (mixed server-rendered/client-rendered surfaces), and what does not (SPAs with no server-emitted form HTML, multi-step wizards, CAPTCHA-protected forms).

AutoWebMCP is not a vision-based browser agent and does not try to be. It is the cheap, fast, deterministic path for sites that publish meaningful structure. Section 9 compares it to vision-based agents and to PCMCP, the desktop-app analog covered in a separate paper.

2.1 OpenAPI, Swagger, and the spec-publication gap

The OpenAPI Specification (OAI, 2017) — formerly Swagger — is the dominant machine-readable format for HTTP APIs. Adoption is partial: surveys of the public-API landscape (APIs.guru, 2023; Postman State of the API, 2024) suggest 30–40% of enterprise REST APIs ship some form of OpenAPI document, dropping sharply for consumer product surfaces. AutoWebMCP probes well-known paths and prefers the spec when one is found because it is denser and more reliable than HTML inference; the structural-parse stage is the fallback.

2.2 Headless browser frameworks

Headless browser engines (Playwright, Puppeteer, Selenium) provide programmatic control of full browser engines. They are the right substrate for sites that depend on JavaScript to produce their interactive surface, but the interface they expose is imperative: click this, type that. They do not produce a schema of what the site can do. Memagen™‘s browser.headless action tool wraps a headless engine for cases AutoWebMCP’s structural parse cannot handle; the two are complementary.

2.3 HTML parsing libraries

The Python standard library html.parser.HTMLParser is a SAX-style streaming parser. BeautifulSoup and lxml provide tree APIs. AutoWebMCP uses the standard library parser because the generator runs in the agent runtime and must function without optional dependencies. The custom subclass _SiteParser in core/mcp/webmcp_generator.py extracts only the elements the synthesis stage needs.

2.4 Schema inference

Schema inference for arbitrary data has a long literature (hidden Markov and learning-based methods over sample data). AutoWebMCP’s setting is gentler: HTML form authors have already declared types in the markup. The work is not inferring what type a field is from observations; it is translating an existing declaration. The hard cases are not statistical — they are malformed or non-standard markup, handled as documented in Section 4.

2.5 The “every web app is an API” gap

The observation that every web application is potentially an API is older than the web (Berners-Lee, 1989). Recent browser-driven agent work (Nakano et al., 2021; Yao et al., 2022) and commercial vision-based browser agents close the gap by treating the page as pixels. AutoWebMCP closes it from the other direction: treat the page as already-self-describing and translate the description.

3. The four-stage pipeline

The pipeline is implemented in core/mcp/webmcp_generator.py as WebMCPGenerator.generate_from_url. Stages execute in order; later stages depend on artifacts from earlier ones. Figure 1 summarises the flow, including where the result is cached.

First visit Web page DOM URL · HTML body Semantic analyzer _SiteParser · OpenAPI spec sniff Tool spec emitter JSON-Schema in/out name · required · types WebMCP server register_webmcp_tools() _WEBMCP_REGISTRY Agent catalog auto-update no restart Cache and restore In-memory registry _WEBMCP_REGISTRY (dict) session lifetime L4 graph node kind=webmcp_tool · permanent=True stamp_new_node() Restore on startup restore_webmcp_from_graph() SQLite · $CRACKEDCLAW_DB Generation runs once at first-visit time. On subsequent runs the agent loads the cached tool surface in milliseconds.
Figure 1. AutoWebMCP pipeline. Top row: first-visit generation (web page DOM → semantic analyzer → tool spec emitter → WebMCP server registration → agent catalog auto-update). Bottom row: cache. Tools are stamped into the L4 graph substrate and restored on every subsequent process start.

The same pipeline as a Mermaid source, for environments that render it natively:

flowchart LR
  A["Web page DOM<br/>URL + HTML"] --> B["Semantic analyzer<br/>_SiteParser + OpenAPI sniff"]
  B --> C["Tool spec emitter<br/>JSON-Schema in/out"]
  C --> D["WebMCP server<br/>register_webmcp_tools()"]
  D --> E["Agent tool catalog<br/>auto-update, no restart"]
  D -. cache .-> F["In-memory registry<br/>_WEBMCP_REGISTRY"]
  D -. persist .-> G["L4 graph node<br/>kind=webmcp_tool"]
  G -. on startup .-> H["restore_webmcp_from_graph()"]
  H --> D
  classDef phase fill:#eef2ff,stroke:#3730a3
  classDef cache fill:#f5f3ff,stroke:#6d28d9,stroke-dasharray: 4 3
  class D,E phase
  class F,G,H cache

3.1 SSRF-safe fetch

Every outbound HTTP request that AutoWebMCP issues passes through core.security.ssrf.assert_public_url. The validator is the single source of truth for “is this URL safe to fetch” across the entire Memagen™ runtime; the WebMCP generator imports it directly, and the executor in core/tools/webmcp_executor.py revalidates at invocation time as defense in depth. The SSRF guard rejects, in order:

  • Empty or non-string URLs.
  • Schemes other than http or httpsfile://, gopher://, ftp://, data://, and anything else the parser produces.
  • Unicode-confusable hostnames after NFKC normalization. The string localhost (with a full-width ) normalizes to localhost and is matched against an explicit blocklist.
  • Hostnames in _BLOCKED_HOSTS: localhost, ip6-localhost, ip6-loopback, metadata.google.internal, metadata. These are symbolic names an attacker might place into DNS pointing at public IPs to bypass IP-range checks; the symbolic name itself is refused.
  • Literal IP addresses (v4 or v6) that fail is_public(). The helper rejects is_private, is_loopback, is_link_local, is_multicast, is_reserved, and is_unspecified — covering 10/8, 172.16/12, 192.168/16, 169.254/16 (link-local, including the standard cloud-metadata IP at 169.254.169.254), 127/8, 224/4, the IPv6 ULA range fc00::/7, the IPv6 link-local range fe80::/10, and the reserved/multicast/unspecified blocks.
  • Hostnames whose DNS resolution returns any private IP across A and AAAA records. The validator calls socket.getaddrinfo(host, None) (consults both record types) and rejects on the first non-public result. This defends against multi-record DNS rebinding where one A record is public and another is 127.0.0.1.

The validator returns the (possibly NFKC-normalized) URL on success and raises SSRFBlocked (a ValueError subclass) otherwise. The fetch wrapper, WebMCPGenerator._safe_get, calls the validator on the initial URL and on every redirect target before following the redirect.

This is non-negotiable. The AutoWebMCP code path is the most likely SSRF surface in the runtime: it accepts a URL from the agent (which received it from the user, who may have pasted it from anywhere) and fetches it.

3.2 HTML structural parse

_SiteParser (in core/mcp/webmcp_generator.py) is a custom subclass of html.parser.HTMLParser. It collects:

  • The first <title> element’s text, accumulated across handle_data calls while the _in_title flag is set.
  • The first <meta name="description"> content attribute, with <meta property="og:description"> as a secondary fallback. Subsequent meta tags do not overwrite a description already captured.
  • Every <form> element, with its action, method, id, name, and a list of inputs. The current form is tracked in _cur_form and cleared on the matching </form>.
  • Every <input>, <textarea>, and <select> element whose type is not in {hidden, submit, button, reset, image} and which has a non-empty name or id. The element is appended to the current form’s input list if one is open, or to the standalone inputs list if not.
  • Every <a> whose href does not start with #, javascript:, mailto:, or tel:.

Deliberate choices:

  • Inputs without names are dropped, not synthesized. The source comment reads: “Without a name, the form field has no posting key. We could synthesise one, but it would never round-trip to the server — skip.” A schema with synthesized argument names would generate tool calls the server cannot accept.
  • Hidden, submit, button, reset, and image inputs are excluded. They are not user-supplied values; including them would mislead the LLM into setting them.
  • Form-internal and standalone inputs are kept separate. A search box outside a form produces a dedicated {site}_search tool; a search box inside a form is part of that form’s tool.

The parser is intentionally tolerant: malformed HTML is handled by the standard library parser’s recovery rules. AutoWebMCP does not “fix” pages; it captures what it can recognize and lets synthesis reject anything that would produce an unusable schema.

3.3 Spec sniffing

Before relying on the structural parse, the generator probes for an OpenAPI or Swagger document in two places:

  • Direct URL hint. If the URL ends in .json, .yaml, or .yml, or contains the substrings openapi or swagger, the generator fetches it directly and tries to parse it as a spec. A response is accepted if it contains a paths field and at least one of openapi, swagger, or info at the top level.
  • Well-known paths. The generator tries each of OPENAPI_PATHS (/openapi.json, /swagger.json, /api-docs, /api/openapi.json, /v1/openapi.json, /v2/api-docs, /api/swagger.json, /.well-known/mcp.json) in _fetch_openapi. Each candidate is built with urljoin(base_url, path) and fetched through _safe_get. The first 200 response with a JSON body that validates as a spec wins.

When a spec is found, _openapi_to_tools translates it:

  • The first servers[0].url (or the original base URL if servers is absent) becomes the canonical endpoint root. Server URLs starting with / are resolved against the original base URL.
  • Each (path, method) pair in paths becomes one tool. Methods outside {get, post, put, patch, delete} are ignored.
  • The tool’s operationId becomes its name (sanitized via _safe_name and prefixed with the site name); summary or description becomes its description.
  • Path and query parameters become input-schema properties, with the parameter type mapped to JSON-Schema (string, number, boolean, integer; anything else falls back to string).
  • A request body with a JSON content type and an object schema contributes its properties directly. A non-object body becomes a single body: string argument as a fallback.

The generator is defensive about malformed specs. The source has explicit checks for path_item being non-dict (a $ref placeholder), op being non-dict, parameters that are non-dict, and bodies whose content type isn’t application/json or */*. Each check is a short-circuit continue; a malformed segment doesn’t prevent the rest of the spec from being consumed.

When both a spec and HTML data exist, the generator merges: spec-derived tools first, HTML-derived form tools appended only if their endpoint is not already covered. This handles the common case of a site with an OpenAPI document for its REST API and a separate HTML contact form on the marketing page.

3.4 Schema synthesis

Synthesis turns the structural parse and any specs into a list of MCP tool definitions. The output shape, before the executor strips internal metadata, is:

{
  "name": "submit_contact_form",
  "description": "Submit the 'contact' form on https://example.com/contact via POST",
  "inputSchema": {
    "type": "object",
    "properties": {
      "name":    {"type": "string", "description": "Your name"},
      "email":   {"type": "string", "description": "Email address"},
      "message": {"type": "string", "description": "Your message"},
    },
    "required": ["name", "email", "message"],
  },
  "_meta": {
    "endpoint": "https://example.com/contact",
    "method":   "POST",
    "type":     "form",
  },
}

The internal _meta block is used by the executor to issue the actual HTTP request and is stripped from the tool definition exposed to the LLM, leaving canonical MCP fields plus endpoint, method, and type as separate top-level keys.

Synthesis always emits a generic {site}_navigate tool as the first entry — a low-cost catch-all for cases where the structural parse missed the action the agent wanted to take. Total tool count is capped at 20 per site. Sites with very large OpenAPI documents (a fully exported payment-API spec runs to several hundred operations) would otherwise overwhelm the agent’s catalog with low-relevance tools; the cap is a pragmatic ceiling that the user can override by re-running against more specific path roots.

4. Argument inference

The most consequential design choice in AutoWebMCP is how it translates HTML form metadata into JSON Schema. The mapping is defined in _form_to_tool and follows the table below.

HTML constructJSON-Schema fragmentNotes
<input type="text">{"type": "string"}Default.
<input type="email">{"type": "string"}Format hint not currently set; future work.
<input type="number">{"type": "number"}
<input type="range">{"type": "number"}Treated as number; min/max not currently propagated.
<input type="checkbox">{"type": "boolean"}
<input type="search">{"type": "string"}Standalone search inputs trigger a dedicated {site}_search tool.
<textarea>{"type": "string"}Treated as text input.
<select>{"type": "string"}Enum extraction from <option> is future work.
placeholder attributedescription fieldFalls back to "Value for {raw_name}" when absent.
required attributename appended to required[]
name attributeproperty key (after _safe_name)
id attributefallback property keyUsed only when name is absent.

The _safe_name helper replaces any non-alphanumeric character with _, truncates to 48 characters, and strips leading/trailing underscores; an empty result is mapped to the literal tool so the schema is always valid. The same sanitizer is applied to tool names and property keys, with collision handling for property keys: when two raw input names sanitize to the same key (the source comment cites "user-name" and "user_name" both becoming user_name), the second occurrence is suffixed with _2, the third with _3, and so on, tracked in seen_names.

Tool-name construction follows a similar pattern. The site name comes from the URL’s netloc (dots and dashes replaced by underscores) and is prefixed to the form’s id, name, or the literal form, producing names like example_com_contact_form or acme_com_search_form.

The methodology is conservative on purpose. We do not infer types from input names or guess at richer JSON-Schema constructs (format, pattern, minLength, maxLength) the markup does not directly declare. A future revision will read the HTML5 pattern, minlength, and maxlength attributes and emit corresponding constraints; today the schema is type-only, the minimum the LLM needs to produce a usable tool call.

5. Tool execution at runtime

A generated tool is not yet runnable. The executor in core/tools/webmcp_executor.py is the bridge from agent tool call to live HTTP request.

5.1 Registration and persistence

register_webmcp_tools(tools) adds each tool to _WEBMCP_REGISTRY (a module-global dict) and calls _persist_webmcp_node(tool). Persistence is best-effort: it imports core.memory.temporal.stamp_new_node lazily, builds a content blob with the description, endpoint, method, and a truncated JSON dump of the input schema, and stamps an L4 graph node with kind="webmcp_tool", permanent=True. If the import fails (Lite-mode, no graph), persistence is silently skipped — the in-memory registry still works for the session.

restore_webmcp_from_graph() is the complementary path. On startup, the executor opens the SQLite database at $CRACKEDCLAW_DB (defaulting to /tmp/crackedclaw.db), selects all rows with kind = 'webmcp_tool', and parses the content blob back into a registry entry. Tools that already exist in _WEBMCP_REGISTRY are not overwritten. The function returns the count restored so the agent can surface “5 WebMCP tools restored from previous session” at startup.

This is the cache layer Figure 1 marks with dashed edges. The expected lifecycle: first visit pays the parse-and-register cost; every subsequent process starts up with the catalog already populated; agent dispatch is unaffected by re-generation latency.

5.2 Invocation

The agent calls webmcp_invoke(tool_name, params). The schema for this entrypoint is defined as INVOKE_SCHEMA:

INVOKE_SCHEMA = {
    "name": "webmcp_invoke",
    "description": (
        "Invoke a previously generated WebMCP tool by name with the given parameters. "
        "Use webmcp_create first if the tool doesn't exist yet."
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "tool_name": {"type": "string"},
            "params":    {"type": "object"},
        },
        "required": ["tool_name"],
    },
}

The implementation:

  1. Looks up tool_name in _WEBMCP_REGISTRY. A miss returns {"error": ..., "available": [...]} with the first ten registry keys.
  2. Re-validates the registered endpoint through _ssrf_safe_url. This is the second SSRF check on the same URL (the first happened at generation time). Re-validation defends against a registry populated under one SSRF policy and invoked under a stricter one, against DNS records that have changed since registration, and against future tools whose endpoints are computed at invocation rather than at registration.
  3. Constructs an httpx.AsyncClient with follow_redirects=False, timeout=15.0, and verify=True. The redirect setting is deliberate: an automatic redirect from a public endpoint to a private IP would bypass the SSRF check. The TLS-verification setting is also deliberate; an earlier draft disabled it for testing, and the source comment notes that “disabling it allowed silent MitM of every WebMCP call.”
  4. Dispatches by method: GET and DELETE use params=params (URL query string); POST, PUT, and PATCH use json=params (JSON body). Unknown methods fall through to GET.
  5. Returns {"status_code", "body", "tool"} on success. The body is parsed as JSON if possible, otherwise truncated to 4000 characters. Exceptions are caught and returned as {"error": "webmcp call failed", "tool": tool_name} — the inner exception is deliberately not exposed, because a verbose upstream error is a prompt-injection vector.

5.3 Headless browser fallback

The HTTP path is sufficient for sites whose forms POST to plain URLs. Sites that depend on JavaScript to assemble the request payload, or that issue requests via XHR/fetch with computed signatures, are not. For these the agent falls back to browser.headless, which wraps a headless engine. The fallback is not automatic; the agent decides based on response shape whether to retry through the browser. A future revision will surface a “this site needs the browser” hint in the generated tool’s metadata to remove the round-trip.

6. SSRF defense in depth

This is the section auditors check first. AutoWebMCP issues outbound HTTP requests on behalf of the agent, with URLs derived from agent-supplied input. The defense has four layers, not one:

Layer 1 — Pre-fetch URL validation. core.security.ssrf.assert_public_url runs on every URL before the first HTTP request. Behavior is described in Section 3.1; it is the single source of truth across the runtime.

Layer 2 — DNS resolution check. The validator does not just check whether the URL parses to a private IP literal; it resolves the hostname via socket.getaddrinfo and rejects if any returned address (across A and AAAA) is non-public. This defends against three concrete attacks: (a) public-facing hostnames whose DNS records have been pointed at private IPs; (b) multi-record DNS rebinding where one record is public and another is loopback; (c) AAAA records that resolve to IPv6 ULA or link-local space when the v4 record is innocuous.

Layer 3 — Redirect chain re-validation. _safe_get follows redirects manually, calling _assert_public_url on every Location header before re-issuing. The redirect budget is six hops (one initial + five redirects). After six, SSRFBlocked("too many redirects") is raised. The httpx.AsyncClient is constructed with follow_redirects=False so the underlying library cannot bypass the wrapper.

Layer 4 — Invocation-time re-check. The executor calls _ssrf_safe_url at every invocation, even though the URL was already validated at generation time. From the source: “The registry can be populated by a prior agent action, so we validate every URL at invocation time — not just at registration. Resolve hostnames via DNS to catch CNAME/A-record tricks pointing at internal IPs.”

There is one explicit escape hatch. The invocation-time validator consults core.settings.is_enabled("pentest_ssrf_relaxed"). When set, the private-range block is lifted, but the validator still:

  • Refuses non-http(s) schemes.
  • Refuses plaintext HTTP entirely (per the comment at the top of _ssrf_safe_url: "plaintext http blocked — use https").
  • Refuses cloud-metadata IPs (169.254.169.254 and fd00:ec2::254) regardless of mode. The source justifies it: those IPs “are an attractive prompt-injection sink even on engagement networks.”

Pentest mode is gated behind the safety toggle framework (master paper section 6) with its own warning text, verbatim acknowledgment, and 60-second cooldown. It is intended for authorized engagement work where targets are by definition on RFC1918 networks.

What AutoWebMCP does not defend against. The fetch is not anonymous; the request carries the agent’s user-agent and any cookies configured into the browser action tool’s isolated context. A site that denies scraping via rate limits, CAPTCHAs, or IP bans will succeed. AutoWebMCP is not a scraping-evasion tool. Blocked fetches surface as {"fetch_error": "HTTP 403"}.

7. Evaluation

We are explicit about scope. AutoWebMCP V1-lite has been tested on a small, hand-curated set of pages chosen for category coverage. The paper does not claim a single open-web “success rate” because the input distribution is not representative.

7.1 Methodology

For each site we ran WebMCPGenerator.generate_from_url(url) against the public homepage and one to three additional path roots (search, contact, login where applicable). For each generated tool we manually inspected: (a) the tool name; (b) input-schema property names against the actual form input names; (c) the required list against actual required attributes; (d) the endpoint against the form’s action. For OpenAPI sites we additionally checked that the generator preferred the spec over the HTML parse and that spec-derived tools covered the operations a developer would expect.

Latency measurements come from time.perf_counter() brackets around _SiteParser and the synthesis loop, on a 4 vCPU x86_64 build host (16 GB RAM) with HTML bodies pre-fetched to local disk so the timing excludes network. The per-page DOM-analysis range over 18 sample pages was 18–96 ms with a median of 34 ms; the 96 ms outlier was the spec-merge case on the largest OpenAPI document tested (213 operations, reduced by the 20-tool cap).

We did not run the executor against test sites for first-call accuracy. Issuing real form submissions to third-party services without the operator’s consent is either irresponsible or pointless. Runtime evaluation is reported in Section 7.4 against a controlled internal harness.

7.2 Sites tested

The set was 18 pages drawn from:

  • Static, well-formed: an open-encyclopedia search; a popular news-aggregator submission form; the Memagen™ contact form.
  • OpenAPI-publishing: a payment-API public spec; an HTTP-debugging service; a developer-API documentation site.
  • Mixed (server-rendered with JS overlays): a major code-host login page; a Python web-framework documentation site; a typical CMS contact form.
  • Heavy SPA: a project-management landing page; a productivity-app signup page; a representative React marketing site.
  • CAPTCHA-protected: a major search engine; two of the above when fronted by a CAPTCHA proxy.

We use category descriptions rather than vendor names because the failure modes belong to the page type, not the brand.

7.3 Structural-parse results

On well-formed and mixed categories the parser identified every visible form and every named input. Tool names were sensible (encyclopedia_org_search, news_site_form, memagen_com_contact_form). Required-flag inference matched every case. Aggregating across the 7 well-formed and mixed pages, 41 of 43 generated argument names round-tripped to the target server on a dry run (95.3%); the two failures were a hidden-by-CSS field counted by the parser and a data-name attribute the markup used in place of name (which the parser correctly does not synthesize).

On the OpenAPI-publishing category the spec sniff succeeded for two of three sites via well-known paths; one required pointing the generator at the published spec URL directly. The third site does not publish a spec at any well-known path and fell back to the HTML parse.

On the heavy SPA category the parser captured almost nothing. The <form> elements on these pages are constructed by client-side JavaScript and are not present in the server-emitted HTML. The structural parse returned a {site}_navigate tool and nothing else. Documented limitation; see Section 8.

On the CAPTCHA-protected category the parser succeeded structurally (the form is in the markup) but the generated tool would fail at runtime because the CAPTCHA challenge is not a parameter the schema expresses. Also documented in Section 8.

7.4 Runtime invocation against a controlled harness

We built a small Flask harness exposing five forms (a contact form, a search form, a JSON-body POST endpoint, a redirect chain ending at a public endpoint, and a redirect chain ending at 127.0.0.1). We registered the harness URL and issued tool calls.

  • The four well-behaved forms succeeded; the executor issued the correct method (GET for search, POST for contact, POST with a JSON body for the JSON endpoint, GET with redirect-following for the public chain) and returned the expected response bodies. End-to-end median latency from webmcp_invoke call to response was 41 ms on localhost, 218 ms against an internet-routed echo endpoint.
  • The redirect-to-private-IP case was rejected at the second _assert_public_url call inside _safe_get, with SSRFBlocked("DNS record points to non-public IP: 127.0.0.1"). The error propagated cleanly to the agent.

We did not run the harness under load. Manual redirect handling and the second SSRF check require DNS resolution per hop, which under hostile DNS could add multiple seconds per redirect; we believe this is acceptable for an agent-facing tool, but it is a real cost and we report it.

7.5 What we did not measure

We did not measure: (a) end-to-end first-call success against real third-party services beyond the round-trip name-validation result above; (b) tool-selection accuracy when AutoWebMCP-generated tools are mixed with hand-authored MCP servers in the same catalog — see UIMCP for the compression layer that question depends on; (c) the false-positive rate of the SSRF guard against split-horizon DNS. Each requires infrastructure we have not yet built. AutoWebMCP works well in the cases its design targets and fails predictably in the cases it does not.

8. Limitations

We declare these explicitly because the alternative is dishonesty.

  • Single-page applications. React, Vue, Svelte, Solid, and similar frameworks do not emit <form> elements in the server response. The structural parse returns the navigate-tool stub and little else. The mitigation is the headless-browser fallback (Section 5.3); that path is not currently automatic.
  • Multi-step workflows. Wizards, multi-page checkouts, and OAuth flows span multiple URLs and require state between steps. AutoWebMCP generates one tool per page; composing tools into a workflow is the AutoMCP layer’s job (master paper section 9.1). The integration point — recording an AutoMCP macro that strings together AutoWebMCP-generated tools — is currently manual.
  • CAPTCHA-protected forms. The schema does not express a CAPTCHA challenge; a generated tool against one will produce a 403 or a redirect. CAPTCHA solving is out of scope.
  • Heavy JavaScript validation. Validation logic that lives in client-side JS (regex assembled at runtime, conditional required flags) is not captured. The schema reflects markup-declared constraints only; the server rejects mismatched arguments and the agent retries.
  • Best for deterministic form-submit workflows. AutoWebMCP fits “submit this form with these fields”: contact forms, search boxes, simple REST endpoints, OpenAPI-described APIs. It does not fit “navigate this site like a human” — interpret responses, follow visual affordances, recover from arbitrary errors. For those, headless browser with vision is the right substrate.
  • The 20-tool cap. Sites with very large OpenAPI documents are truncated at 20 tools. A future revision will accept a path-prefix filter to target a subset of a large spec.

9. Comparison to vision-based browser agents

Vision-based browser agents interpret the viewport as pixels and produce actions (clicks, types, scrolls). They handle JavaScript-heavy sites well because they do not depend on the page’s structural surface; they handle dynamic UI, pop-ups, and visual affordances; they succeed on many sites where AutoWebMCP fails.

The cost is real:

  • Per-action latency. Each action requires a screenshot to be sent to the model, the model to produce an action, and the action to be executed. Three to ten seconds per action is typical. AutoWebMCP-generated tools execute in roughly the latency of one HTTP round trip; our harness measured a 41 ms localhost median and a 218 ms wide-area median (Section 7.4).
  • Per-action token cost. Vision tokens are not free. A 1280×800 screenshot costs on the order of 1500 tokens at current pricing tiers; a twenty-action workflow spends 30,000 tokens in screenshots before any reasoning. AutoWebMCP carries the per-tool schema in the catalog (compressed via UIMCP, master paper section 9.3) and the per-call token cost is the size of the tool name and arguments.
  • Fragility under UI redesigns. Vision agents are sensitive to visual changes. AutoWebMCP-generated tools depend on form input names, which are typically stable across redesigns of the visible HTML.

The recommended pattern: AutoWebMCP first; vision-based agent for the cases AutoWebMCP cannot handle. The master paper documents the same pattern with PCMCP (section 9.4) for desktop applications: structural introspection of the accessibility tree first; vision-based fallback for canvas-rendered surfaces.

10. Future work

  • ARIA-tree parsing for SPA inference. Most modern SPAs ship accessibility trees that mirror their structural surface even when the DOM does not. Reading the rendered ARIA tree (after a brief headless render) could close the SPA gap without needing vision.
  • Cross-page workflow recording. AutoMCP can already record macros over typed action streams; the missing piece is the binding between a recorded macro and the AutoWebMCP-generated tools it invokes. Intended flow: the user runs a workflow once, AutoMCP records it, AutoWebMCP-generated tools are stitched into a parameterized recipe, and the recipe enters the catalog as a single high-level tool.
  • Schema validation roundtrip. The generator could optionally test a generated tool against the live site by issuing a no-op request (a HEAD, or a GET with empty parameters) and checking the response. This would catch tools whose endpoints have shifted or whose parameter shapes don’t match. Running it automatically against every generated tool is a politeness violation; as an opt-in step it would meaningfully improve first-call success rates.
  • HTML5 validation propagation. pattern, minlength, maxlength, min, max, and step are already in the markup the parser sees. Mapping them to JSON-Schema (pattern, minLength, maxLength, minimum, maximum, multipleOf) is straightforward and will produce richer schemas.
  • Enum extraction from <select>. Today every <select> is a string. Extracting the <option> values into a JSON-Schema enum would let the LLM choose from valid values without a runtime failure on the first wrong guess.
  • OpenAPI 3.1 union types and discriminators. The current spec consumer collapses union types to string. Faithful translation of oneOf, anyOf, and discriminated unions would benefit users targeting OpenAPI-rich enterprise APIs.

11. References

  1. Berners-Lee, T. (1989). Information Management: A Proposal. CERN. https://www.w3.org/History/1989/proposal.html
  2. Levitas, T. (2026). Memagen™: An Open-Core Agent Platform with Lenient Tool Routing, Capability-Gated Execution, and a Compound-Graph Memory Substrate. Memagen™ research papers. https://memagen.com/papers/memagen-master
  3. Levitas, T. (2026). AutoMCP: Recording and Replaying Tool Macros over Typed Action Streams. Memagen™ research papers. https://memagen.com/papers/2026-05-08-automcp
  4. Levitas, T. (2026). PCMCP: Accessibility-Tree-First Tool Synthesis for Desktop Applications. Memagen™ research papers. https://memagen.com/papers/2026-05-08-pcmcp
  5. Levitas, T. (2026). UIMCP: Adaptive Catalog Compression for Multi-Tool LLM Agent Dispatch. Memagen™ research papers. https://memagen.com/papers/2026-05-08-uimcp
  6. OAI (OpenAPI Initiative). (2017). OpenAPI Specification. https://www.openapis.org
  7. APIs.guru. (2023). OpenAPI directory: a curated registry of public OpenAPI documents. https://apis.guru
  8. Postman. (2024). State of the API Report 2024. https://www.postman.com/state-of-api/
  9. Eddy, S.R. (1996). Hidden Markov models. Current Opinion in Structural Biology, 6(3), 361–365.
  10. Nakano, R., Hilton, J., Balaji, S., et al. (2021). WebGPT: Browser-assisted question-answering with human feedback. arXiv:2112.09332
  11. Yao, S., Chen, H., Yang, J., & Narasimhan, K. (2022). WebShop: Towards scalable real-world web interaction with grounded language agents. NeurIPS 2022.

Source: https://github.com/tablevitas123/crackedclaw