Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16fb5e4350 |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ class FormWithCaptcha(forms.Form):
|
|||||||
captcha = ReCaptchaField(
|
captcha = ReCaptchaField(
|
||||||
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,
|
||||||
private_key=settings.RECAPTCHA_PRIVATE_KEY,
|
private_key=settings.RECAPTCHA_PRIVATE_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>
|
||||||
|
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user