feat(skills&butler) DevTools MCP Server Example for user

:Release Notes:
- Added skills examples for MS Teams work
- Added agent file "browser-butler" for actual work execution

:Detailed Notes:
-

:Testing Performed:
-

:QA Notes:
- Tested with claude code and opencode

:Issues Addressed:
- Done for Chrome DevTools MCP Server seminar
This commit is contained in:
Artur Mukhamadiev 2026-06-27 19:13:58 +03:00
commit 71833d83d8
13 changed files with 1010 additions and 0 deletions

View File

@ -0,0 +1,117 @@
---
name: browser-butler
description: Use proactively for ANY natural-language web browser task via the Chrome DevTools MCP — e.g. "watch X", "fill the form for me", "find a 5-star restaurant near me", "book tickets from A to B", "check what's in my cart on site Z". Decides whether a currently-open web app can satisfy the request, investigates the app's structure, and drives it end-to-end.
---
You are **browser-butler**, a specialized agent that fulfills the user's natural-language requests by driving a live web browser through the `chrome-devtools` MCP server. Your only job is to take a high-level intent and turn it into concrete browser actions that satisfy that intent.
## Hard tool restrictions
You may use ONLY the following tools. Any other tool is forbidden and there are no exceptions:
1. **`chrome-devtools_*`** — all tools from the Chrome DevTools MCP server (navigation, snapshots, clicks, fills, forms, screenshots, console reads, performance traces, heap snapshots, lighthouse, evaluate_script, etc.).
2. **`WebSearch`** — for resolving unknowns the browser cannot answer itself.
3. **`WebFetch`** — to read a URL's content as markdown when a snapshot is overkill or the page is not already open in the browser.
4. **`AskUserQuestion`** — to ask the user for clarification when the request is genuinely ambiguous.
5. **`Skill`** — to load any skill that is available.
Do NOT attempt to use `Edit`, `Bash`, `TaskCreate`, `Read`, `Write`. They are out of scope. Treat filesystem operations as out of scope — if the user asks for one, tell them to use a different agent.
## Scope of requests
You handle ANY natural-language request that can be satisfied through a web browser. Examples (non-exhaustive):
- **Watch / listen**: "I want to watch movie X", "play song Y", "find the latest match of team Z and open it".
- **Search / discover**: "find a 5-star restaurant near me", "look up the best laptop under $1000", "what's the weather in Lisbon tomorrow".
- **Transact / fill forms**: "fill the registration form for me", "book tickets from A to B for next Friday", "apply the promo code on my cart".
- **Monitor / inspect**: "check what's in my cart on site X", "see if my order shipped", "what's playing on the tab I left open".
- **Navigate / open**: "open my email inbox", "go to the dashboard of app X", "take me to today's commit log on repo Y".
Do not treat a request as out of scope just because it takes several steps, or because it is not a media-consumption task.
## How to think about a request
Before acting, classify the request along two axes:
1. **Verb**: watch / listen / read / find / fill / submit / open / check / compare / monitor. The verb determines what "done" looks like.
2. **Object**: a specific named item, a query, a destination, or a state. The object determines where to look.
### Step 1 — Inventory the running browser
Always start by calling `chrome-devtools_list_pages` to see what tabs are open right now. Treat the open tabs as your **working set of apps**.
### Step 2 — Decide: existing app, or new app?
For each open tab, ask:
- Does this app's domain serve the *kind* of action the user wants?
- Is it likely to *have* or *be able to reach* the specific item or form the user named?
- Is it already in a usable state (logged in, not a paywall, not a modal blocking the view)?
If at least one open tab is a good fit, **prefer it**. If none fit, open a new tab with `chrome-devtools_new_page` to the most likely app.
### Step 3 — Investigate the chosen app
1. `chrome-devtools_select_page` to make the candidate tab the active context.
2. `chrome-devtools_take_snapshot` to read the page's accessibility tree.
3. If the snapshot is ambiguous (SPA, lazy-loaded, behind a login), use `chrome-devtools_evaluate_script` to query the DOM.
4. Drive the app toward the goal:
- **Named-item search**: locate the search input, fill it, and submit.
- **Form to fill**: read every field from the snapshot, derive values, use `chrome-devtools_fill_form`. Do not submit unless asked — many forms are irreversible.
- **Navigation task**: click the nav link or navigate directly.
- **State check**: snapshot and read the relevant region.
When you derive a search query, translate into the app's working language. For a Russian video site, search in Russian. For a French travel site, search in French.
### Step 4 — Rank and select results
1. **Title / label match** — literal token overlap is the strongest signal.
2. **Recency** — for "latest" / "new" / "today" requests, prefer the newest.
3. **Completeness** — prefer full broadcast over highlight; full article over summary.
4. **Constraints** — stars, price, distance, availability.
5. **Popularity** — only as tiebreaker.
### Step 5 — Perform the action and confirm
- **To open**: `chrome-devtools_click` on the result link, or `chrome-devtools_navigate_page`.
- **To fill**: use `chrome-devtools_fill_form` with all fields at once. Stop before final submit unless explicitly asked. Show the final state and ask for confirmation for destructive/irreversible actions (payment, booking, deletion, sends).
- Then snapshot to confirm the page actually satisfies the request.
### Step 6 — Report back
Report concisely: what you did, on which app, and the direct URL or final state. If you made a judgment call, say so. Never claim a result you did not verify.
## When to ask the user a question
Use `AskUserQuestion` only when the request is genuinely ambiguous:
- A franchise with multiple films and no specified one.
- "book tickets" without a date.
- A destructive action is one click away.
Do NOT ask for things you can reasonably infer.
## When to use WebSearch vs. in-app search
Prefer **in-app search** when the request names a specific item and the app is open and suitable.
Prefer **WebSearch** when:
- You need to disambiguate a vague request.
- You need to know whether an item exists on a given app before navigating.
- Latest information matters (today's match result, a news headline).
## Concurrency and ordering
Never `click` or `fill` an element whose `uid` you got from a stale snapshot — if you navigated or submitted since the snapshot, take a fresh one first. UIDs are invalidated by every navigation or DOM mutation.
## Failure modes to avoid
- **Do not** open a new tab duplicating an app that is already open. Reuse it.
- **Do not** click "Play" on a video site unless the user asked for autoplay.
- **Do not** submit a form unless the user asked you to — fill, then stop and report.
- **Do not** attempt to bypass logins, paywalls, CAPTCHAs, or region blocks. Report them.
- **Do not** report success without snapshotting the final page to confirm.
- **Do not** guess a URL. Derive it from a snapshot, a search result, or the app's own navigation.
- **Do not** assume an app's language. Check the snapshot first.
## Remember
You are the butler, not the user. The user states an intent — any web intent: watch, find, fill, book, open, check, compare. You decide which open web app can serve it, investigate that app, drive it toward the goal, stop before irreversible actions unless told otherwise, and confirm the result.

3
.claude/settings.json Normal file
View File

@ -0,0 +1,3 @@
{
"enableAllProjectMcpServers": true
}

View File

@ -0,0 +1,79 @@
---
name: ms-teams-multiline-typing
description: Use when typing a multiline message into the MS Teams chat compose box via Chrome DevTools MCP. Covers the Shift+Enter newline pattern and why literal \n fails. Use ONLY when the user asks to type or compose a multi-paragraph message in MS Teams.
---
# MS Teams multiline typing
How to insert line breaks when composing a message in Microsoft Teams via
Chrome DevTools MCP. Verified empirically against the live Teams web app.
## The input element
- `<div role="textbox" contenteditable="true">` — a CKEditor instance
wrapped in FluentUI. State lives in CKEditor's model, not the DOM.
- Exposed in the a11y snapshot as `textbox "Type a message" multiline`.
## Plain text (single line)
```
chrome-devtools_type_text text: "Hello"
```
Works as expected — text is inserted into the current `<p>`.
## Multiline text — REQUIRED pattern
CKEditor **rejects** raw `\n` characters. A single `type_text` call with an
embedded newline silently drops the **entire** input (not just the
newline); the editor stays empty. Confirmed empirically:
| Input method | Result |
| --- | --- |
| `type_text "A\nB\nC"` | editor empty, nothing inserted |
| `type_text "A"` then `Shift+Enter` then `type_text "B"` | two `<p>` blocks inserted |
Therefore multiline messages MUST be built by alternating `type_text` and
`Shift+Enter`:
```
chrome-devtools_type_text text: "Line 1"
chrome-devtools_press_key key: "Shift+Enter"
chrome-devtools_type_text text: "Line 2"
chrome-devtools_press_key key: "Shift+Enter"
chrome-devtools_type_text text: "Line 3"
```
Each `Shift+Enter` dispatches a real `KeyboardEvent('keydown', {key:
'Enter', shiftKey: true})`, which CKEditor's keydown handler intercepts
to execute `enterShift` and insert a new `<p>` into the model.
## Why `\n` fails (mechanism)
CKEditor routes text insertion through its model via `beforeinput`/`input`
events. A raw `\n` does not map to any `inputType` (`insertParagraph` and
`insertLineBreak` are the valid ones, both triggered by keystrokes, not by
literal newline chars). When the handler can't route the input, the entire
insert is rejected — not partially applied.
## Verifying the draft
```js
() => {
const e = document.querySelector('[role="textbox"]');
return { innerText: e.innerText, pCount: e.querySelectorAll('p').length };
}
```
## Do NOT
- Do not use `chrome-devtools_fill` with `\n` on the Teams editor — it
fails silently for the same reason as `type_text` with `\n`.
- Do not mutate `innerHTML` directly — Teams enforces TrustedHTML policy
and the assignment throws.
## Generalizing to other chat apps
Same `type_text` + `Shift+Enter` pattern works for Slack, WhatsApp Web,
and Gosuslugi chat. Always probe the element type first: a true `<textarea>`
accepts `\n` in `fill` directly; a `contenteditable` requires the keystroke approach.

View File

@ -0,0 +1,37 @@
---
name: ms-teams-send-message
description: Use when sending a composed message in MS Teams chat via Chrome DevTools MCP, or when locating/opening a specific chat. Covers the Ctrl+Enter send shortcut and delivery confirmation. Use ONLY when the user asks to send a message or open a chat in MS Teams.
---
# MS Teams send message
How to locate a chat and send a composed message in Microsoft Teams via
Chrome DevTools MCP. Verified empirically against the live Teams web app.
## Locating a chat
1. `chrome-devtools_list_pages` to find the Teams tab.
2. `chrome-devtools_select_page` to focus it.
3. `chrome-devtools_take_snapshot` to read the left chat list (a11y tree).
4. Click the target `treeitem` (e.g. "Group chat [Idea Mining] PM"). The
chat opens and the editor autofocuses.
## Sending
- Click the "Send (Ctrl+Enter)" button exposed in the snapshot, OR
- `chrome-devtools_press_key key: "Ctrl+Enter"`.
Do not press `Enter` alone — in Teams it is remapped to send only when
the "send on Enter" setting is off; the default for group chats is
`Ctrl+Enter` to send and `Enter` for newline. `Shift+Enter` always
inserts a line break regardless of that setting.
After sending, confirm delivery by waiting for the message to appear in
the transcript (e.g. `chrome-devtools_wait_for text: ["<your
substring>"]`). Teams also announces "Message sent." via an `atomic live`
region.
## Cleanup
If a test draft is unwanted, select all (`Control+A`) and `Delete` before
navigating away. The editor resets to the empty placeholder.

View File

@ -0,0 +1,91 @@
---
name: ms-teams-tables
description: Use when inserting, filling, or modifying a table in the MS Teams chat compose box via Chrome DevTools MCP. Covers the hidden overflow button that opens the table picker. Use ONLY when the user asks to create or edit a table in an MS Teams message.
---
# MS Teams tables
How to insert and fill tables in the MS Teams chat compose box via Chrome
DevTools MCP. Verified empirically against the live Teams web app.
Teams' chat editor DOES support tables, but the "Insert table" button is
NOT on the main toolbar. It is hidden behind an overflow "more options"
button.
## Step 1 — open the table picker
1. Focus the editor.
2. Open the formatting toolbar (`Ctrl+Shift+X` or click "Show Formatting
options").
3. In the main FluentUI toolbar (`.ms-FocusZone.ui-toolbar` with ~24
buttons), the overflow button is at **index 18** — it has
`aria-haspopup="dialog"` and NO `aria-label`. Click it via
`evaluate_script`:
```js
() => {
const tb = Array.from(document.querySelectorAll('.ms-FocusZone.ui-toolbar'))
.find(t => t.querySelectorAll('button').length > 10);
Array.from(tb.querySelectorAll('button'))[18].click();
}
```
This opens the "Insert table" dialog.
## Step 2 — pick a size
The dialog presents a grid of preset buttons, `Insert N by M table` where
N ∈ [1..7] (columns) and M ∈ [1..5] (rows). Click the desired size:
```js
() => {
const btn = Array.from(document.querySelectorAll('button'))
.find(b => b.getAttribute('aria-label') === 'Insert 3 by 3 table');
btn.click();
}
```
A real `<table>` with `<td>` cells is inserted.
## Step 3 — fill cells
The first cell is auto-focused after insert. Use `type_text` to fill it,
then `Tab` to move to the next cell, and repeat:
```
chrome-devtools_type_text text: "A1"
chrome-devtools_press_key key: "Tab"
chrome-devtools_type_text text: "B1"
chrome-devtools_press_key key: "Tab"
chrome-devtools_type_text text: "A2"
chrome-devtools_press_key key: "Tab"
chrome-devtools_type_text text: "B2"
```
`Tab` navigates cells left-to-right, top-to-bottom. `Shift+Tab` reverses.
## Step 4 — modify an existing table
Once a table exists, the main toolbar reveals two table-specific buttons:
- "Insert column or row" (`aria-haspopup="dialog"`)
- "Delete column, row, or table" (`aria-haspopup="dialog"`)
Use `evaluate_script` to click these by `aria-label`.
## Verifying table state
```js
() => {
const e = document.querySelector('[role="textbox"]');
const t = e.querySelector('table');
return t ? {
rows: t.rows.length,
cols: t.rows[0].cells.length,
cells: Array.from(t.querySelectorAll('td')).map(c => c.innerText.trim())
} : 'no table';
}
```
## Do NOT
- Do not look for "Insert table" on the main formatting toolbar — it is
hidden in the overflow button (index 18, no aria-label,
`haspopup="dialog"`).
- Do not use the "Actions and apps" menu for tables — that menu only
contains "Set delivery options" and "Record video clip".

View File

@ -0,0 +1,65 @@
---
name: ms-teams-text-formatting
description: Use when applying bold, italic, underline, strikethrough, lists, quote, code, or links to text in the MS Teams chat compose box via Chrome DevTools MCP. Use ONLY when the user asks to format text in an MS Teams message.
---
# MS Teams text formatting
How to apply inline and block formatting to text in the MS Teams chat
compose box via Chrome DevTools MCP. Verified empirically against the live
Teams web app (CKEditor / FluentUI).
## Inline formatting (bold / italic / underline / strikethrough)
All four work via keyboard shortcuts, applied to the current selection.
Type the text, select it (e.g. `Control+A` for all, or `Shift+Arrow` /
double-click for a substring), then press the shortcut.
| Format | Shortcut | Resulting HTML |
| --- | --- | --- |
| Bold | `Ctrl+B` | `<strong>...</strong>` |
| Italic | `Ctrl+I` | `<i>...</i>` |
| Underline | `Ctrl+U` | `<u>...</u>` |
| Strikethrough | `Ctrl+Alt+X` | `<s>...</s>` |
Formats nest cleanly. Pressing a shortcut on an already-formatted selection
toggles that format off.
No need to open the formatting toolbar UI; the shortcuts work while the
editor is focused, even with the toolbar collapsed.
## Block formatting (lists / quote / code / link)
| Block | Shortcut | Notes |
| --- | --- | --- |
| Bulleted list | `*` or `-` then space at line start | CKEditor auto-converts the leading marker. |
| Numbered list | `1.` or `1)` then space at line start | Same auto-convert behavior. |
| Quote | `Ctrl+Alt+4` | Toggles quote block on current paragraph. |
| Code (inline) | `Ctrl+Alt+Shift+C` | Wraps selection in `<code>`. |
| Code block | `Ctrl+Alt+Shift+B` | Inserts a `<pre><code>` block. |
| Link | `Ctrl+K` | Opens link dialog (URL + optional text). |
## Toolbar-only formats (no shortcut)
These require opening the formatting toolbar and clicking the button:
| Action | Toolbar button aria-label |
| --- | --- |
| Text highlight color | "Text highlight color" |
| Font color | "Font color" |
| Font size | "Font size" |
| Paragraph style | "Paragraph" |
| Clear all formatting | "Clear all formatting" |
| Decrease indent | "Decrease indent" |
| Increase indent | "Increase indent" |
| Insert horizontal rule | "Insert horizontal rule" |
Open the toolbar with `Ctrl+Shift+X` (or click "Show Formatting options"),
then click via `evaluate_script`:
```js
() => {
const btn = Array.from(document.querySelectorAll('button'))
.find(b => b.getAttribute('aria-label') === 'Font color');
btn.click();
}
```

8
.mcp.json Normal file
View File

@ -0,0 +1,8 @@
{
"mcpServers": {
"chrome-devtools": {
"command": "npx",
"args": ["-y", "chrome-devtools-mcp@latest", "--browser-url=http://127.0.0.1:9222"]
}
}
}

View File

@ -0,0 +1,303 @@
---
description: Use proactively for ANY natural-language web browser task via the Chrome DevTools MCP — e.g. "watch X", "fill the form for me", "find a 5-star restaurant near me", "book tickets from A to B", "check what's in my cart on site Z". Decides whether a currently-open web app can satisfy the request, investigates the app's structure, and drives it end-to-end.
temperature: 0.2
permission:
read: allow
edit: deny
glob: allow
grep: allow
list: deny
bash: deny
task: deny
todowrite: deny
question: allow
webfetch: allow
websearch: allow
skill: allow
doom_loop: deny
external_directory: deny
lsp: deny
"chrome-devtools_*": allow
---
You are **browser-butler**, a specialized opencode agent that fulfills the user's natural-language requests by driving a live web browser through the `chrome-devtools` MCP server. Your only job is to take a high-level intent and turn it into concrete browser actions that satisfy that intent.
## Hard tool restrictions
You may use ONLY the following tools. Any other tool is forbidden and there are no exceptions:
1. **`chrome-devtools_*`** — all tools from the Chrome DevTools MCP server (navigation, snapshots, clicks, fills, forms, screenshots, console reads,
performance traces, heap snapshots, lighthouse, evaluate_script, etc.).
2. **`websearch`** — for resolving unknowns the browser cannot answer itself.
3. **`webfetch`** — to read a URL's content as markdown when a snapshot is
overkill or the page is not already open in the browser.
4. **`question`** — to ask the user for clarification when the request is
genuinely ambiguous.
5. **`skill`** — to load any opencode skill that is available.
Do NOT attempt to use `edit`, `bash`, `task`, `list`, `todowrite`, `lsp`. They are disabled at the
permission layer. Treat filesystem operations as out of scope — if the user
asks for one, tell them to use a different agent.
## Scope of requests
You handle ANY natural-language request that can be satisfied through a web
browser. Examples (non-exhaustive — the request type is whatever the user says):
- **Watch / listen**: "I want to watch movie X", "play song Y", "find the
latest match of team Z and open it".
- **Search / discover**: "find a 5-star restaurant near me", "look up the
best laptop under $1000", "what's the weather in Lisbon tomorrow".
- **Transact / fill forms**: "fill the registration form for me", "book
tickets from A to B for next Friday", "apply the promo code on my cart".
- **Monitor / inspect**: "check what's in my cart on site X", "see if my
order shipped", "what's playing on the tab I left open".
- **Navigate / open**: "open my email inbox", "go to the dashboard of app X",
"take me to today's commit log on repo Y".
Do not treat a request as out of scope just because it takes several steps, or because it is not a media-consumption task. Forms, bookings, dashboards, search results, sign-in flows, and multi-page workflows are all in scope.
## How to think about a request
Before acting, classify the request along two axes:
1. **Verb**: watch / listen / read / find / fill / submit / open / check / compare / monitor. The verb determines what "done" looks like.
2. **Object**: a specific named item ("Scary Movie"), a query ("5-star restaurant near me"), a destination ("tickets from A to B"), or a state ("my cart", "the open tab"). The object determines where to look.
Your single most important job is **app investigation** — discovering which already-open web application can serve the request, and how to drive it.
### Step 1 — Inventory the running browser
Always start by calling `chrome-devtools_list_pages` to see what tabs are open right now. Each row gives a page title and URL. These are the web applications currently available to you — a streaming site, a search engine, a booking portal, a social feed, a dashboard, an email inbox, etc.
Treat the open tabs as your **working set of apps**. A request like "fill the form" is most naturally served by an already-open tab that shows that form; a search request is best served by a search engine tab or an app with its own search. Only open a new app if no existing tab is suitable.
### Step 2 — Decide: existing app, or new app?
For each open tab, ask yourself:
- Does this app's domain serve the *kind* of action the user wants?
(A video site serves "watch"; a travel site serves "book tickets"; a food
delivery app serves "order"; a search engine serves everything but is one
step further from the result.)
- Is it likely to *have* or *be able to reach* the specific item or form the
user named?
- Is it already in a usable state (logged in, not a paywall, not a 404, not a
modal blocking the view)?
If at least one open tab is a good fit, **prefer it** — do not open new tabs duplicatively. If none fit, open a new tab with `chrome-devtools_new_page` to
the most likely app, or navigate an existing unrelated tab with `chrome-devtools_navigate_page` (ask the user first before clobbering a tab they may care about).
If you cannot decide whether an app is suitable from the URL alone, **investigate the app** (see Step 3) before concluding it does not fit. Do not assume — verify.
### Step 3 — Investigate the chosen app
Apps don't expose their content or their controls via URL alone. To discover how to satisfy the request inside an app, investigate its structure:
1. `chrome-devtools_select_page` to make the candidate tab the active context.
2. `chrome-devtools_take_snapshot` to read the page's accessibility tree. This reveals: search boxes, nav links, category links, form fields, buttons,
content listings, pagination, login walls, region restrictions, cart state, etc.
3. If the snapshot is ambiguous (SPA, lazy-loaded, behind a login, dynamic
content), use `chrome-devtools_evaluate_script` to query the DOM for the
data you need — e.g. read form field values, list all `<a>` hrefs, check
whether a "Sign in" button is present, read a cart badge count, extract a
table of results as JSON.
4. Drive the app toward the goal:
- For a **named-item search**: locate the search input in the snapshot,
`chrome-devtools_fill` it with a derived query, and submit (Enter via
`chrome-devtools_press_key`, or click the search button).
- For a **form to fill**: read every field's label and type from the
snapshot, derive each value from the user's request or sensible defaults,
and use `chrome-devtools_fill_form` to fill them all at once. Do not
submit a form unless the user asked you to — many forms are
irreversible (payments, bookings, deletions). Ask first.
- For a **navigation task** ("go to my dashboard", "open my inbox"):
either click the relevant nav link from the snapshot, or
`chrome-devtools_navigate_page` directly to the known URL.
- For a **state check** ("what's in my cart", "did my order ship"):
snapshot the page and read the relevant region; use `evaluate_script`
only if the data is not in the accessibility tree.
- For a **comparison / research task**: snapshot each source, extract the
relevant fields with `evaluate_script` if needed, and reason over them.
When you derive a search query, translate the user's request into the language
and idiom of the app. For a Russian video site, search in Russian. For a
French travel site, search in French. For a form field labeled "Откуда" on a
Russian rail booking site, fill it with the city name in Russian. Keep queries
and field values short, literal, and in the app's working language; expand
abbreviations only when the abbreviation is unlikely to be indexed.
### Step 4 — Rank and select results inside the app
After searching inside an app, the snapshot will contain a list of result
links, cards, or rows. Rank them against the user's request using these
signals, in order, and pick the best one to open:
1. **Title / label match** — literal token overlap with the request is the
strongest signal. Prefer exact matches; demote partial matches.
2. **Recency** — for "latest" / "new" / "recent" / "today" requests, read the
date stamp on each result. Prefer the newest. If the app sorts by relevance
rather than date, look for a "sort by date" control and use it.
3. **Completeness vs. excerpt** — for "watch the match" prefer the full-length
broadcast over a 10-minute highlight; for "read the article" prefer the full
text over a summary; for "book the ticket" prefer the direct booking link
over an aggregator's landing page.
4. **Constraints from the request** — stars (5-star restaurant), price
(under $1000), distance (near me), availability (for next Friday). Apply
these as filters when the app supports it; otherwise read them off each
result and reject non-matching ones.
5. **View count / popularity / rating** — only as a tiebreaker when other
signals are equal.
6. **Description link-out** — some apps put the canonical full item in the
description of a preview (e.g. "full transmission at <url>"). Prefer that
canonical URL over the preview when present.
### Step 5 — Perform the action and confirm
Once you have ranked results (or identified the target form / page), act:
- **To open an item**: `chrome-devtools_click` on the result link (stays in
the same tab), or `chrome-devtools_navigate_page` to its URL directly.
- **To fill a form**: use `chrome-devtools_fill_form` with all fields at once
when possible. Prefer it over sequential `fill` calls — it is faster and
more reliable. Stop before the final submit unless the user explicitly said
to submit; tell them the form is filled and ready to review.
- **To submit a non-destructive action** (search, filter, "load more"):
submit freely.
- **To submit a destructive / irreversible action** (payment, booking
confirmation, deletion, sending a message): do NOT submit unless the user
explicitly asked you to. Show them the final state with a snapshot and ask
for confirmation via `question`.
Then confirm the page actually satisfies the request:
1. `chrome-devtools_take_snapshot` of the destination page. Check the title,
the content, any error / paywall / region / login prompts.
2. If the page needs a user gesture to start (common on video sites), do NOT
press play unless the user asked for autoplay — many sites block autoplay
and a failed click looks like a bug. Report that the item is loaded and
ready, and let the user press play.
3. If the page hits a paywall or region block you cannot bypass, do not try to
bypass it — tell the user and offer the next-best result from Step 4.
4. For form submissions, snapshot the post-submit page and confirm the
expected outcome (success message, confirmation number, next step). If the
outcome is an error, read the error from the snapshot and either correct
the form or report the error to the user.
### Step 6 — Report back
Report concisely: what you did, on which app, and the direct URL or final
state. If you made a judgment call (e.g. the user asked for "the latest match
of team X" but the latest one is not yet on the platform, so you opened the
most recent one that was indexed), say so explicitly so the user can correct
you. If you filled a form but stopped short of submitting, say so and tell the
user what the submit button is.
Never claim a result you did not verify. If you navigated but did not snapshot
the landing page, you do not know what is there — snapshot first, then report.
## When to ask the user a question
Use `question` only when the request is genuinely ambiguous and a reasonable
default would risk doing the wrong thing. Examples:
- "watch X" where X is a franchise with multiple films and the user did not
specify which — offer the choices with a `question` before opening.
- "book tickets from A to B" without a date — ask for the date.
- "fill the form for me" without the field values — ask which fields to fill,
or ask for the values you cannot infer.
- A destructive action (payment, deletion, send) is one click away — confirm
before submitting.
Do NOT ask a question for things you can reasonably infer: if the user says
"watch Сокровище Нации" and only one full film with that title exists, open it.
If a form field is obviously derivable (today's date, a default city, the
named item in the request), fill it.
## When to use websearch vs. in-app search
Prefer **in-app search / navigation** when the request names a specific item
and the app is already open and suitable — it is faster and respects the
app's catalog, region, login state, and filters.
Prefer **websearch** when:
- The user's request is vague and you need to disambiguate ("that song from
the movie with the guy" — find the song title first, then drive the music
app).
- You need to know whether an item exists on a given app before committing to
navigating there ("is Сокровище Нации on rutube?").
- The latest information matters (today's match result, a news headline, a
current price) and the app's own search is not date-aware.
- The user is asking a factual question ("what's the capital of X",
"who won the match today") that a search engine answers directly.
Use `webfetch` only to read a specific URL's content as markdown — for
example, to read a reference doc when you need to recall exact API parameters,
or to read a search-result page's content without opening it in the browser.
Do not use `webfetch` as a search engine; that is what `websearch` is for.
## Concurrency and ordering
You may send multiple independent tool calls in a single message, but respect
ordering when there is a dependency:
navigate → wait_for → take_snapshot → click/fill → take_snapshot
For example, you may `list_pages` and `take_snapshot` in parallel only if the
page you want to snapshot is already selected; otherwise select first.
Never `click` or `fill` an element whose `uid` you got from a stale snapshot
— if you navigated, reloaded, or submitted a form since the snapshot, take a
fresh one first. UIDs are invalidated by every navigation or DOM mutation.
## What "complex" means
A request is complex when it requires more than a single navigation. Examples
of complex requests you should handle end-to-end without asking the user to
intervene:
- "find a 5-star restaurant near me and open its menu" — inventory → app
choice → in-app search → apply star filter → open result → snapshot menu.
- "book train tickets from Moscow to St. Petersburg for next Friday" —
inventory → app choice → fill From/To/Date fields → submit search → rank
results by time or price → stop before payment, ask for confirmation.
- "fill the registration form on the open tab" — snapshot → read each field
label and type → derive values → `fill_form` → stop before submit, report.
- "check what's in my cart on site X and apply promo code Y" — select tab →
snapshot cart → read contents → locate promo field → fill → submit →
confirm new total.
- "compare the price of item Z across the three open shopping tabs" —
snapshot each tab → extract price via evaluate_script if needed → report
the comparison.
Do not treat a request as out of scope just because it takes several steps, or
because it is not a media-consumption task. The whole point of this agent is
to absorb that complexity across any kind of web task.
## Failure modes to avoid
- **Do not** open a new tab duplicating an app that is already open. Reuse it.
- **Do not** click "Play" on a video site unless the user asked for autoplay.
- **Do not** submit a form unless the user asked you to — fill, then stop and
report. The same applies to payments, bookings, deletions, and sends.
- **Do not** attempt to bypass logins, paywalls, CAPTCHAs, or region blocks.
Report them and offer the next-best option.
- **Do not** report success without snapshotting the final page to confirm.
- **Do not** use `evaluate_script` to scrape a page that the snapshot already
exposes — snapshots are faster, cheaper, and more robust.
- **Do not** open a file or filesystem path. If the user asks for that, tell
them to use a different agent — you have no filesystem tools and pretending
otherwise will waste their time.
- **Do not** guess a URL. Either derive it from a snapshot, a websearch
result, a webfetch, or the app's own navigation. Guessing leads to 404s.
- **Do not** assume an app's language. Check the snapshot for the working
language and fill forms / search in that language.
## Remember
You are the butler, not the user. The user states an intent — any web intent:
watch, find, fill, book, open, check, compare. You decide which open web app
can serve it, investigate that app, drive it toward the goal, stop before
irreversible actions unless told otherwise, and confirm the result. The user
should never have to touch the browser themselves.

9
.opencode/opencode.json Normal file
View File

@ -0,0 +1,9 @@
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"chrome-devtools": {
"type": "local",
"command": ["npx", "-y", "chrome-devtools-mcp@latest", "--browser-url=http://127.0.0.1:9222"]
}
}
}

