1 Commits
Author SHA1 Message Date
westfarn 16fb5e4350 Fix WebMCP schema validity on contact form for Lighthouse audits.
Add toolparamdescription to contact fields, move reCAPTCHA outside the annotated form, and always render declarative WebMCP form attributes.
2026-07-02 13:02:09 -05:00
7 changed files with 67 additions and 22 deletions
+2 -2
View File
@@ -32,7 +32,7 @@ Test at minimum:
When `WEBMCP_ENABLED=True`: When `WEBMCP_ENABLED=True`:
- Navigation tools (`list_services`, `get_page_content`, `navigate_to_service`, `open_contact_with_subject`) load on all public pages - Navigation tools (`list_services`, `get_page_content`, `navigate_to_service`, `open_contact_with_subject`) load on all public pages
- Contact page registers `submit_contact_inquiry` and annotates the HTML form with `toolname` / `tooldescription` - Contact page registers `submit_contact_inquiry` via declarative form annotations (`toolname`, `tooldescription`, `toolparamdescription`); reCAPTCHA renders outside the annotated form
- Default is **disabled** (`WEBMCP_ENABLED=False`) until deliberately enabled per environment - Default is **disabled** (`WEBMCP_ENABLED=False`) until deliberately enabled per environment
Full tool catalog, Chrome flag setup, and reCAPTCHA notes: **[docs/webmcp.md](webmcp.md)** Full tool catalog, Chrome flag setup, and reCAPTCHA notes: **[docs/webmcp.md](webmcp.md)**
@@ -45,7 +45,7 @@ Before merging public-facing template or CSS changes:
2. Nav dropdowns use `<button>` triggers with `aria-expanded` / `aria-haspopup`. 2. Nav dropdowns use `<button>` triggers with `aria-expanded` / `aria-haspopup`.
3. Cookie consent banner stays `position: fixed` (no document flow shift). 3. Cookie consent banner stays `position: fixed` (no document flow shift).
4. `GET /robots.txt`, `/sitemap.xml`, `/llms.txt` return 200. 4. `GET /robots.txt`, `/sitemap.xml`, `/llms.txt` return 200.
5. With `WEBMCP_ENABLED=True`, public pages include `webmcp-config` and `webmcp-tools.js`; contact form has `toolname` when enabled. 5. With `WEBMCP_ENABLED=True`, public pages include `webmcp-config` and `webmcp-tools.js`; contact form always has `toolname`, `tooldescription`, and `toolparamdescription` on fields.
6. Re-run Lighthouse agentic-browsing on homepage and contact page (Chrome experimental flag on). 6. Re-run Lighthouse agentic-browsing on homepage and contact page (Chrome experimental flag on).
## References ## References
+10 -6
View File
@@ -26,18 +26,18 @@ Serve the site over HTTPS (or `localhost`) — WebMCP requires a secure context.
### 3. Lighthouse audit ### 3. Lighthouse audit
```bash ```bash
npx lighthouse@latest http://127.0.0.1:8000/contact \ npx lighthouse@latest https://aimloperations.com/contact \
--only-categories=agentic-browsing \ --only-categories=agentic-browsing \
--chrome-flags="--enable-experimental-web-platform-features" \ --chrome-flags="--enable-experimental-web-platform-features" \
--output=html --output-path=agentic-browsing-contact.html --output=html --output-path=agentic-browsing-contact.html
npx lighthouse@latest http://127.0.0.1:8000/ \ npx lighthouse@latest https://aimloperations.com/ \
--only-categories=agentic-browsing \ --only-categories=agentic-browsing \
--chrome-flags="--enable-experimental-web-platform-features" \ --chrome-flags="--enable-experimental-web-platform-features" \
--output=html --output-path=agentic-browsing-home.html --output=html --output-path=agentic-browsing-home.html
``` ```
Run against a local dev server with `WEBMCP_ENABLED=True`. For contact submission audits, `DEBUG=True` skips reCAPTCHA validation. Use `https://` URLs to avoid redirect warnings. Run with `WEBMCP_ENABLED=True` on the target environment.
## Registered tools ## Registered tools
@@ -66,14 +66,18 @@ Run against a local dev server with `WEBMCP_ENABLED=True`. For contact submissio
### Declarative form annotation ### Declarative form annotation
The contact `<form>` also declares WebMCP coverage via HTML attributes when enabled: The contact `<form>` declares WebMCP coverage via HTML attributes (always rendered, independent of `WEBMCP_ENABLED`):
```html ```html
<form toolname="submit_contact_inquiry" <form id="contact-form" toolname="submit_contact_inquiry"
tooldescription="Submit a contact inquiry to AI ML Operations"> tooldescription="Submit a contact inquiry to AI ML Operations">
<input name="name" toolparamdescription="Full name of the person submitting the inquiry." required>
...
</form>
<!-- reCAPTCHA renders outside the annotated form and links via form="contact-form" -->
``` ```
This satisfies Lighthouse **form coverage** without duplicating field schemas in JavaScript. The imperative `registerTool` on `/contact` adds structured `execute` behavior with success/error responses. The imperative `registerTool` on `/contact` adds structured `execute` behavior with success/error responses when `WEBMCP_ENABLED=True`.
## Data source ## Data source
+1
View File
@@ -8,6 +8,7 @@ class FormWithCaptcha(forms.Form):
widget=ReCaptchaV3( widget=ReCaptchaV3(
attrs={ attrs={
'required_score': 0.85, 'required_score': 0.85,
'form': 'contact-form',
} }
), ),
public_key=settings.RECAPTCHA_PUBLIC_KEY, public_key=settings.RECAPTCHA_PUBLIC_KEY,
@@ -780,6 +780,14 @@ input:focus, select:focus, textarea:focus {
margin: 0 0 1rem 1.5rem; margin: 0 0 1rem 1.5rem;
} }
.contact-captcha-group {
margin-top: 1rem;
}
.contact-captcha-group + .btn {
margin-top: 1rem;
}
/* Cookie consent banner */ /* Cookie consent banner */
.cookie-consent-banner { .cookie-consent-banner {
position: fixed; position: fixed;
@@ -0,0 +1,8 @@
{% include "django_recaptcha/includes/js_v3.html" %}
<input
type="hidden"
name="{{ widget.name }}"
class="g-recaptcha"
form="contact-form"
{% for name, value in widget.attrs.items %}{% if value is not False %} {{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}{% endfor %}
>
@@ -33,41 +33,53 @@
<div class="card"> <div class="card">
<h2 class="card-title contact-form-heading" id="contact-form-heading" style="margin-bottom: 2rem;">Send Us a Message</h2> <h2 class="card-title contact-form-heading" id="contact-form-heading" style="margin-bottom: 2rem;">Send Us a Message</h2>
<form action="{% url 'contact' %}" method="POST" aria-labelledby="contact-form-heading" <form id="contact-form" action="{% url 'contact' %}" method="POST" aria-labelledby="contact-form-heading"
{% if webmcp_enabled %}toolname="submit_contact_inquiry" tooldescription="Submit a contact inquiry to AI ML Operations"{% endif %}> toolname="submit_contact_inquiry"
tooldescription="Submit a contact inquiry to AI ML Operations">
{% csrf_token %} {% csrf_token %}
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label class="form-label" for="contact-name">Your Name</label> <label class="form-label" for="contact-name">Your Name</label>
<input type="text" class="form-control" name="name" id="contact-name" autocomplete="name" required> <input type="text" class="form-control" name="name" id="contact-name" autocomplete="name" required
toolparamdescription="Full name of the person submitting the inquiry.">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label" for="contact-email">Your Email</label> <label class="form-label" for="contact-email">Your Email</label>
<input type="email" class="form-control" name="email" id="contact-email" autocomplete="email" required> <input type="email" class="form-control" name="email" id="contact-email" autocomplete="email" required
toolparamdescription="Email address where AI ML Operations can reply.">
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label" for="contact-subject">Subject</label> <label class="form-label" for="contact-subject">Subject</label>
<input type="text" class="form-control" name="subject" id="contact-subject" value="{{ request.GET.subject|default:'' }}" required> <input type="text" class="form-control" name="subject" id="contact-subject" value="{{ request.GET.subject|default:'' }}" required
toolparamdescription="Short summary of the inquiry topic or service of interest.">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label" for="contact-message">Message</label> <label class="form-label" for="contact-message">Message</label>
<textarea name="message" class="form-control" id="contact-message" rows="5" placeholder="What workflow is costing you the most time? What systems does it touch?"></textarea> <textarea name="message" class="form-control" id="contact-message" rows="5"
</div> placeholder="What workflow is costing you the most time? What systems does it touch?"
toolparamdescription="Optional details about the workflow, bottleneck, or systems involved."></textarea>
<div class="form-group">
{% if capchaForm %}
{{ capchaForm }}
{% endif %}
</div> </div>
{% if not capchaForm %}
<button class="btn" type="submit" <button class="btn" type="submit"
data-tianji-event="contact_form_submit"> data-tianji-event="contact_form_submit">
Send Message Send Message
</button> </button>
{% endif %}
</form> </form>
{% if capchaForm %}
<div class="form-group contact-captcha-group">
{{ capchaForm }}
</div>
<button class="btn" type="submit" form="contact-form"
data-tianji-event="contact_form_submit">
Send Message
</button>
{% endif %}
</div> </div>
</div> </div>
+12
View File
@@ -196,10 +196,22 @@ class WebMcpTests(TestCase):
response = self.client.get(reverse("contact")) response = self.client.get(reverse("contact"))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, 'id="contact-form"')
self.assertContains(response, 'toolname="submit_contact_inquiry"') self.assertContains(response, 'toolname="submit_contact_inquiry"')
self.assertContains(response, "tooldescription=") self.assertContains(response, "tooldescription=")
self.assertContains(response, "toolparamdescription=")
self.assertContains(response, "submit_contact_inquiry") self.assertContains(response, "submit_contact_inquiry")
@override_settings(DEBUG=False)
def test_contact_page_renders_captcha_outside_annotated_form(self):
response = self.client.get(reverse("contact"))
self.assertEqual(response.status_code, 200)
form_end = response.content.index(b"</form>")
captcha_index = response.content.index(b"g-recaptcha")
self.assertGreater(captcha_index, form_end)
self.assertContains(response, b'form="contact-form"')
def test_webmcp_services_json_includes_all_service_pages(self): def test_webmcp_services_json_includes_all_service_pages(self):
response = self.client.get(reverse("public_index")) response = self.client.get(reverse("public_index"))