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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user