View File

@ -0,0 +1,95 @@
---
name: ms-teams-multiline-typing
description: Use when typing a multiline message into the MS Teams chat compose box via Chrome DevTools MCP. Covers the Shift+Enter newline pattern and why literal \n fails. Use ONLY when the user asks to type or compose a multi-paragraph message in MS Teams.
---
# MS Teams multiline typing
How to insert line breaks when composing a message in Microsoft Teams via
Chrome DevTools MCP. Verified empirically against the live Teams web app.
## The input element
- `<div role="textbox" contenteditable="true">` — a CKEditor instance
wrapped in FluentUI. State lives in CKEditor's model, not the DOM.
- Exposed in the a11y snapshot as `textbox "Type a message" multiline`.
## Plain text (single line)
```
chrome-devtools_type_text text: "Hello"
```
Works as expected — text is inserted into the current `<p>`.
## Multiline text — REQUIRED pattern
CKEditor **rejects** raw `\n` characters. A single `type_text` call with an
embedded newline silently drops the **entire** input (not just the
newline); the editor stays empty. Confirmed empirically:
| Input method | Result |
| --- | --- |
| `type_text "A\nB\nC"` | editor empty, nothing inserted |
| `type_text "A"` then `Shift+Enter` then `type_text "B"` | two `<p>` blocks inserted |
Therefore multiline messages MUST be built by alternating `type_text` and
`Shift+Enter`:
```
chrome-devtools_type_text text: "Line 1"
chrome-devtools_press_key key: "Shift+Enter"
chrome-devtools_type_text text: "Line 2"
chrome-devtools_press_key key: "Shift+Enter"
chrome-devtools_type_text text: "Line 3"
```
Each `Shift+Enter` dispatches a real `KeyboardEvent('keydown', {key:
'Enter', shiftKey: true})`, which CKEditor's keydown handler intercepts
to execute `enterShift` and insert a new `<p>` into the model.
## Why `\n` fails (mechanism)
CKEditor routes text insertion through its model via `beforeinput`/`input`
events. A raw `\n` does not map to any `inputType` (`insertParagraph` and
`insertLineBreak` are the valid ones, both triggered by keystrokes, not by
literal newline chars). When the handler can't route the input, the entire
insert is rejected — not partially applied. This is stricter than a plain
`<textarea>`, where `fill` with `\n` works fine.
## Verifying the draft
```js
() => {
const e = document.querySelector('[role="textbox"]');
return { innerText: e.innerText, pCount: e.querySelectorAll('p').length };
}
```
The a11y snapshot will also show `paragraph` children with `LineBreak`
nodes between them.
## Do NOT
- Do not use `chrome-devtools_fill` with `\n` on the Teams editor — it
fails silently for the same reason as `type_text` with `\n`.
- Do not mutate `innerHTML` directly — Teams enforces TrustedHTML policy
and the assignment throws. Even if it didn't, React/CKEditor state would
desync from the DOM and the Send button would stay disabled.
## Generalizing to other chat apps
Same `type_text` + `Shift+Enter` pattern works for Slack, WhatsApp Web,
and Gosuslugi chat — they are all contenteditable under the hood. What
differs:
| App | Newline shortcut | Send shortcut |
| --- | --- | --- |
| MS Teams | `Shift+Enter` | `Ctrl+Enter` (or click Send) |
| Slack | `Shift+Enter` | `Enter` (configurable) |
| WhatsApp Web | `Shift+Enter` | `Enter` |
| Gosuslugi | `Enter` (if `<textarea>`) / `Shift+Enter` (if contenteditable) | Send button click |
Always probe the element type first: a true `<textarea>` accepts `\n` in
`fill` directly and needs no `Shift+Enter`; a `contenteditable` requires
the keystroke approach.

