Interfejs Imperative API używa document.modelContext.registerTool() do programowego definiowania narzędzi JavaScript. Jest to idealne rozwiązanie w przypadku aplikacji jednostronicowych (SPA), w których narzędzia muszą być dodawane lub usuwane na podstawie bieżącej ścieżki lub stanu użytkownika.
WebMCP działa w architekturze producent/konsument:
- Strony internetowe są producentami narzędzi: definiujesz i rejestrujesz narzędzia za pomocą
document.modelContext.registerTool(). To wszystko, co musi zawierać strona internetowa. Nie musisz wywoływać na stronie funkcjigetTools()aniexecuteTool(), aby udostępnić narzędzia agentom. - Przeglądarka i rozszerzenia są głównymi konsumentami: agenty udostępniane przez przeglądarkę lub za jej pośrednictwem – w tym wbudowany agent przeglądarki, rozszerzenia przeglądarki, Narzędzia deweloperskie i asystenci na poziomie systemu – wykrywają i wywołują zarejestrowane narzędzia poza pasmem za pomocą wewnętrznych mechanizmów platformy.
- Agenty i jarzma testowe na stronie:
getTools()iexecuteTool()istnieją specjalnie dla asystentów JavaScriptu na stronie (np. wbudowanego widżetu czatu lub<iframe>) oraz automatycznych pakietów testowych, które muszą wykrywać i wywoływać narzędzia bezpośrednio w kontekście strony.
Rejestracja i cykl życia
Narzędzia są rejestrowane przez przekazanie obiektu definicji narzędzia i opcjonalnego obiektu opcji (ModelContextRegisterToolOptions), który obsługuje 2 opcjonalne elementy:
signal:AbortSignalużywany do wyrejestrowania narzędzia, gdy nie jest już potrzebne.exposedTo: tablica ciągów znaków pochodzenia, która określa, które dokumenty w drzewie dokumentów mogą wykrywać i wykonywać narzędzie poza granicami ramki.
Obsługa cyklu życia za pomocą AbortController
WebMCP nie udostępnia metody unregisterTool(). Aby wyrejestrować narzędzie, musisz przekazać AbortSignal podczas rejestracji i przerwać ten sygnał, gdy narzędzie nie będzie już potrzebne.
const controller = new AbortController();
await document.modelContext.registerTool({
name: "get_user_preferences",
description: "Retrieves the user's saved preferences.",
inputSchema: { type: "object", properties: {} },
execute() {
const prefs = localStorage.getItem("user_prefs");
return prefs ? JSON.parse(prefs) : { theme: "light" };
},
annotations: { readOnlyHint: true }
}, { signal: controller.signal });
// To unregister the tool (e.g., on component unmount):
controller.abort();
Przerwanie sygnału rejestracji powoduje jedynie wyrejestrowanie narzędzia – nie anuluje już uruchomionych wykonań. Narzędzia, które rozpoczynają długotrwałe zadania, muszą też uwzględniać opisany poniżej sygnał dotyczący wykonania. W przeciwnym razie wyrejestrowanie pozostawi osierocone żądania i nieaktualne aktualizacje interfejsu.
Kontrolowanie ekspozycji narzędzi w różnych domenach za pomocą exposedTo
Domyślnie zarejestrowane narzędzie jest prywatne dla dokumentu rejestrującego i ramek z tego samego źródła. Jeśli Twoja aplikacja jest umieszczona w elemencie <iframe> (lub zawiera widżety innych firm) i potrzebujesz ramek nadrzędnych lub równorzędnych, aby wykryć i wywołać narzędzie, określ dozwolone źródła za pomocą exposedTo.
const controller = new AbortController();
await document.modelContext.registerTool({
name: "get_cart_items",
description: "Retrieves items currently in the user's shopping cart.",
inputSchema: { type: "object", properties: {} },
async execute(input, { signal }) {
return await cartService.getItems({ signal });
},
annotations: { readOnlyHint: true },
}, {
signal: controller.signal,
exposedTo: ["https://checkout.example.com"],
});
- Wzajemne uzgadnianie akceptacji: wykrywanie narzędzi współdzielenia wymaga akceptacji po obu stronach. Ramka dostawcy narzędzia autoryzuje źródło klienta za pomocą
exposedTo, a ramka klienta musi wyraźnie zażądać tego źródła dostawcy za pomocągetTools({ fromOrigins: [...] }). BezfromOriginsgetTools()wysyła zapytania tylko do dokumentów z tego samego źródła. - Dopasowanie źródła: źródło dokumentu rejestracyjnego zawsze ma dostęp. Źródła określone w
exposedTomają uprawnienia do wykrywania narzędzia za pomocądocument.modelContext.getTools({ fromOrigins: [...] })i wykonywania go za pomocądocument.modelContext.executeTool(). - Wymaganie dotyczące bezpieczeństwa: każde źródło w
exposedToMUSI być potencjalnie godne zaufania (HTTPS lub localhost). Przekazanie nieprawidłowego lub niezabezpieczonego źródła powoduje odrzucenie metodyregisterTool()z błędemSecurityError. - Zakres:
exposedTokontroluje wykrywanie dokumentów na stronie w ramkach w drzewie dokumentu.
Definiowanie parametrów
Parametry są definiowane za pomocą właściwości inputSchema. Musi to być obiekt JSON Schema, który opisuje uporządkowane dane oczekiwane przez narzędzie.
await document.modelContext.registerTool({
name: "calculate_area",
description: "Calculates the area of a rectangle.",
inputSchema: {
type: "object",
properties: {
width: { type: "number", description: "The width of the rectangle." },
height: { type: "number", description: "The height of the rectangle." }
},
required: ["width", "height"]
},
execute(input) {
// input is { width: 10, height: 20 }
return input.width * input.height;
},
annotations: { readOnlyHint: true }
});
Pisanie opisów narzędzi i parametrów
Agent otrzymuje już pełny inputSchema, więc description musi tylko określać, co robi narzędzie i kiedy go używać. NIE powtarzaj w nim nazw parametrów, typów, wartości enum, wartości domyślnych ani limitów. Zamiast tego zadeklaruj je w schemacie (enum, default, minimum/maximum). Opisy parametrów podlegają tej samej zasadzie: podaj, co oznacza wartość, a nie jej typ lub dozwolone wartości.
Przeglądarka nie sprawdza argumentów pod kątem inputSchema ani nie stosuje wartości default, więc obie te czynności wykonaj w execute.
const SORT_ORDERS = ["newest", "price_low_to_high"];
await document.modelContext.registerTool({
name: "search_products",
// Don't list the sort orders here, even via SORT_ORDERS: `enum` already does.
description: "Searches the product catalog by keyword. Use when the user wants to find or compare products.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Words to search for." },
sort: { type: "string", enum: SORT_ORDERS, default: "newest", description: "Order of the results." },
},
required: ["query"],
},
async execute({ query, sort = "newest" }, { signal }) {
if (typeof query !== "string" || !SORT_ORDERS.includes(sort)) {
return { error: "Pass a search query and a supported sort order.", sortOrders: SORT_ORDERS };
}
return await productService.search({ query, sort }, { signal });
},
annotations: { readOnlyHint: true },
});
Wzorce wykonywania
Wywołanie zwrotne execute zawsze otrzymuje 2 argumenty: obiekt wejściowy i obiekt opcji zawierający AbortSignal – execute(inputObject, { signal }).
Kiedy używać async execute
Użyj async, gdy narzędzie obejmuje operacje, które zwracają obiekt Promise lub wymagają czasu na wykonanie:
- Wywołania sieciowe: pobieranie danych z interfejsu API.
- Asynchroniczne miejsce na dane: dostęp do IndexedDB.
- Zdarzenia zewnętrzne: oczekiwanie na zmianę stanu lub zakończenie animacji.
async execute(input, { signal }) {
const response = await fetch(`/api/data/${input.id}`, { signal });
return await response.json();
}
Kiedy używać execute (synchronicznego)
Użyj standardowej funkcji synchronicznej do natychmiastowych operacji. Narzędzie synchroniczne działa do zakończenia, zanim będzie można zaobserwować anulowanie, więc może zignorować sygnał:
- Czysta logika: obliczenia matematyczne, filtrowanie lub sortowanie danych, które są już w pamięci.
- Stan synchroniczny: odczyt z
localStoragelub menedżera stanu synchronicznego.
execute(input) {
return input.items.filter(item => item.active);
}
Obsługa błędów w execute
Gdy execute zgłasza błąd (lub zwraca odrzuconą obietnicę), agent otrzymuje tylko ogólny błąd wykonania. Komunikat o błędzie nie jest przekazywany do pracownika obsługi klienta. Jeśli narzędzie napotka błąd, który można naprawić lub który można rozwiązać (np. nieprawidłowe dane wejściowe, brakujące elementy lub nieprawidłowy stan HTTP), NIE throw new Error(...). Zamiast tego zwróć uporządkowany ładunek błędu za pomocą normalnej wartości zwracanej przez narzędzie, aby agent AI otrzymał szczegóły diagnostyczne i mógł samodzielnie poprawić błąd lub spróbować ponownie:
execute({ itemName }) {
const item = menuItems.find((entry) => entry.name.toLowerCase() === itemName.toLowerCase());
if (!item) {
// Return the error as a value instead of throwing, so the agent receives the message:
return {
error: `Menu item "${itemName}" not found.`,
availableItems: menuItems.map((entry) => entry.name),
};
}
return item;
}
Zgłaszaj ponownie tylko wtedy, gdy wykonanie zostało anulowane (signal.aborted to true).
Anuluję wykonywanie w trakcie
Anulowanie jest inicjowane przez użytkownika lub agenta. OBOWIĄZKOWE: każde narzędzie, które wykonuje żądania sieciowe lub inne długotrwałe zadania asynchroniczne, MUSI uwzględniać signal. W przeciwnym razie anulowane wykonania będą nadal zużywać zasoby i mogą zapisywać nieaktualne dane w interfejsie po przejściu agenta do innego zadania.
- Interfejsy API, które akceptują sygnał (czytniki
fetch(),addEventListener(),ReadableStream): przekazująsignalbezpośrednio. - Interfejsy API, które nie obsługują (
setTimeout,setInterval, pętle odpytywania, potoki z dużą liczbąawait): wywołajsignal.throwIfAborted()między krokami lub odrzuć w przypadkusignal.addEventListener('abort', ...). Minutniki nie akceptują sygnału, więc musisz je samodzielnie wyczyścić w detektorze przerwania.
await document.modelContext.registerTool({
name: "fetch_tool",
description: "Fetch the text content of a URL and show it on the page.",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "The URL to fetch" },
priority: {
type: "string",
enum: ["high", "low", "auto"],
description: "Relative network priority for the request",
},
},
required: ["url"],
},
execute: async ({ url, priority }, { signal }) => {
const output = document.querySelector("pre");
output.textContent = "Loading…";
try {
// Passing `signal` aborts the network request the moment execution is cancelled.
const response = await fetch(url, { priority, signal });
if (!response.ok) {
output.textContent = "";
return { error: `Request failed with status ${response.status}` };
}
const text = await response.text();
output.textContent = text;
return text;
} catch (err) {
// Roll back the UI state this execution set, so a cancelled run leaves no trace.
output.textContent = "";
if (signal.aborted) {
// The execution was cancelled: re-throw instead of returning an error value.
throw err;
}
// Return other errors as a value instead of throwing, so the agent receives the message.
return { error: err instanceof Error ? err.message : String(err) };
}
},
annotations: { readOnlyHint: true }
});
Narzędzia do zgłaszania treści
Od Chrome 154 zasada annotations obsługuje consequentialHint. Ustaw wartość true w przypadku narzędzi, które wykonują działania o wysokiej stawce, nieodwracalne lub w świecie rzeczywistym – rezerwowanie podróży, przesyłanie pieniędzy, wysyłanie wiadomości lub usuwanie danych. Sygnalizuje agentom, że przed wykonaniem działania muszą poprosić użytkownika o wyraźne potwierdzenie.
await document.modelContext.registerTool({
name: 'book_flight',
description: 'Book a flight for the user with confirmed flight details.',
inputSchema: {
type: 'object',
properties: {
flightId: { type: 'string', description: 'ID of the flight to book' },
passengers: { type: 'number', description: 'Number of tickets to purchase' },
},
required: ['flightId', 'passengers'],
},
execute: async ({ flightId, passengers }, { signal }) => {
// Forward `signal` to the transaction API to honor cancellation:
return await bookFlightReservation({ flightId, passengers }, { signal });
},
annotations: {
readOnlyHint: false, // This tool mutates state (it makes a booking).
consequentialHint: true, // Spends money and is not trivially reversible.
untrustedContentHint: false, // Output is first-party data, not third-party content.
}
});
OBOWIĄZKOWO: traktuj consequentialHint jako wskazówkę, a nie mechanizm egzekwowania. Nie blokuje on samodzielnie wykonywania, a przeglądarki, które nie obsługują tej wskazówki, nie będą w ogóle wyświetlać prośby. Zachowaj potwierdzenie w aplikacji, które jest już wymagane przez działanie.
Narzędzia do debugowania zgłaszania
Od wersji Chrome 156 annotations obsługuje debugging. W przypadku narzędzi przeznaczonych do debugowania i narzędzi dla programistów, a nie do zadań użytkowników, ustaw annotations.debugging na true. Przykłady: zrzucanie stanu wewnętrznego aplikacji, przełączanie flag funkcji lub wypełnianie danymi testowymi. Dzięki temu agenty mogą odróżnić narzędzia przeznaczone tylko dla deweloperów od narzędzi, które mają działać w imieniu użytkownika. Domyślna wartość to false. Agenty na stronie i jarzma testowe mogą odczytywać te dane z RegisteredTool.annotations.debugging za pomocą getTools().
await document.modelContext.registerTool({
name: "dump_app_state",
description: "Returns a snapshot of the internal application store for debugging.",
execute() {
return appStore.getState();
},
annotations: {
readOnlyHint: true, // Only reads state.
debugging: true, // Developer tooling, not an end-user action.
}
});
Agenty na stronie i testowanie (getTools i executeTool)
Strony internetowe nie potrzebują getTools() ani executeTool(), aby udostępniać narzędzia agentom AI. Wystarczy wywołać document.modelContext.registerTool(), aby agenty przeglądarki, rozszerzenia przeglądarki, Narzędzia deweloperskie i asystenci na poziomie systemu mogli wykrywać i wykonywać narzędzia poza pasmem.
Metody getTools() i executeTool() są przeznaczone specjalnie dla klientów działających w środowisku JavaScript:
- Agenty JavaScript na stronie: asystent AI, widżet pomocniczy lub interfejs czatu umieszczony bezpośrednio na stronie (lub działający w elemencie
<iframe>), który musi wykryć, jakie narzędzia oferuje otaczająca strona lub dostępne ramki w drzewie dokumentu, i wywołać je w imieniu użytkownika. - Zautomatyzowane testowanie i debugowanie: pakiety testów (takie jak Playwright, Vitest lub testy jednostkowe w przeglądarce), które sprawdzają zarejestrowane metadane narzędzi i symulują wywołania narzędzi agenta bez konieczności korzystania z zewnętrznego LLM.
Odkrywanie dostępnych narzędzi za pomocą getTools()
Agenty na stronie wywołują document.modelContext.getTools(), aby pobrać tablicę obiektów RegisteredTool z bieżącego dokumentu i wszystkich dostępnych ramek w drzewie dokumentu.
Od wersji Chrome 155 RegisteredTool.inputSchema jest zwracany bezpośrednio jako obiekt w JavaScripcie (głęboka kopia schematu dostarczonego podczas rejestracji). Zmiana tego obiektu nie wpływa na rzeczywisty schemat zarejestrowanego narzędzia.
// Example: An in-page AI assistant discovering tools available on the page:
const tools = await document.modelContext.getTools();
// In-page agents can listen for tools being registered or unregistered dynamically:
document.modelContext.addEventListener("toolchange", async () => {
const updatedTools = await document.modelContext.getTools();
console.log(`Active tools updated: ${updatedTools.map((t) => t.name).join(", ")}`);
});
Aby wykryć narzędzia zarejestrowane w elementach iframe potomnych współdzielenia (które podczas rejestracji muszą wyraźnie przyznać dostęp przez exposedTo), przekaż fromOrigins w słowniku opcji:
// Query tools registered in the embedded cart frame that exposed them to this checkout origin:
const cartTools = await document.modelContext.getTools({
fromOrigins: ["https://cart.example.com"],
});
Wywoływanie narzędzi za pomocą executeTool()
Agenty na stronie i skrypty testowe wywołują funkcję document.modelContext.executeTool(tool, inputObject, options), aby wykonać zarejestrowane narzędzie. Narzędzie działa w dokumencie, w którym zostało zarejestrowane, a obietnica jest rozwiązywana do wyniku w postaci ciągu znaków zwróconego przez wywołanie zwrotne execute narzędzia.
Od wersji Chrome 155 executeTool() akceptuje zwykły obiekt w JavaScripcie bezpośrednio dla inputObject, co odpowiada formatowi danych wejściowych, który otrzymuje wywołanie zwrotne execute narzędzia.
// 1. Discover the tool (typically done during agent initialization or test setup):
const tools = await document.modelContext.getTools();
const areaTool = tools.find((t) => t.name === "calculate_area");
if (areaTool) {
const controller = new AbortController();
// Example input payload matching the tool's inputSchema:
const inputObject = { width: 10, height: 20 };
const options = { signal: controller.signal };
// 2. Pass input arguments as a JavaScript object, with a JSON string fallback for Chrome <155:
let resultString;
try {
resultString = await document.modelContext.executeTool(areaTool, inputObject, options);
} catch (err) {
if (err?.message?.startsWith("Failed to parse input")) {
resultString = await document.modelContext.executeTool(
areaTool,
JSON.stringify(inputObject),
options
);
} else {
// Re-throw real tool execution or cancellation errors:
throw err;
}
}
console.log("Execution output string:", resultString);
}
Zwrócona obietnica zostanie odrzucona, jeśli:
inputObjectnie jest obiektem lub nie można go przekształcić w ciąg JSON (np. odwołania cykliczne lub wartościBigInt). Pamiętaj, że wartości pominięte przez serializację JSON (takie jak funkcje iundefined) zostaną pominięte w obiekcie otrzymanym przez narzędzie, a nie spowodują odrzucenia.- Wykonanie zostało przerwane przez:
options.signal. - Narzędzie zostało wyrejestrowane przed wykonaniem lub wystąpił nieobsługiwany błąd środowiska wykonawczego.
Wzorzec fabryki narzędzi
Aby przekazać kontekst (np. sklepy lub instancje aplikacji) do narzędzi, użyj funkcji fabrycznych.
export function createInventoryTool(inventoryManager) {
return {
name: "get_inventory",
description: "Lists items in the inventory.",
inputSchema: { type: "object", properties: {} },
execute() {
return inventoryManager.getItems();
},
annotations: { readOnlyHint: true }
};
}
Typy TypeScript
W projektach TypeScript NIE pisz ręcznie deklaracji dla document.modelContext. Zainstaluj oficjalny pakiet webmcp-types (npm install --save-dev webmcp-types) i dodaj go do tsconfig.json, ponieważ nie jest on automatycznie wykrywany:
{
"compilerOptions": {
"types": ["webmcp-types"]
}
}
- Typ
document.modelContextjest opcjonalny, więc przed użyciem zabezpiecz go za pomocąif (document.modelContext). - W przypadku wbudowanego elementu
inputSchematyp danych wejściowychexecutejest określany na podstawie schematu. - Nazwane typy, takie jak
WebMCP.ModelContextTool, są globalne (nie trzeba ich importować) i przydatne do wpisywania fabryk narzędzi.
Obsługa platformy
OBOWIĄZKOWO: jeśli aplikacja korzysta z React, Angular lub Vue, zarejestruj narzędzia za pomocą oficjalnej integracji WebMCP tej platformy, która jest wymieniona poniżej. NIE wywołuj funkcji document.modelContext.registerTool() z komponentów ani nie twórz własnego kodu opakowującego cykl życia. Każda integracja obsługuje już wykrywanie funkcji, czyszczenie AbortSignal i narzędzia do określania zakresu w cyklu życia komponentu lub wstrzykiwacza. Te interfejsy API są eksperymentalne, więc przed napisaniem kodu sprawdź bieżącą sygnaturę w typach lub dokumentacji pakietu.
| Platforma | Pakiet | Interfejs API | Dokumenty |
|---|---|---|---|
| React 18+ | use-webmcp-tool |
useWebMCP(options): 1 wywołanie na narzędzie |
npmjs.com/package/use-webmcp-tool |
| Angular | @angular/core, @angular/forms/signals |
provideExperimentalWebMcpTools([...]) (dostawcy aplikacji lub tras), declareExperimentalWebMcpTool(...) (usługi wewnętrzne), provideExperimentalWebMcpForms() + opcja experimentalWebMcpTool w formularzach Signal Forms |
angular.dev/ai/webmcp |
| Vue | @vueuse/core |
Funkcja kompozycyjna useWebMCP(options) |
vueuse.org/core/usewebmcp |
Wszystkie zasady opisane w tym przewodniku obowiązują również w przypadku korzystania z tych integracji:
- Zachowaj ten sam kształt definicji narzędzia:
name,description,inputSchema,execute(input, { signal })iannotations. - Przestrzegaj zasad dotyczących opisów i adnotacji.
- Sprawdź dane wejściowe i zastosuj wartości domyślne w
execute. - Przekaż
signaldo długotrwałego zadania. - Aby udostępniać narzędzie tylko przez pewien czas, przekaż
enabled(React, Vue). NIE umieszczaj wywołania funkcji hook ani funkcji kompozycyjnej w warunku. - Integracje mogą opakowywać wyniki i błędy (np. jako
{ content, isError }). Zanim zaczniesz polegać na sposobie, w jaki błędy docierają do agenta, zapoznaj się z dokumentacją integracji.
Uwagi dotyczące interfejsu API
- annotations: (opcjonalnie) słownik metadanych narzędzia.
- readOnlyHint: (opcjonalnie) ustaw wartość
true, jeśli narzędzie nie modyfikuje żadnego stanu i tylko odczytuje dane. Pomaga to agentom zdecydować, kiedy można bezpiecznie wywołać narzędzie. - consequentialHint: (opcjonalnie, Chrome 154 lub nowszy) ustaw wartość
truew przypadku działań o dużym znaczeniu, nieodwracalnych lub rzeczywistych, aby agenty najpierw prosiły użytkownika o potwierdzenie. - untrustedContentHint: (opcjonalnie) ustaw wartość
true, gdy narzędzie zwraca treści, których Twoja witryna nie kontroluje, np. tekst wygenerowany przez użytkownika lub odpowiedzi interfejsu API innej firmy. - debugging: (opcjonalnie, Chrome 156 lub nowszy) ustaw wartość
true, gdy narzędzie jest przeznaczone do debugowania i narzędzi dla programistów, a nie do zadań użytkownika. Domyślna wartość tofalse.
- readOnlyHint: (opcjonalnie) ustaw wartość
- Opcje rejestracji:
- signal: (opcjonalnie)
AbortSignalużywany do wyrejestrowania narzędzia, gdy nie jest już potrzebne. - exposedTo: (opcjonalnie) tablica ciągów znaków bezpiecznego źródła, która określa, które dokumenty w drzewie dokumentów mogą wykrywać i wykonywać narzędzie poza granicami ramki.
- signal: (opcjonalnie)
- Opcje wykrywania narzędzi (
ModelContextGetToolOptions):- fromOrigins: (opcjonalnie) tablica ciągów znaków bezpiecznych źródeł, które mają być wyszukiwane w dostępnych ramkach. Wywołanie
getTools()bezfromOriginswysyła zapytania tylko do dokumentów z tego samego źródła.
- fromOrigins: (opcjonalnie) tablica ciągów znaków bezpiecznych źródeł, które mają być wyszukiwane w dostępnych ramkach. Wywołanie
- Format zwracanych danych: funkcja
executemoże zwracać dowolną wartość, którą można przekształcić w format JSON (obiekt, tablicę, ciąg znaków, liczbę, wartość logiczną). Komunikat o wyjątku zgłoszonym przezexecutenie jest przekazywany do agenta, dlatego zwracaj błędy, które można naprawić, jako ustrukturyzowane ładunki błędów (np.{ error: "..." }) za pomocą wartości zwracanej przez narzędzie, aby agent mógł je odczytać i podjąć odpowiednie działania. - Bezpieczny kontekst: WebMCP wymaga protokołu HTTPS. Wszystkie źródła w
exposedToifromOriginsmuszą być potencjalnie godne zaufania. - Wycofane/usunięte:
navigator.modelContext(wycofane w Chromium 150),unregisterTool(),provideContext()iclearContext()nie są już obsługiwane.
Strategie zastępcze
Interfejs WebMCP Imperative API powinien być używany z wykrywaniem funkcji, aby zapewnić zgodność z przeglądarkami, które nie obsługują jeszcze WebMCP.
if ('modelContext' in document) {
// Register tools
}