WebMCP на практиці: перетворення дошки завдань на інструменти для AI-агентів
WebMCP на практиці: дошка завдань, перетворена на інструменти для AI-агентів із декларативними та скриптованими інструментами, змінним списком інструментів, мостом MCP та відмовою.
Агенти, які здогадуються щодо вашого веб-сайту
Сьогодні ШІ-агент, який працює з веб-сайтом, переважно здогадується. Він бере скріншот та шукає щось, що нагадує кнопку, або аналізує DOM, сподіваючись, що назви класів означають саме те, що здається. Після кліку він перевіряє, чи змінився сайт у спосіб, який вказує на успіх. Це вражаючий трюк, але він ґрунтується на слабкій основі, адже сайт ніколи нічого не повідомляє агенту, і кожний висновок робиться шляхом інференції.
WebMCP — це пропозиція Chrome щодо припинення цих здогадок. Сторінка оголошує про свої можливості, агент читає це оголошення та викликає функцію зі структурованими аргументами, замість того щоб націлюватися на прямокутник на екрані. Зміни здаються незначними, але вони переміщують інтелект: замість того, щоб відновлювати мету додатку з пікселів та маркапу, агент отримує цю мету безпосередньо від самого додатку.
Щоб перевірити, чи це працює на практиці, була створена невелика панель завдань для роботи, до якої був підключений реальний агент. Найбільш показовим результатом стало те, коли агент категорично відхилив запит.
Три ролі
У кожну мить експерименту бере участь один із трьох учасників.
- Сторінка є постачальником. Вона реєструє інструменти. Кожен інструмент містить назву та опис, спрямований до моделі, JSON Schema, який описує аргументи, та функцію, яка виконує роботу.
- Браузер є реєстром. Для кожної вкладки він зберігає список інструментів, де можливо отримує схеми з HTML та вирішує, коли інструмент з’являється чи зникає.
getTools() та запускає обраний інструмент за допомогою executeTool(). DOM залишається поза його досяжністю, і у нього немає засобів для створення функцій, які сторінка ніколи не оприлюднювала.Останній момент є суттю цієї ідеї. Документація Chrome достатньо чітко описує API, але читання пояснень — це не те саме, що розуміння, тому найшвидшим шляхом було створення найменшої можливої версії.
Демонстрація: панель завдань у трьох файлах
Панель складається з index.html, app.js та style.css. Нічого не потрібно створювати чи встановлювати. Люди можуть додавати завдання, позначати їх як виконані та фільтрувати список — це проект, який можна завершити за один день. Новизною є те, що сторінка також представляє себе для штучного інтелекту як набір інструментів, які можна викликати, через один об’єкт, який надає браузер.
const mc = document.modelContext; // undefined if WebMCP is off
Цей єдиний рядок є повним точкою входу. Коли об’єкт існує, сторінка може реєструвати інструменти. Коли його немає, сторінка є просто дошкою завдань, що є правильним способом її функціонування у цьому випадку.
Щоб обидві частини були видимі, демо-сторінка розміщує їх поруч одна з одною. Зліва знаходиться дошка завдань, тобто постачальник. Праворуч — панель „Уявний агент“, яка виступає у ролі споживача та виконує виклики getTools() та executeTool() так само, як це робив би справжній агент, що уникає додаткових налаштувань. Ви побачите цю панель на кожному з наведених нижче скріншотів.
WebMCP досі має статус „Proposed“ у Chrome Platform Status та за замовчуванням не активований ніде, тому спочатку потрібен відповідний флаг.
Відкрийте chrome://flags/#enable-webmcp-testing, поставте значення на «Enabled» та перезапустіть браузер. Без цього document.modelContext залишається невизначеним. Chrome також пропонує пробну версію WebMCP origin, яка стала доступною з Chrome 149. Цей параметр підходить для локальних експериментів, тоді як пробна версія origin — це шлях, який Chrome рекомендує для більш широких тестувань.
Спочатку завершена петля
Легше зрозуміти API після того, як побачити, що він створює. Claude Code було підключено до плати, і йому було надано одне речення з проханням додати чотири завдання: знаходження інформації та проведення досліджень, складання планів блогу, створення повного блогу з прикладами та його публікація. З’явилися чотири завдання. У другому реченні його попросили позначити завдання 3, 4 та 5 як виконані, і це також спрацювало.
Клод не зробив скріншот дошки та не читав її HTML-коду. Він запитав браузер про доступні інструменти, знайшов add-task та викликав його чотири рази зі структурованими аргументами. Ось той самий момент, показаний зсередини браузера.
Чотири нові завдання тепер знаходяться під номерами від #3 до #6 на дошці, а панель DevTools (Application, потім WebMCP) відображає всі шість інструментів, які наразі використовує сторінка. Один з них, clear-completed, тут не був навіть хвилину раніше, і причина цього незабаром стане зрозумілою. Три завершення завдань записуються окремо для кожного виклику.
Кожен виклик відображається разом із своїми аргументами ({"id":3}, {"id":4} та {"id":5}), а у рядку підсумку зазначено загалом три виклики без жодних помилок, скасувань чи викликів, які все ще тривають. Дебагування WebMCP тут відбувається найшвидше, оскільки панель відображає саме те, що надіслав агент.
Отже, речення, введене в термінал, перетворилося на чотири структуровані функційні виклики всередині вкладки браузера. Наступне питання — що мусила зробити сторінка, щоб цього досягти.
Два способи публікації інструменту
У першому варіанті зовсім не потрібен JavaScript. Існуюча форма отримує чотири додаткові атрибути.
<form id="add-form"
toolname="add-task"
tooldescription="Add a new task to the user's task board."
toolautosubmit>
<input name="title" required maxlength="80"
toolparamdescription="Short description of the task to add.">
<select name="priority"
toolparamdescription="How urgent the task is.">
<option value="low">low</option>
<option value="normal" selected>normal</option>
<option value="high">high</option>
</select>
<button type="submit">Add</button>
</form>
Ця форма — це інструмент add-task, який Claude викликав чотири рази. Назви полів, атрибути required та опції <select> читаються браузером, який самостійно формує JSON Schema. Агент бачить один обов’язковий рядок та енумерацію з трьома значеннями, причому ніхто не вводив схему вручну. У існуючому додатку це, ймовірно, найпростіший експеримент з WebMCP, адже звичайна HTML-форма дає початкову перевагу. Єдине, що додається, — це опис того, що форма означає для агента.
Є одна деталь, яку потрібно трохи часу, щоб зрозуміти: той самий обробник submit обслуговує як користувача, який натискає «Додати», так і агента, який викликає інструмент, причому подія вказує, який з них це був.
$('#add-form').addEventListener('submit', (e) => {
e.preventDefault();
const task = addTask(new FormData(e.target).get('title'), /* ... */);
if (!e.agentInvoked) {
e.target.reset(); // human — clear the box
} else {
e.respondWith?.(Promise.resolve(
text(`Added task #${task.id}: "${task.title}".`)
));
}
});
Прапорець agentInvoked вказує, хто натиснув кнопку, а respondWith() повертає результат агенту без відходу на іншу сторінку. Існує один шлях виконання коду з двома викликачами, що є гарним дизайнерським рішенням, адже ніхто не хоче „версії AI“ для кожної дії в додатку. Та сама логіка має слугувати як людям, так і агентам, коли це має сенс.
Декларативний підхід, можливо, є менш цікавим, проте саме він, ймовірно, приверне увагу, оскільки він гарно демонструється та не вимагає особливих зусиль. Саме там, де форма не може виразити потреби, все стає цікавим. Це другий підхід — registerTool, який дуже схожий на те, що б ви написали для сервера MCP на бекенді.
await mc.registerTool({
name: 'list-tasks',
description: 'List the tasks on the board. Use this before acting so you know the task IDs.',
inputSchema: {
type: 'object',
properties: {
status: { type: 'string', enum: ['all', 'open', 'done'] },
},
},
annotations: { readOnlyHint: true },
async execute({ status = 'all' }) {
const rows = visible(tasks, status);
return text(rows.map((t) => `#${t.id} [${t.done ? 'x' : ' '}] ${t.title}`).join('\n'));
},
});
Уважно подивіться на той description. Він написаний для моделі, а не для розробника, і рядок на кшталт „Використовуйте це перед дією, щоб знати ID завдань“ вказує, коли його слід викликати. Описи тепер є частиною інтерфейсу API, і нечіткі описи призводять до створення агентів, які здогадуються.
Прапорець readOnlyHint сигналізує про те, що інструмент можна безпечно викликати, не запитуючи спочатку користувача. Його мають list-tasks та estimate-task. complete-task та clear-completed його не мають, оскільки вони змінюють щось.
Інструменти також можуть працювати повільно, і агент може змінити свою думку по дорозі. Інструмент estimate-task навмисно чекає три секунди, і як другий аргумент він отримує AbortSignal.
async execute({ id }, { signal }) {
const t = findTask(tasks, id);
if (!t) {
return text(`No task #${id}.`);
}
await sleep(3000, signal); // throws if the agent aborts
return text(
`Task #${t.id} is about ${
t.priority === 'high' ? '2 hours' : '30 minutes'
}.`
);
}
Якщо дотримуватися цього сигналу, процес скасування буде працювати. Якщо його ігнорувати, інструмент продовжуватиме працювати після того, як агент піде.
Список інструментів, який змінюється під час відкриття сторінки
Найсильнішою причиною для реєстрації інструментів у скрипті, на відміну від використання статичного маніфесту, є те, що список може змінюватися під час виконання. У системі є інструмент clear-completed, який видаляє всі завершені завдання. Це має сенс лише тоді, коли щось завершено, що пояснює, чому він з’явився на попередньому скріншоті відразу після того, як три завдання були позначені як завершені. Отже, він існує лише в такі моменти.
let clearCtl = null;
async function syncClearTool() {
const has = tasks.some((t) => t.done);
if (has && !clearCtl) {
const ctl = new AbortController();
clearCtl = ctl;
await mc.registerTool(
{
name: 'clear-completed',
/* ... */
},
{
signal: ctl.signal
}
);
} else if (!has && clearCtl) {
const ctl = clearCtl;
clearCtl = null;
setTimeout(() => ctl.abort(), 0); // abort() IS unregister
}
}
Не існує функції unregisterTool(). Під час реєстрації ви передаєте об’єкт AbortSignal, а скасування цього сигналу видаляє інструмент. Саме тут проявляється ефективність дизайну: сигнал, який скасовує поточний запит, водночас скасовує й відповідну можливість, тобто один механізм виконує дві функції.
Якщо позначити завдання як виконане, у списку агента з’являється опція clear-completed, а якщо зняти позначку, інструмент зникає. Агент дізнається про це через подію toolchange і не потребує періодичного перевіряння.
mc.addEventListener('toolchange', refreshTools);
Отже, список інструментів — це не маніфест, який публікується один раз. Він описує те, що додаток може робити зараз, у своєму поточному стані.
У чому це відрізняється від сервера MCP на бекенді
Далі від Claude Code попросили обмежити список завдань лише завершеними.
Агент викликав функцію filter-tasks з параметром {"status":"done"}; кнопка «Завершено» стала активною, і на списку відобразилися лише завершені завдання. Агент не завантажував фільтровану копію даних — він просто змінив те, що бачив користувач.
Бекенд-сервер MCP не може цього здійснити. Знаходячись по інший бік мережевого кордону, він працює з збереженими даними та не має жодної інформації про те, що зараз відображається на будь-якому екрані. Інструменти WebMCP знаходяться всередині вкладки, ділячись її DOM, сеансом та кукі, тому агент та користувач бачать один екран, і кожен з них може його змінити. Це є справжньою перевагою, але водночас і справжнім ризиком, про який йтиметься окремо нижче. Саме тому називати WebMCP «MCP всередині браузера» — це недооцінка його можливостей. Місце розташування інструменту має значення, адже він пов’язаний із саме тим станом сторінки, який переглядає користувач.
Підключення зовнішнього агента
Панель Pretend Agent допомагає під час розробки, проте її корисність обмежена, адже це сторінка, яка сама себе викликає. Для належного тестування потрібен інструмент поза браузером, який зможе бачити лише те, що повідомляє getTools(). Тому існує agent.mjs — невеликий інструмент командного рядка з елементами протоколу Chrome DevTools та без жодних залежностей, оскільки Node 22 має глобальний об’єкт WebSocket. За своєю конструкцією він може бачити лише те, що повідомляє getTools(), та діяти лише через executeTool(), без можливості збору даних з DOM.
node agent.mjs list
node agent.mjs call add-task '{"title":"Ship the demo","priority":"high"}'
Ці два команди перелічують інструменти, а потім викликають add-task із заголовком та пріоритетом. Налаштування, які створили скріншоти, йдуть ще на крок далі. Claude Desktop, Claude Code та ChatGPT використовують MCP, а не WebMCP, тому mcp-bridge.mjs виступає у ролі адаптера, причому досить простого, оскільки протоколи майже повністю збігаються: tools/list відповідає getTools(), а tools/call — executeTool(). Claude Code отримує ці можливості через файл .mcp.json.
{
"mcpServers": {
"webmcp-board": {
"command": "node",
"args": ["C:\\projects\\web-mcp-demo\\mcp-bridge.mjs"],
"env": { "PAGE_URL": "https://tusharkanjariya.github.io/web-mcp-demo/" }
}
}
}
Отже, вся ланцюгова структура полягає у тому, що Claude Code використовує MCP для спілкування з бріджем, брідж — CDP для спілкування з Chrome, а Chrome викликає інструменти, зареєстровані на сторінці. Це чотири етапи, і сторінка в кінці не може дізнатися, який клієнт знаходиться з іншого боку.
Claude Code
↓
MCP
↓
mcp-bridge.mjs
↓
Chrome DevTools Protocol
↓
WebMCP
↓
my task board
Ще одна зручність: якщо на порту для налагодження ніхто не відповідає, міст запускає власний Chrome та відкриває сторінку, коли її немає у вкладці. Клієнт MCP може запустити все з нуля.
Відмова
Потім від агента попросили видалити одне завдання.
У верхній частині скріншота показано запит на фільтрацію з раніше. У нижній частині — найцікавіше: Claude сказав, що на цьому панелі не існує інструменту delete-task, що єдиний спосіб видалення — це clear-completed, який також видалить завдання №2, №3, №4 та №5, і що він не збирається це робити.
Жоден захисний елемент, системний запит чи рівень безпеки не спричинили відмови. Функція видалення окремого завдання просто не існувала, і агент це помітив. Було б помилкою надавати занадто багатого значення одній переписці в іграшковому додатку. Проте це змінює сприйняття проблеми. Агент, який керує сторінкою за допомогою пікселів та координат, знайшов би щось схоже на засіб видалення та спробував би його використати, адже агенти для збору даних з екрана можуть покладатися лише на імпровізацію. Цей агент перерахував свої шість інструментів, побачив, що жоден з них не передбачає видалення завдання, усвідомив можливі побічні наслідки найближчого варіанту та зупинився, щоб запитати.
Поверхня інструментів була межею. Ніхто не написав правило проти видалення чого-небудь. Сторінка просто ніколи не пропонувала видалення як можливості, і цього виявилося достатньо, хоча WebMCP і не робить агентів магічно безпечними.
Де Chrome та специфікація не згодні
Під час тестування виявились розбіжності між документацією та поведінкою браузера, і їх усунення зайняло півдня. Три з них стосуються формату даних.
- Аргументи для
executeTool(): у специфікації описується об’єкт, але Chrome 151 очікує рядок у форматі JSON, і об’єкт викликає помилкуFailed to parse input arguments. RegisteredTool.inputSchema: у специфікації вказаний як об’єкт, але Chrome повертає рядок у форматі JSON, тому його потрібно розпарсити перед читаннямproperties.- Те, що насправді повертає
executeTool(): у специфікації зазначеноDOMString, що є правдою, але це серіалізований обгорток{content:[…]}, який потрібно розпакувати двічі, щоб отримати текст.
Усе це не є складним, якщо знати принципи. Дві проблеми, які справді завдали труднощів, були помилками життєвого циклу, і вони з’являються лише тоді, коли справжній агент працює з сторінкою.
Виклик form.reset() під час виконання процедури надсилання даних, ініційованої агентом, скасовує виклик інструменту з попередженням Tool execution cancelled by a form reset. Звичайний обробник, який, як це робить більшість рішень, спорожнює поля введення після надсилання даних, призводив до зупинки кожної операції add-task, виконуваної агентом. Завдання створювалося, але відповідь так і не надходила, саме тому раніше показаний обробник скидає дані лише у випадку надсилання даних людиною.
У версіях до Chrome 153 видалення інструменту також скасовувало всі його запити, які ще виконувалися. Це не становило проблеми, доки інструмент не видаляв себе внаслідок успішного виконання. Функція clear-completed видаляла всі завершені завдання, тож нічого не залишалося незавершеним, і інструмент сам відступав. У версії 151 цей механізм переривання завершував власну відповідь ще до того, як вона могла бути надіслана. Рішенням є setTimeout(() => ctl.abort(), 0): відкладення переривання на один такт дозволяє спочатку завершити запит. Це зрозуміло та ефективно, але відкладення завершення роботи інструменту на один такт — це той тип виправлення, який варто переглянути після кількох випусків Chrome.
Випуск демо-версії
Демо-версія є повністю статичною, тому GitHub Pages може її подавати без змін, а HTTPS забезпечує безпечний контекст, необхідний для WebMCP. Таким чином у всіх є функціональна дошка завдань, але не функціональний WebMCP, оскільки відвідувачі, які не встановили відповідну позначку, все ще бачать, що document.modelContext є невизначеним. Щоб це виправити, потрібен токен тестового доступу з указанням походження в розділі <head>, зареєстрований як токен першої сторони, оскільки Chrome відхиляє токени третьої сторони, розміщені в тегах meta. У чистому профілі без жодних позначок modelContext повертався у вигляді „object“ разом із усіма шістьма інструментами. Тестовий період триває до версії Chrome 156.
Питання безпеки без належної відповіді
Інструменти WebMCP виконуються всередині вікна, у якому є автентифікація. У цьому й полягає суть, але водночас і вся проблема.
У специфікації є поля untrustedContentHint, яке є лише підказкою. Там досі не вирішено, як слід поводитися з вставкою команд, і жодна частина цього API не є межею авторизації. Якщо текст, контрольований зловмисником, може бути відображений на сторінці та прочитаний агентом, readOnlyHint не допоможе.
Цей міст також можна надавати через HTTP через тунель, щоб конектори ChatGPT могли до нього дістатися. Він функціонує, але обережно ставтесь до цього, адже тунель робить кінцеву точку доступною без автентифікації, що дозволяє будь-кому, хто вгадає адресу, запускати інструменти у вашій відкритій вкладці браузера. Двадцять хвилин експериментів — це нормально, але залишати його увімкненим на всю ніч — ні.
Спробувати у власному додатку
Адміністратори веб-додатку можуть оцінити API за один день, адже він невеликий. Виберіть будь-яку існуючу форму, додайте до неї атрибути toolname та tooldescription, потім відкрийте панель WebMCP у DevTools — інструмент з’явиться. Приблизно десять хвилин роботи навчать більше, ніж ще одна година читання.
Це стосується не лише синтаксису. Чесне описання того, що може робити ваш додаток, стало частиною самого продукту, а не документацією, написаною пізніше. Керування не передбачало можливості видалення окремих елементів, тому агент ніколи їх не видаляв. Те, що повертає функція getTools(), визначає, як агент уявляє можливості вашого додатку, і це уявлення поступово перетворюється на сам інтерфейс. У демонстрації було шість інструментів у трьох файлах без жодних залежностей, тож подумайте, що б написала ваша власна сторінка про себе. Щоб відтворити це, увімкніть параметр chrome://flags/#enable-webmcp-testing, а потім виконайте команду node agent.mjs list. Якщо у відповідь отримаєте шість інструментів, це означає, що ви спілкуєтесь із веб-сторінкою так, як це робив би агент.
Швидкі відповіді
Що таке WebMCP? Це запропонований API браузера, за допомогою якого сторінка може оприлюднювати власні функції для AI-агента у вигляді інструментів, які можна викликати, використовуючи document.modelContext. Агенти викликають ці інструменти безпосередньо, уникаючи парсингу DOM та координації кліків.
У чому він відрізняється від звичайного сервера MCP? Бекенд-сервери працюють за межами мережі та обробляють збережені дані. Інструменти WebMCP виконуються у вкладці, тому вони можуть змінювати те, що бачить користувач, та використовують сеанс та дозволи сторінки. Виклик filter-tasks видимо змінив інтерфейс.
Як сторінка реєструє інструмент? Можна додати атрибути toolname та tooldescription до існуючої форми та дозволити браузеру вивести схему з полів, або викликати document.modelContext.registerTool() з ім’ям, описом, JSON Schema та функцією execute. Форми підходять для простих дій, а registerTool дозволяє реалізувати все, що неможливо виразити за допомогою форм.
Чи потрібен прапорець? Наразі — так. Увімкніть цей прапорець та знову запустіть Chrome, або запустіть його з параметром --enable-features=WebMCP. Для відвідувачів, які не налаштували цей прапорець, підійде тимчасовий токен від первинного джерела в розділі <head> сторінки.
Чи можуть Claude Desktop чи ChatGPT викликати ці інструменти? Не самостійно, оскільки вони використовують MCP замість WebMCP. Невеликий адаптер перенаправляє tools/list на getTools(), а tools/call — на executeTool(). Цей код складається приблизно з 130 рядків, не вимагає додаткових залежностей та працює через стандартний ввод-вивід для локальних клієнтів.
Чи є це безпечним? Це питання залишається відкритим. untrustedContentHint та readOnlyHint є лише підказками; жоден з них не визначає меж авторизації, а специфікація ще не вирішила проблему вставки запитів. Розробляйте інтерфейс інструментів, припускаючи, що агент може бути переконаний викликати будь-що, що ви надаєте.
Пов’язана література
- Коли у штучного інтелекту є 1 000 інструментів MCP: вибір, контекст та навички — Підключення агентів до багатьох серверів MCP призводить до надмірної кількості інструментів, перевантаженого контексту та невдалих виборів. Чому процес відкриття інструментів, навички та шлюзи можуть бути важливішими за прямий доступ.
- Практичні нотатки: MCP для дослідницьких лабораторій: один контракт для інструментів, даних та — Детальний огляд практичних нотаток: MCP для дослідницьких лабораторій: один контракт для інструментів, даних та — контракти, перевірки та готові фрагменти коду для команд, які використовують цю модель.