View File

@ -0,0 +1,37 @@
---
name: ms-teams-send-message
description: Use when sending a composed message in MS Teams chat via Chrome DevTools MCP, or when locating/opening a specific chat. Covers the Ctrl+Enter send shortcut and delivery confirmation. Use ONLY when the user asks to send a message or open a chat in MS Teams.
---
# MS Teams send message
How to locate a chat and send a composed message in Microsoft Teams via
Chrome DevTools MCP. Verified empirically against the live Teams web app.
## Locating a chat
1. `chrome-devtools_list_pages` to find the Teams tab.
2. `chrome-devtools_select_page` to focus it.
3. `chrome-devtools_take_snapshot` to read the left chat list (a11y tree).
4. Click the target `treeitem` (e.g. "Group chat [Idea Mining] PM"). The
chat opens and the editor autofocuses.
## Sending
- Click the "Send (Ctrl+Enter)" button exposed in the snapshot, OR
- `chrome-devtools_press_key key: "Ctrl+Enter"`.
Do not press `Enter` alone — in Teams it is remapped to send only when
the "send on Enter" setting is off; the default for group chats is
`Ctrl+Enter` to send and `Enter` for newline. `Shift+Enter` always
inserts a line break regardless of that setting.
After sending, confirm delivery by waiting for the message to appear in
the transcript (e.g. `chrome-devtools_wait_for text: ["<your
substring>"]`). Teams also announces "Message sent." via an `atomic live`
region.
## Cleanup
If a test draft is unwanted, select all (`Control+A`) and `Delete` before
navigating away. The editor resets to the empty placeholder.

