Fix WebMCP schema validity on contact form (#13)
Deploy Company Site / test (push) Successful in 9s
Deploy Company Site / deploy (push) Successful in 4s

## Summary

Follow-up to PR #12 / Lighthouse agentic-browsing audit on `/contact` (score 0.88).

- Add `toolparamdescription` to all contact form fields for WebMCP schema validity
- Move reCAPTCHA outside the annotated `<form>` so `g-recaptcha-response` is excluded from the declarative tool schema
- Always render `toolname` / `tooldescription` on the contact form (not gated by `WEBMCP_ENABLED`)
- Override `django_recaptcha/widget_v3.html` to associate captcha with `form="contact-form"`
- Update docs with HTTPS Lighthouse commands

## Test plan

- [x] `python manage.py test public.tests` — 21 tests pass
- [ ] Deploy and re-run Lighthouse agentic-browsing on `https://aimloperations.com/contact`
- [ ] Confirm `webmcp-schema-validity` passes (expected overall score 1.0)

Reviewed-on: #13
This commit was merged in pull request #13.
This commit is contained in:
2026-07-02 18:02:44 +00:00
parent bb5aa6de82
commit 2fb5204614
7 changed files with 67 additions and 22 deletions
+2 -2
View File
@@ -32,7 +32,7 @@ Test at minimum:
When `WEBMCP_ENABLED=True`:
- 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
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`.
3. Cookie consent banner stays `position: fixed` (no document flow shift).
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).
## References
+10 -6
View File
@@ -26,18 +26,18 @@ Serve the site over HTTPS (or `localhost`) — WebMCP requires a secure context.
### 3. Lighthouse audit
```bash
npx lighthouse@latest http://127.0.0.1:8000/contact \
npx lighthouse@latest https://aimloperations.com/contact \
--only-categories=agentic-browsing \
--chrome-flags="--enable-experimental-web-platform-features" \
--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 \
--chrome-flags="--enable-experimental-web-platform-features" \
--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
@@ -66,14 +66,18 @@ Run against a local dev server with `WEBMCP_ENABLED=True`. For contact submissio
### 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
<form toolname="submit_contact_inquiry"
<form id="contact-form" toolname="submit_contact_inquiry"
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
+3 -2
View File
@@ -7,8 +7,9 @@ class FormWithCaptcha(forms.Form):
captcha = ReCaptchaField(
widget=ReCaptchaV3(
attrs={
'required_score':0.85,
}
'required_score': 0.85,
'form': 'contact-form',
}
),
public_key=settings.RECAPTCHA_PUBLIC_KEY,
private_key=settings.RECAPTCHA_PRIVATE_KEY,
@@ -780,6 +780,14 @@ input:focus, select:focus, textarea:focus {
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 {
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">
<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"
{% if webmcp_enabled %}toolname="submit_contact_inquiry" tooldescription="Submit a contact inquiry to AI ML Operations"{% endif %}>
<form id="contact-form" action="{% url 'contact' %}" method="POST" aria-labelledby="contact-form-heading"
toolname="submit_contact_inquiry"
tooldescription="Submit a contact inquiry to AI ML Operations">
{% csrf_token %}
<div class="form-row">
<div class="form-group">
<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 class="form-group">
<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 class="form-group">
<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 class="form-group">
<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>
</div>
<div class="form-group">
{% if capchaForm %}
{{ capchaForm }}
{% endif %}
<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?"
toolparamdescription="Optional details about the workflow, bottleneck, or systems involved."></textarea>
</div>
{% if not capchaForm %}
<button class="btn" type="submit"
data-tianji-event="contact_form_submit">
Send Message
</button>
{% endif %}
</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>
+12
View File
@@ -196,10 +196,22 @@ class WebMcpTests(TestCase):
response = self.client.get(reverse("contact"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'id="contact-form"')
self.assertContains(response, 'toolname="submit_contact_inquiry"')
self.assertContains(response, "tooldescription=")
self.assertContains(response, "toolparamdescription=")
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):
response = self.client.get(reverse("public_index"))