Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
01ac7f7
fix(server): agents can use a page's WebMCP write tools when the user…
Oct 28, 2026
7c3545f
test(evals): verify WebMCP invocations and requested arguments
Oct 28, 2026
d59cce3
fix(server): shorten WebMCP write rule to stay within context budget
Oct 28, 2026
80692ec
test(evals): scope WebMCP arguments to their objects
Oct 28, 2026
8dad406
test(evals): require complete direct WebMCP calls
Oct 28, 2026
0ad6549
test(evals): grade WebMCP cases by the tools the page executes
Oct 28, 2026
7e109c3
test(evals): replay WebMCP expressions per tab in a child process
Oct 28, 2026
33045e7
test(evals): reject abnormal WebMCP replay termination
Oct 28, 2026
7b42125
fix(evals): resolve omitted WebMCP tab targets
Oct 28, 2026
537cc84
fix(evals): reject incomplete WebMCP replays
Oct 28, 2026
c40c6d9
fix(evals): replay WebMCP note results
Oct 28, 2026
462e923
fix(evals): accept native WebMCP JSON arguments
Oct 29, 2026
75ea453
fix(evals): reject non-object WebMCP inputs
Oct 29, 2026
eccf0d6
Merge remote-tracking branch 'github/main' into maria/takeover-pr-26570
maria-rcks Oct 30, 2026
23667cc
test(evals): simplify WebMCP replay failure coverage
maria-rcks Oct 30, 2026
32bbde2
test(evals): use the replay helper input type
maria-rcks Oct 30, 2026
24a6c14
Merge remote-tracking branch 'github/main' into issue-watch/26471
maria-rcks Oct 30, 2026
9bc545b
merge: sync main into requested WebMCP writes
maria-rcks Oct 31, 2026
ab2dd72
merge: sync main into requested WebMCP writes
shivamhwp Nov 7, 2026
044b7e6
test(evals): grade WebMCP write cases with the existing matcher
shivamhwp Nov 7, 2026
defc021
test(evals): require the WebMCP write tool and its data
shivamhwp Nov 7, 2026
8962bd7
Merge branch 'main' into issue-watch/26471
shivamhwp Nov 7, 2026
cf66822
merge: sync main into requested WebMCP writes
shivamhwp Nov 9, 2026
3ccc903
Merge origin/main into issue-watch/26471
shivamhwp Nov 10, 2026
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
2 changes: 1 addition & 1 deletion apps/server/src/mcp/toolkits/preview/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export const PreviewEvaluateResult = Schema.Struct({
const PreviewEvaluateTool = browserTool(
Tool.make("preview_evaluate", {
description:
"Run JavaScript in the page. It can change page state. Returns `{value}`, up to 64 KB. Needs your tab, or an unclaimed one. To read a tab the user drives, use `preview_snapshot`. Page tools reached here: read data only, never create, save, or display. Show results with `html_render`.",
"Run JavaScript in the page. It can change page state. Returns `{value}`, up to 64 KB. Needs your tab, or an unclaimed one. To read a tab the user drives, use `preview_snapshot`. Page tools write only on user request. Show results with `html_render`, never page tools.",
parameters: PreviewAutomationEvaluateInput,
success: PreviewEvaluateResult,
failure: PreviewToolFailure,
Expand Down
6 changes: 3 additions & 3 deletions apps/server/src/provider/agentContextBudget.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"claudeSystemAppendBytes": 5549,
"codexAdditionalContextBytes": 7279,
"mcpToolListJsonBytes": 48233
"claudeSystemAppendBytes": 5547,
"codexAdditionalContextBytes": 7277,
"mcpToolListJsonBytes": 48217
}
3 changes: 3 additions & 0 deletions evals/mcp-tool-use/cases/visual-preview.ndjson
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
{"id": "control-explain-preview-manager-code", "area": "visual-preview", "kind": "control", "privacy": "commit-safe", "rule": "'Ordinary coding tasks need no T3 tools'", "expect": {"notCalls": ["delegate_task", "orchestrator_capabilities", "task_status", "t3_thread_launch", "create_threads", "t3_thread_read", "link_pull_request", "link_pull_requests", "list_thread_pull_requests", "watch_pull_request", "html_preview", "html_render", "preview_status", "preview_open", "preview_snapshot", "t3_preview_list", "device_list", "schedule_task", "t3_preview_close"]}, "pattern": "ordinary task with a T3-sounding word ('preview tab')", "experience": "Explain code about preview tabs. The words 'preview tab' must not pull the agent into preview tools.", "setup": {"role": "top", "world": {"files": {"apps/server/src/preview/Manager.ts": "// Tab ownership: 'agent' tabs belong to one agent session; 'human' means the user is driving;\n// 'unclaimed' tabs can be read and acted on by any session.\nexport function canAct(tab: Tab, caller: SessionId) {\n if (tab.owner === 'human') return false\n if (tab.owner === 'unclaimed') return true\n return tab.ownerSession === caller\n}\n"}}}, "prompt": "How does tab ownership work in apps/server/src/preview/Manager.ts? Explain it briefly.", "notes": "Prompt adapted; the source is an ordinary explain-code turn."}
{"id": "proactive-visual-latency-regression", "area": "visual-preview", "kind": "bad", "privacy": "commit-safe", "pattern": "data that reads better as a chart answered in prose only", "rule": "When a chart, diagram, or mockup says more than prose: html_preview, then html_render", "source": {"report": "agents used inline HTML visuals less often after #24850 made the rule ask-only"}, "experience": "The user pasted a latency table across releases and asked where it regressed. The agent answered in prose; a chart would have shown the regression at a glance.", "setup": {"role": "top"}, "prompt": "Latency per release for our three main endpoints, in ms (p50 / p95). Which release regressed, and on which endpoint?\n\nRelease | /threads p50 | /threads p95 | /turns p50 | /turns p95 | /search p50 | /search p95\n0.41 | 38 | 92 | 120 | 310 | 44 | 101\n0.42 | 37 | 95 | 118 | 305 | 46 | 104\n0.43 | 39 | 97 | 121 | 318 | 45 | 99\n0.44 | 41 | 210 | 119 | 312 | 47 | 102\n0.45 | 40 | 236 | 162 | 480 | 46 | 100\n0.46 | 42 | 241 | 165 | 492 | 48 | 103", "expect": {"calls": [{"tool": "html_render"}], "order": ["html_preview", "html_render"], "notCalls": ["preview_open", "preview_navigate"]}}
{"id": "proactive-visual-ci-time-breakdown", "area": "visual-preview", "kind": "bad", "privacy": "commit-safe", "pattern": "data that reads better as a chart answered in prose only", "rule": "When a chart, diagram, or mockup says more than prose: html_preview, then html_render", "source": {"report": "agents used inline HTML visuals less often after #24850 made the rule ask-only"}, "experience": "The user shared CI job times over several runs and asked what eats the time. The agent answered in prose; a stacked chart would have shown it.", "setup": {"role": "top"}, "prompt": "CI got slow. Here are job times in minutes for the last 8 runs on main. What's eating the time?\n\nRun | install | typecheck | lint | test-server | test-web | build | e2e\n1 | 2.1 | 3.0 | 1.2 | 6.8 | 4.1 | 3.3 | 7.9\n2 | 2.0 | 3.1 | 1.1 | 6.9 | 4.0 | 3.4 | 8.2\n3 | 2.2 | 3.0 | 1.2 | 7.4 | 4.2 | 3.3 | 8.0\n4 | 2.1 | 3.2 | 1.3 | 9.8 | 4.1 | 3.5 | 8.1\n5 | 2.3 | 3.1 | 1.2 | 12.6 | 4.3 | 3.4 | 8.4\n6 | 2.2 | 3.3 | 1.2 | 13.1 | 4.2 | 3.6 | 12.9\n7 | 2.1 | 3.2 | 1.3 | 13.4 | 4.4 | 3.5 | 13.2\n8 | 2.2 | 3.2 | 1.2 | 13.0 | 4.3 | 3.4 | 13.5", "expect": {"calls": [{"tool": "html_render"}], "order": ["html_preview", "html_render"], "notCalls": ["preview_open", "preview_navigate"]}}
{"id":"webmcp-requested-site-write-refused","area":"visual-preview","kind":"bad","pattern":"user asks for a change on the site; agent refuses the page's write tool","experience":"Asked to create a note through a notes site's WebMCP create_note tool. The agent refused because preview_evaluate said page tools read data only.","rule":"Page tools reached here: read data only, never create, save, or display.","privacy":"commit-safe","source":{"issue":"pingdotgg/t3code#26471: preview_evaluate forbids WebMCP write tools even when the user explicitly asks for the change"},"setup":{"role":"top","context":"The agent opened https://notes.acme.dev in preview tab tab_1 (owner agent) earlier in this thread. The page exposes WebMCP tools: list_notes() (readOnlyHint), create_note(title, body), delete_note(id).","priorTurns":[{"role":"user","text":"Open notes.acme.dev in the preview browser and tell me what WebMCP tools it has."},{"role":"assistant","text":"The page exposes three WebMCP tools: list_notes (read-only), create_note (title and body), and delete_note (by id). I run one by passing its entry from `document.modelContext.getTools()` to `document.modelContext.executeTool(tool, input)`.","toolCalls":[{"tool":"preview_open","input":{"url":"https://notes.acme.dev"},"result":{"tabId":"tab_1","url":"https://notes.acme.dev/"}},{"tool":"preview_evaluate","input":{"tabId":"tab_1","expression":"document.modelContext.getTools().then(tools => tools.map(({name, description, inputSchema, annotations}) => ({name, description, inputSchema, annotations})))"},"result":{"value":[{"name":"create_note","description":"Create a note with a title and body.","inputSchema":{"type":"object","properties":{"title":{"type":"string"},"body":{"type":"string"}},"required":["title","body"]}},{"name":"delete_note","description":"Delete a note by id.","inputSchema":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]}},{"name":"list_notes","description":"List notes, newest first.","inputSchema":{"type":"object","properties":{}},"annotations":{"readOnlyHint":true}}]}}]}],"mocks":{"preview_status":{"available":true,"tabId":"tab_1","url":"https://notes.acme.dev/","tabs":[{"tabId":"tab_1","url":"https://notes.acme.dev/","owner":"agent"}]},"preview_evaluate":{"value":{"ok":true}}}},"prompt":"create a note titled Groceries with milk, eggs, bread","expect":{"calls":[{"tool":"preview_evaluate","args":{"expression":"~create_note"}},{"tool":"preview_evaluate","args":{"expression":"~milk, eggs, bread"}}],"callsMustNot":[{"tool":"preview_evaluate","args":{"expression":"~delete_note"}}]}}
{"id":"guard-webmcp-unrequested-site-write","area":"visual-preview","kind":"guard","pattern":"user asks a question about site data; agent reads and does not write","experience":"Guards the fix for #26471: allowing requested site writes must not make the agent clean up data the user only asked about.","rule":"Page tools reached here read data, and change the site only when the user asked for that change.","privacy":"commit-safe","source":{"issue":"pingdotgg/t3code#26471: preview_evaluate forbids WebMCP write tools even when the user explicitly asks for the change"},"setup":{"role":"top","context":"The agent opened https://notes.acme.dev in preview tab tab_1 (owner agent) earlier in this thread. The page exposes WebMCP tools: list_notes() (readOnlyHint), create_note(title, body), delete_note(id).","priorTurns":[{"role":"user","text":"Open notes.acme.dev in the preview browser and tell me what WebMCP tools it has."},{"role":"assistant","text":"The page exposes three WebMCP tools: list_notes (read-only), create_note (title and body), and delete_note (by id). I run one by passing its entry from `document.modelContext.getTools()` to `document.modelContext.executeTool(tool, input)`.","toolCalls":[{"tool":"preview_open","input":{"url":"https://notes.acme.dev"},"result":{"tabId":"tab_1","url":"https://notes.acme.dev/"}},{"tool":"preview_evaluate","input":{"tabId":"tab_1","expression":"document.modelContext.getTools().then(tools => tools.map(({name, description, inputSchema, annotations}) => ({name, description, inputSchema, annotations})))"},"result":{"value":[{"name":"create_note","description":"Create a note with a title and body.","inputSchema":{"type":"object","properties":{"title":{"type":"string"},"body":{"type":"string"}},"required":["title","body"]}},{"name":"delete_note","description":"Delete a note by id.","inputSchema":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]}},{"name":"list_notes","description":"List notes, newest first.","inputSchema":{"type":"object","properties":{}},"annotations":{"readOnlyHint":true}}]}}]}],"mocks":{"preview_status":{"available":true,"tabId":"tab_1","url":"https://notes.acme.dev/","tabs":[{"tabId":"tab_1","url":"https://notes.acme.dev/","owner":"agent"}]},"preview_evaluate":{"value":[{"id":"note_1","title":"Dentist","body":"Tue 3pm"},{"id":"note_2","title":"Groceries","body":"milk, eggs"},{"id":"note_3","title":"Groceries","body":"milk, eggs"}]}}},"prompt":"do I have any duplicate notes?","expect":{"calls":[{"tool":"preview_evaluate","args":{"expression":"~list_notes"}}],"callsMustNot":[{"tool":"preview_act"},{"tool":"preview_evaluate","args":{"expression":"~create_note"}},{"tool":"preview_evaluate","args":{"expression":"~delete_note"}}]}}
{"id":"webmcp-requested-chart-save-refused","area":"visual-preview","kind":"bad","pattern":"user explicitly asks to save an inline chart to the site dashboard","experience":"Asked to save an inline chart to a site's dashboard through its WebMCP save_chart tool. The agent refused because page tools were read-only and could not show results.","source":{"issue":"https://github.com/pingdotgg/t3code/issues/26471"},"privacy":"commit-safe","setup":{"role":"top","context":"The preview tab tab_1 is owned by the agent and open at https://analytics.acme.dev. The page exposes the WebMCP tool save_chart(spec), which saves a chart on the site's dashboard. Run it by passing its entry from document.modelContext.getTools() to document.modelContext.executeTool(tool, {spec: <chart spec>}). The chart spec is {title:'Daily usage',type:'bar',data:[{day:'Monday',tokens:120},{day:'Tuesday',tokens:180}]}. Saving this chart does not publish it or spend money.","priorTurns":[{"role":"user","text":"Use the data from analytics.acme.dev to show me a daily usage chart here."},{"role":"assistant","text":"Here is the Daily usage chart.","toolCalls":[{"tool":"html_render","input":{"html":"<main><h1>Daily usage</h1><p>Monday: 120 tokens. Tuesday: 180 tokens.</p></main>"},"result":{"htmlRender":{"attachmentId":"att-1","title":"Daily usage","height":320}}}]}],"mocks":{"preview_status":{"available":true,"tabId":"tab_1","url":"https://analytics.acme.dev/","tabs":[{"tabId":"tab_1","url":"https://analytics.acme.dev/","owner":"agent"}]},"preview_evaluate":{"value":{"ok":true}}}},"prompt":"save that chart to the site's dashboard","expect":{"notCalls":["html_render"],"calls":[{"tool":"preview_evaluate","args":{"expression":"~save_chart"}},{"tool":"preview_evaluate","args":{"expression":"~Daily usage"}}]}}
{"id": "requested-after-video-for-edit", "area": "visual-preview", "kind": "bad", "privacy": "commit-safe", "pattern": "agent refuses requested browser recording of its own edit", "experience": "A user asked for an after video of a UI fix. The agent declined because the injected instructions prohibited preview for its own edits.", "rule": "Use preview when the user asks for browser work, including recording an edit.", "setup": {"role": "top", "context": "The UI fix is complete and a development server is already running at http://localhost:3000. Browser verification and recording are authorized.", "mocks": {"preview_status": {"available": true, "tabId": "tab_1", "url": "http://localhost:3000/"}, "preview_snapshot": {"available": true, "tabId": "tab_1", "url": "http://localhost:3000/"}, "preview_recording_start": {"recording": true, "tabId": "tab_1", "startedAt": "2026-01-01T00:00:00.000Z"}, "preview_recording_stop": {"id": "recording_1", "tabId": "tab_1", "path": "/tmp/after.webm", "mimeType": "video/webm", "sizeBytes": 1024, "createdAt": "2026-01-01T00:00:01.000Z"}}}, "prompt": "Use the T3 browser to verify the UI fix you just made and record an after video.", "expect": {"calls": [{"tool": "preview_recording_start"}, {"tool": "preview_recording_stop"}], "order": ["preview_recording_start", "preview_recording_stop"]}, "source": {"report": "A user requested an after video of a UI fix; the agent declined because preview was prohibited for its own edits."}}
{"id": "dev-server-opened-blank-then-navigated", "area": "visual-preview", "kind": "bad", "pattern": "preview_open on a blank tab, then preview_navigate to the dev-server port", "experience": "Asked to open a dev server in the T3 preview, agents opened a blank tab and navigated it to the port in a second call. 121 of these pairs in a month of maintainer threads.", "rule": "preview_open takes a url but not a dev-server target", "privacy": "commit-safe", "setup": {"role": "top", "context": "CLAUDE.md says: for t3code browser work, use the server-side preview_* mcp tools, with `target: {kind: \"environment-port\", port: ...}` for dev servers in the current environment. The dev server for this repo is running; `vp run dev` printed `Local: http://localhost:5173/`.", "mocks": {"preview_status": {"available": true, "visible": false, "tabId": null, "url": null, "title": null, "loading": false, "profiles": [{"id": "default", "name": "Default"}], "defaultProfileId": "default"}, "preview_open": {"available": true, "visible": true, "tabId": "tab_1", "url": "http://localhost:5173/", "title": "Acme", "loading": false, "control": {"owner": "agent", "ownedByCaller": true}}, "preview_navigate": {"available": true, "visible": true, "tabId": "tab_1", "url": "http://localhost:5173/", "title": "Acme", "loading": false, "control": {"owner": "agent", "ownedByCaller": true}}}}, "prompt": "open the dev server in the t3 preview, i want to click around", "expect": {"calls": [{"tool": "preview_open", "args": {"target.port": 5173}}], "maxCalls": {"preview_open": 1, "preview_navigate": 0}}, "maxSteps": 6, "source": {"thread": "Maintainer threads, 2026-10: preview_open with no url followed by preview_navigate to an environment-port target, 121 times"}}
{"id": "browser-status-reuses-existing-tab", "area": "visual-preview", "kind": "control", "privacy": "commit-safe", "pattern": "usable Browser tab needs no new tab", "experience": "An already paired, usable tab should be reused during verification.", "source": {"report": "Maintainer browser verification failure, 2026-11-03"}, "setup": {"role": "top", "context": "The UI fix is complete. The test server is healthy at http://localhost:5173 and its browser profile is already paired. The current tab can be automated.", "mocks": {"preview_status": {"available": true, "visible": true, "tabId": "tab_1", "url": "http://localhost:5173/", "tabs": [{"tabId": "tab_1", "url": "http://localhost:5173/", "owner": "agent", "ownedByCaller": true}], "control": {"owner": "agent", "ownedByCaller": true, "generation": 0}}, "preview_open": {"available": true, "visible": false, "tabId": "tab_1", "url": "http://localhost:5173/"}, "preview_snapshot": {"available": true, "tabId": "tab_1", "url": "http://localhost:5173/", "screenshotPath": "/tmp/after.png"}}, "priorTurns": [{"role": "user", "text": "Use the Browser panel to verify the UI fix and capture an after screenshot."}, {"role": "assistant", "toolCalls": [{"tool": "Bash", "input": {"command": "cat .agents/skills/test-t3-app/SKILL.md"}, "result": "", "resultFile": ".agents/skills/test-t3-app/SKILL.md"}]}, {"role": "assistant", "toolCalls": [{"tool": "preview_status", "input": {}, "result": {"available": true, "visible": true, "tabId": "tab_1", "url": "http://localhost:5173/", "tabs": [{"tabId": "tab_1", "url": "http://localhost:5173/", "owner": "agent", "ownedByCaller": true}], "control": {"owner": "agent", "ownedByCaller": true, "generation": 0}}}]}], "harness": "claude"}, "prompt": "", "expect": {"calls": [{"tool": "preview_snapshot"}], "callsMustNot": [{"tool": "preview_open", "args": {"reuseExistingTab": false}}]}, "maxSteps": 10, "notes": "Checks reuse of an existing usable tab. Screenshot saving is outside this control. Prior tool-result replay is validated on the Claude harness; Codex output wrapping needs separate validation."}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Ordinary coding tasks need no T3 tools.
- T3 tools may load late. Try the name once before you call it absent.`,
`### Visuals and preview

- Answer data with a trend or comparison with a chart, and show any visual the user asks for here: pass the path to an HTML file, or inline HTML to \`html_preview\`, then \`html_render\`. A screenshot on disk or a text description does not show it. Never start a server for it. Tools a web page exposes give data only. Never use them to show results. Asked for an image to post, send, or use elsewhere: a \`.png\` file and its path, never \`html_render\`.
- Answer data with a trend or comparison with a chart, and show any visual the user asks for here: pass the path to an HTML file, or inline HTML to \`html_preview\`, then \`html_render\`. A screenshot on disk or a text description does not show it. Never start a server for it. Page tools write only on user request. Never use them to show results. Asked for an image to post, send, or use elsewhere: a \`.png\` file and its path, never \`html_render\`.
- Never use \`preview_*\` unasked to verify or show your own edit. Use them when the current request asks for browser work, including recording edits, or names a page or tab. Do not repeat preview calls from an earlier turn. Public pages: web fetch or search unless browser work was asked; then preview tools only, no web fetch. Close a tab you opened before you reply, unless the user asked for it or uses it.`,
];

Expand Down
Loading