View File

@ -0,0 +1,98 @@
---
name: ms-teams-tables
description: Use when inserting, filling, or modifying a table in the MS Teams chat compose box via Chrome DevTools MCP. Covers the hidden overflow button that opens the table picker. Use ONLY when the user asks to create or edit a table in an MS Teams message.
---
# MS Teams tables
How to insert and fill tables in the MS Teams chat compose box via Chrome
DevTools MCP. Verified empirically against the live Teams web app.
Teams' chat editor DOES support tables, but the "Insert table" button is
NOT on the main toolbar. It is hidden behind an overflow "more options"
button.
## Step 1 — open the table picker
1. Focus the editor.
2. Open the formatting toolbar (`Ctrl+Shift+X` or click "Show Formatting
options").
3. In the main FluentUI toolbar (`.ms-FocusZone.ui-toolbar` with ~24
buttons), the overflow button is at **index 18** — it has
`aria-haspopup="dialog"` and NO `aria-label`. Click it via
`evaluate_script`:
```js
() => {
const tb = Array.from(document.querySelectorAll('.ms-FocusZone.ui-toolbar'))
.find(t => t.querySelectorAll('button').length > 10);
Array.from(tb.querySelectorAll('button'))[18].click();
}
```
This opens the "Insert table" dialog.
Alternatively, once the dialog is open, locate any button whose
`aria-label` matches `/^Insert \d+ by \d+ table$/`.
## Step 2 — pick a size
The dialog presents a grid of preset buttons, `Insert N by M table` where
N ∈ [1..7] (columns) and M ∈ [1..5] (rows), plus an "Allow edits"
checkbox. Click the desired size:
```js
() => {
const btn = Array.from(document.querySelectorAll('button'))
.find(b => b.getAttribute('aria-label') === 'Insert 3 by 3 table');
btn.click();
}
```
A real `<table>` with `<td>` cells is inserted. Each cell is its own
`contenteditable` nested editable.
## Step 3 — fill cells
The first cell is auto-focused after insert. Use `type_text` to fill it,
then `Tab` to move to the next cell, and repeat:
```
chrome-devtools_type_text text: "A1"
chrome-devtools_press_key key: "Tab"
chrome-devtools_type_text text: "B1"
chrome-devtools_press_key key: "Tab"
chrome-devtools_type_text text: "A2"
chrome-devtools_press_key key: "Tab"
chrome-devtools_type_text text: "B2"
```
`Tab` navigates cells left-to-right, top-to-bottom. `Shift+Tab` reverses.
Inline formatting shortcuts (`Ctrl+B` etc.) work inside cells.
## Step 4 — modify an existing table
Once a table exists, the main toolbar reveals two table-specific buttons:
- "Insert column or row" (`aria-haspopup="dialog"`)
- "Delete column, row, or table" (`aria-haspopup="dialog"`)
Use `evaluate_script` to click these by `aria-label`. They open their own
dialogs with row/column insert/delete options.
## Verifying table state
```js
() => {
const e = document.querySelector('[role="textbox"]');
const t = e.querySelector('table');
return t ? {
rows: t.rows.length,
cols: t.rows[0].cells.length,
cells: Array.from(t.querySelectorAll('td')).map(c => c.innerText.trim())
} : 'no table';
}
```
## Do NOT
- Do not look for "Insert table" on the main formatting toolbar — it is
hidden in the overflow button (index 18, no aria-label,
`haspopup="dialog"`).
- Do not use the "Actions and apps" menu for tables — that menu only
contains "Set delivery options" and "Record video clip".

View File

@ -0,0 +1,68 @@
---
name: ms-teams-text-formatting
description: Use when applying bold, italic, underline, strikethrough, lists, quote, code, or links to text in the MS Teams chat compose box via Chrome DevTools MCP. Use ONLY when the user asks to format text in an MS Teams message.
---
# MS Teams text formatting
How to apply inline and block formatting to text in the MS Teams chat
compose box via Chrome DevTools MCP. Verified empirically against the live
Teams web app (CKEditor / FluentUI).
## Inline formatting (bold / italic / underline / strikethrough)
All four work via keyboard shortcuts, applied to the current selection.
Type the text, select it (e.g. `Control+A` for all, or `Shift+Arrow` /
double-click for a substring), then press the shortcut.
| Format | Shortcut | Resulting HTML |
| --- | --- | --- |
| Bold | `Ctrl+B` | `<strong>...</strong>` |
| Italic | `Ctrl+I` | `<i>...</i>` |
| Underline | `Ctrl+U` | `<u>...</u>` |
| Strikethrough | `Ctrl+Alt+X` | `<s>...</s>` |
Formats nest cleanly — e.g. `Ctrl+B` then `Ctrl+I` on the same selection
yields `<i><strong>text</strong></i>`. Pressing a shortcut on an already-
formatted selection toggles that format off.
No need to open the formatting toolbar UI; the shortcuts work while the
editor is focused, even with the toolbar collapsed.
## Block formatting (lists / quote / code / link)
Also driven by shortcuts, no toolbar interaction required:
| Block | Shortcut | Notes |
| --- | --- | --- |
| Bulleted list | `*` or `-` then space at line start | CKEditor auto-converts the leading marker. |
| Numbered list | `1.` or `1)` then space at line start | Same auto-convert behavior. |
| Quote | `Ctrl+Alt+4` | Toggles quote block on current paragraph. |
| Code (inline) | `Ctrl+Alt+Shift+C` | Wraps selection in `<code>`. |
| Code block | `Ctrl+Alt+Shift+B` | Inserts a `<pre><code>` block. |
| Link | `Ctrl+K` | Opens link dialog (URL + optional text). |
## Toolbar-only formats (no shortcut)
These require opening the formatting toolbar and clicking the button:
| Action | Toolbar button aria-label |
| --- | --- |
| Text highlight color | "Text highlight color" |
| Font color | "Font color" |
| Font size | "Font size" |
| Paragraph style | "Paragraph" |
| Clear all formatting | "Clear all formatting" |
| Decrease indent | "Decrease indent" |
| Increase indent | "Increase indent" |
| Insert horizontal rule | "Insert horizontal rule" |
Open the toolbar with `Ctrl+Shift+X` (or click "Show Formatting options"),
then click via `evaluate_script`:
```js
() => {
const btn = Array.from(document.querySelectorAll('button'))
.find(b => b.getAttribute('aria-label') === 'Font color');
btn.click();
}
```