Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions domain-skills/svo/zaehlerstandsmeldung.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# SVO — Zählerstandsmeldung via WebMCP

SVO's meter-reading form exposes a **WebMCP** tool, so you don't have to drive the DOM. See `interaction-skills/webmcp.md` for the general `navigator.modelContext` mechanics (the `executeTool` arg shape is the part that trips people up).

URL: `https://svo-test.de/service/zaehlerstandsmeldung` (test environment).

## Tool

`report_meter_reading` — fills the existing form's fields. **It never submits**: CAPTCHA + final submit stay a human action, and for privacy it returns no echo of the entered values. So verify by reading the DOM, not by the result text.

### Input schema

Required: `zaehlernummer`, `ablesedatum` (`YYYY-MM-DD`), `vorname`, `nachname`, `e_mail_adresse`, `telefonnummer_49`, `haben_sie_einen_ht_nt_zaehler` (boolean).

Meter-reading fields are **conditional on the tariff flag**:

- `haben_sie_einen_ht_nt_zaehler: true` → two-rate (HT/NT) meter → fill `zaehlerstand_ht` **and** `zaehlerstand_nt`. The single-rate field is hidden.
- `haben_sie_einen_ht_nt_zaehler: false` → single-rate meter → fill `zaehlerstand` only.

Never fill both the single-rate and the HT/NT fields.

## Call it (HT/NT example)

```python
import json
args = {
"zaehlernummer": "1ESY1160123456",
"ablesedatum": "2026-06-25",
"vorname": "Max",
"nachname": "Mustermann",
"e_mail_adresse": "max.mustermann@example.com",
"telefonnummer_49": "0171 2345678",
"haben_sie_einen_ht_nt_zaehler": True,
"zaehlerstand_ht": "012345",
"zaehlerstand_nt": "006789",
}

res = js(r'''(async (argsJson) => {
const mc = navigator.modelContext;
let tools = mc.getTools();
if (tools && typeof tools.then === 'function') tools = await tools;
const tool = tools.find(t => t.name === 'report_meter_reading');
let r = mc.executeTool(tool, argsJson); // RegisteredTool object + JSON STRING
if (r && typeof r.then === 'function') r = await r;
return r;
})(%s)''' % json.dumps(json.dumps(args)))
print(res)
```

## Verify

The HT/NT toggle is a radio group `haben_sie_einen_ht_nt_zaehler` (HT/NT vs. "Standardzähler"). After the call, read field values to confirm:

```python
print(js(r'''(() => {
const out = [];
document.querySelectorAll('input,select,textarea').forEach(el => {
if (el.type === 'hidden' || !el.name) return;
out.push({name: el.name, value: (el.type==='checkbox'||el.type==='radio') ? el.checked : el.value});
});
return out;
})()''')

@cubic-dev-ai cubic-dev-ai Bot Jun 25, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Syntax error: missing closing parenthesis for print() in the verify code example. The expression print(js(r'''...''') has an unclosed print( call.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At domain-skills/svo/zaehlerstandsmeldung.md, line 62:

<comment>Syntax error: missing closing parenthesis for `print()` in the verify code example. The expression `print(js(r'''...''')` has an unclosed `print(` call.</comment>

<file context>
@@ -0,0 +1,69 @@
+    out.push({name: el.name, value: (el.type==='checkbox'||el.type==='radio') ? el.checked : el.value});
+  });
+  return out;
+})()''')
+```
+
</file context>
Fix with cubic

```

## Notes

- There's a `honeypot` text input — leave it empty (spam trap; filling it likely flags the submission).
- The page has a fixed notification banner overlaying the lower viewport; `scrollIntoView`/`window.scrollTo` to bring form fields above it before screenshotting.
- Conversational use: read the tool's `inputSchema.required` first, then ask the user for the tariff type + the matching reading(s) before calling.

@cubic-dev-ai cubic-dev-ai Bot Jun 25, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The note advises reading inputSchema.required, but per the companion skill (interaction-skills/webmcp.md) the inputSchema is often a JSON-encoded string, not an object. Accessing .required directly on a string returns undefined. The note should include a JSON.parse() step before reading .required so the guidance works in the common case.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At domain-skills/svo/zaehlerstandsmeldung.md, line 69:

<comment>The note advises reading `inputSchema.required`, but per the companion skill (`interaction-skills/webmcp.md`) the `inputSchema` is **often a JSON-encoded string, not an object**. Accessing `.required` directly on a string returns `undefined`. The note should include a `JSON.parse()` step before reading `.required` so the guidance works in the common case.</comment>

<file context>
@@ -0,0 +1,69 @@
+
+- There's a `honeypot` text input — leave it empty (spam trap; filling it likely flags the submission).
+- The page has a fixed notification banner overlaying the lower viewport; `scrollIntoView`/`window.scrollTo` to bring form fields above it before screenshotting.
+- Conversational use: read the tool's `inputSchema.required` first, then ask the user for the tariff type + the matching reading(s) before calling.
</file context>
Fix with cubic

92 changes: 92 additions & 0 deletions interaction-skills/webmcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# WebMCP — calling page-exposed tools via `navigator.modelContext`

Some sites ship **WebMCP**: the page registers agent-callable tools on `navigator.modelContext`. Instead of scraping the DOM or hand-driving clicks, you discover the tool, read its JSON Schema, and call it — the page's own handler does the work (fill a form, run a search, etc.). Treat it the same way you'd treat a private API: prefer it over DOM automation when it's there.

## Detect

```python
res = js(r'''(() => {
const mc = window.navigator && navigator.modelContext;
if (!mc) return {webmcp: false};
return {webmcp: true, api: Object.getOwnPropertyNames(Object.getPrototypeOf(mc) || {})};
})()''')
print(res)
# {'webmcp': True, 'api': ['ontoolchange', 'executeTool', 'getTools', 'registerTool', 'constructor']}
```

The empty enumerable-keys case is normal — the surface lives on the prototype (`getTools`, `executeTool`, `registerTool`, `ontoolchange`).

## List the registered tools

`getTools()` may be sync or return a Promise — handle both. Each tool has `name`, `description`, and `inputSchema` (a JSON Schema; **often a JSON-encoded *string*, not an object** — parse it).

```python
import json
res = js(r'''(async () => {
const mc = navigator.modelContext;
let tools = mc.getTools();
if (tools && typeof tools.then === 'function') tools = await tools;
return (tools || []).map(t => ({name: t.name, description: t.description, inputSchema: t.inputSchema}));
})()''')
tools = res
for t in tools:
schema = t["inputSchema"]
if isinstance(schema, str):
schema = json.loads(schema) # inputSchema arrives as a JSON string
print(t["name"], "→ required:", schema.get("required"))
```

The schema's `required` array tells you exactly which inputs the tool needs — use it to drive a conversational "ask the user for the missing fields" flow before calling.

## Execute a tool — two non-obvious gotchas

`navigator.modelContext.executeTool` is **not** `executeTool(name, argsObject)`. Field-tested against Chrome's implementation (June 2026):

1. **First arg must be the `RegisteredTool` object itself** (the entry you got back from `getTools()`), **not** the tool name as a string. Passing a string → `TypeError: The provided value is not of type 'RegisteredTool'`.
2. **Second arg must be a JSON *string*** of the arguments, **not** a JS object. Passing an object → `UnknownError: Failed to parse input arguments`.

```python
import json
args = {"field_a": "value", "flag": True}

res = js(r'''(async (argsJson) => {
const mc = navigator.modelContext;
let tools = mc.getTools();
if (tools && typeof tools.then === 'function') tools = await tools;
const tool = tools.find(t => t.name === 'TOOL_NAME_HERE');
if (!tool) return {ok: false, err: 'tool not found'};
try {
let r = mc.executeTool(tool, argsJson); // (RegisteredTool, jsonString)
if (r && typeof r.then === 'function') r = await r;
return {ok: true, result: r};
} catch (e) { return {ok: false, err: String(e)}; }
})(%s)''' % json.dumps(json.dumps(args))) # double-encode: a JS string literal holding JSON
print(res)
```

The handler typically returns MCP-shaped content, e.g.
`{"content": [{"type": "text", "text": "..."}]}` — also often a JSON string, so parse it.

## Verify, don't trust the return value

WebMCP handlers may **intentionally not echo** what they did (privacy: "no entered values are returned"). After calling a tool that fills a form, confirm by reading the DOM yourself rather than relying on the result text:

```python
res = js(r'''(() => {
const out = [];
document.querySelectorAll('input,select,textarea').forEach(el => {
if (el.type === 'hidden') return;

@cubic-dev-ai cubic-dev-ai Bot Jun 25, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Verification snippet may leak sensitive form data by logging values from nearly all input fields (including non-hidden secrets).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At interaction-skills/webmcp.md, line 78:

<comment>Verification snippet may leak sensitive form data by logging values from nearly all input fields (including non-hidden secrets).</comment>

<file context>
@@ -0,0 +1,92 @@
+res = js(r'''(() => {
+  const out = [];
+  document.querySelectorAll('input,select,textarea').forEach(el => {
+    if (el.type === 'hidden') return;
+    out.push({name: el.name || el.id, value: (el.type==='checkbox'||el.type==='radio') ? el.checked : el.value});
+  });
</file context>
Fix with cubic

out.push({name: el.name || el.id, value: (el.type==='checkbox'||el.type==='radio') ? el.checked : el.value});
});
return out.filter(o => o.value !== '' && o.value !== false);
})()''')
print(res)
```

## Traps

- `inputSchema` and tool results are **JSON-encoded strings** as often as not — always `json.loads` defensively.
- `getTools()` / `executeTool()` may be sync or async — always guard with `typeof r.then === 'function'`.
- A well-behaved form-filling tool **won't submit** (it leaves CAPTCHA + submit to the human). Don't expect a success/redirect — verify field state instead.
- Tools can change at runtime; `ontoolchange` fires on re-registration. Re-`getTools()` if a tool you expected is missing.
- Cross-tab isolation: `navigator.modelContext` only exists in the tab that registered it. You reach it here because the harness runs JS *in that page* over CDP — a separate browser/agent tab cannot see another tab's `modelContext`.