Dev

WebMCP auf einer echten Website: Tools mit document.modelContext registrieren, und warum mein navigator.modelContext-Skript nichts registriert

Im Mai ist die API von navigator zu document umgezogen. Schon davor hatte sie drei Methoden verloren, und danach gibt ihr Registrierungsaufruf ein Promise zurück. Ein Skript, das im April geschrieben wurde, um einen Detektor zufriedenzustellen, kann einem aktuellen Browser null Tools anbieten. Hier stehen die funktionierende Registrierung, ein Test, der ohne Agenten auskommt, und die Fälle, in denen ein schlichter MCP-Endpunkt der bessere Zugang ist.

Im April habe ich dieser Website über WebMCP zwei Tools hinzugefügt: get_contact und list_products, beide read-only, beide aus einem kleinen Inline-Skript im Head der Seite registriert. Am 11. Oktober habe ich einen aktuellen Chrome auf die Startseite gerichtet und den Browser gefragt, welche Tools die Seite anbietet. Die Antwort war eine leere Liste.

Auf der Website war nichts kaputtgegangen. Das Skript sucht weiterhin nach navigator.modelContext, und die Spezifikation definiert den Einstiegspunkt inzwischen an document. Der unangenehmere Teil steht in meinen eigenen Notizen vom April: Ich habe dieses Skript nie gegen einen Browser getestet, nur gegen einen Scanner, und auch der Scanner hat die Tools nie gemeldet. Ich gehe hier also die Registrierung von Tools Schritt für Schritt durch, so wie der Entwurf sie heute beschreibt, mit den Befehlen, die ich zur Kontrolle ausgeführt habe, und mit meinem eigenen Skript als Fallbeispiel dafür, was dir ein Detektor nicht sagen kann.

Was ist WebMCP, und wo liegt die API inzwischen?

WebMCP ist eine vorgeschlagene Browser-API, mit der eine Seite einem Agenten eine Liste benannter JavaScript-Funktionen übergibt, jede mit einer Beschreibung und einem JSON Schema für ihre Argumente. Im Entwurf vom 9. Oktober 2026 ist der Einstiegspunkt document.modelContext, nur in sicheren Kontexten. Gekürzt auf die drei Methoden, die ich hier verwende:

webidl
partial interface Document {
  [SecureContext, SameObject] readonly attribute ModelContext modelContext;
};
 
interface ModelContext : EventTarget {
  Promise<undefined> registerTool(ModelContextTool tool,
      optional ModelContextRegisterToolOptions options = {});
  Promise<sequence<RegisteredTool>> getTools(
      optional ModelContextGetToolOptions options = {});
  Promise<DOMString> executeTool(RegisteredTool tool,
      optional object inputObject,
      optional ModelContextExecuteToolOptions options = {});
};

Es ist ein Draft Community Group Report, und das Dokument sagt selbst, dass es kein W3C-Standard ist und nicht auf dem Standards Track liegt. Die Historie des Repositorys zeigt, wie viel sich für alle verschoben hat, die früh integriert haben:

DatumÄnderungPull Request
5. März 2026provideContext() und clearContext() entfernt#132
26. und 27. März 2026registerTool() nimmt ein AbortSignal entgegen, unregisterTool() aus dem Explainer gestrichen#147, #156
27. Mai 2026der Getter modelContext von Navigator nach Document verschoben#184
8. Juni 2026registerTool() gibt ein Promise zurück#200
8. Oktober 2026die deklarative, formularbasierte API "for now" (vorerst) aus der Spec entfernt#338

Chrome hat im Februar eine Early Preview angekündigt und im Juni einen Origin Trial ab Chrome 149. Der Implementierungsstatus im Spec-Repository nennt außerdem einen Origin Trial in Edge 150, experimentelle Unterstützung in Leo von Brave und ChatGPT Desktop als unterstützt. Firefox und Safari tauchen dort nur als Links zu Standards-Position- und Tracking-Issues auf.

Warum registriert ein WebMCP-Skript, das für navigator.modelContext geschrieben wurde, nichts?

Weil es dieses Objekt in einem aktuellen Chrome nicht gibt und weil ein sorgfältiges Skript ein fehlendes Objekt als „nicht unterstützt“ behandelt und sich still beendet. Meines hat genau das getan. Hier ist der Kern dessen, was ich am 18. April ausgeliefert habe, neu formatiert und ohne das äußere try:

js
function register() {
  if (done) return true;
  var mc = typeof navigator !== 'undefined' && navigator.modelContext;
  if (!mc) return false;
  if (typeof mc.registerTool === 'function') {
    tools.forEach(function (t) { try { mc.registerTool(t); } catch (e) {} });
  }
  if (typeof mc.provideContext === 'function') {
    try { mc.provideContext({ tools: tools }); } catch (e) {}
  }
  done = true;
  return true;
}

Darum herum lag eine Polling-Schleife im 50-ms-Takt, die es zehn Sekunden lang immer wieder versucht hat, dazu Listener auf DOMContentLoaded, readystatechange und load. Die Commit-Messages von diesem Tag sagen, warum: Ich habe erwartet, dass der Shim eines Scanners navigator.modelContext einschleust, nachdem mein Skript schon gelaufen ist, also habe ich den Versuch so lange wiederholt, bis der Shim das tun würde. Das Log zeigt vier Commits in elf Minuten. Der erste hat die Tools hinzugefügt, die nächsten drei haben die Registrierung umgebaut, damit die Abfrage eines Scanners sie sieht. Meine Notizen vom selben Tag halten fest, wie das ausging: Der Scanner hat weiterhin keine registrierten Tools gemeldet, und ich habe das Skript dringelassen.

Das ist der Teil, den ich anders machen würde. Eine Browser-Implementierung trudelt nicht irgendwann während des Seitenladens ein. Mit einem Flag oder einem Trial-Token in der Response ist document.modelContext da, bevor das erste Skript läuft. Das Polling war auf einen Checker zugeschnitten, in den ich nicht hineinsehen konnte, der Checker hat nie etwas bestätigt, und ein Browser war zu keinem Zeitpunkt beteiligt. In Chrome 155 mit aktiviertem WebMCP ist typeof navigator.modelContext gleich undefined, die Schleife gibt nach zehn Sekunden auf, und nichts ist registriert. Kein Fehler und keine Warnung.

Ich kann nur für die Version bürgen, die ich getestet habe. Ich habe nicht verifiziert, in welchem Chrome-Release der Name auf navigator aufgehört hat zu funktionieren.

Wie registriert man ein WebMCP-Tool mit document.modelContext.registerTool?

Prüf per Feature Detection auf document.modelContext, ruf registerTool() einmal pro Tool auf und behandle das Promise, das zurückkommt. Das ist der Ersatz für das Skript oben, getestet wie abgedruckt (die zurückgegebenen Daten sind für das Listing gekürzt):

js
(function () {
  var mc = document.modelContext;
  if (!mc || typeof mc.registerTool !== 'function') return;
 
  var empty = { type: 'object', properties: {} };
  var readOnly = { readOnlyHint: true };
 
  var tools = [
    {
      name: 'get_contact',
      description: 'Return contact information for the person behind this site.',
      inputSchema: empty,
      annotations: readOnly,
      execute: async function () {
        return JSON.stringify({ email: 'max@nardit.com', site: 'https://max.nardit.com' });
      },
    },
    {
      name: 'list_products',
      description: 'List the products built by the site owner, with canonical URLs and status.',
      inputSchema: empty,
      annotations: readOnly,
      execute: async function () {
        return JSON.stringify([{ name: 'Beetroot', url: 'https://max.nardit.com/beetroot', status: 'active' }]);
      },
    },
  ];
 
  tools.forEach(function (tool) {
    mc.registerTool(tool).catch(function (err) {
      console.warn('WebMCP: ' + tool.name + ' not registered: ' + err.name);
    });
  });
})();

Gegenüber der April-Version sind die Unterschiede klein, und die meisten davon scheitern lautlos.

Es gibt kein Polling. Fehlt document.modelContext in der ersten Zeile, dann bietet der Browser dieser Seite kein WebMCP an. Die eine Reihenfolgeregel, die bleibt: Wenn du ein Origin-Trial-Token per Skript einfügst, registriere danach.

registerTool() gibt ein Promise zurück, ein try um den Aufruf fängt also nichts. Eine zweite Registrierung unter demselben Namen wurde in meinem Lauf mit InvalidStateError: Duplicate tool name abgelehnt. Die Spec lehnt außerdem einen leeren Namen oder eine leere Beschreibung und ein ungültiges Schema ab. Ohne .catch() enden diese Fälle als Unhandled Rejections.

Es gibt kein unregisterTool(). Ein Tool lebt, bis sein AbortSignal ausgelöst wird:

js
var controller = new AbortController();
// the signal has to go in with the first registration of this name
await document.modelContext.registerTool(
  {
    name: 'get_cart',
    description: 'Return the items in the current cart.',
    inputSchema: { type: 'object', properties: {} },
    annotations: { readOnlyHint: true },
    execute: async function () { return JSON.stringify([]); },
  },
  { signal: controller.signal }
);
// later, for example when the user logs out:
controller.abort();

Nach abort() war das Tool in meinem Test aus getTools() verschwunden. Relevant ist das für eine Single-Page-App, die Tools pro View registriert.

Annotationen lohnen sich auch bei trivialen Tools. readOnlyHint steht standardmäßig auf false, und Chromes Security-Seite sagt, der Hinweis helfe einem Agenten bei der Entscheidung, wann er den Nutzer um Bestätigung bittet. Was der Agent damit macht, bleibt Policy des Agenten. Der Hinweis verspricht also nichts, aber ein Read-only-Tool, das das nicht sagt, hat dem Agenten weniger mitgeteilt, als es könnte. Die beiden anderen Sicherheitshinweise sind untrustedContentHint für Ausgaben, die nutzergenerierten Text oder Text von Dritten enthalten, und consequentialHint für Aktionen wie eine Zahlung. Die Seite zur imperativen API führt eine vierte Annotation auf, debugging, ab Chrome 156.

In einem Cross-Origin-iframe ist die Registrierung standardmäßig aus. Die Spec stellt die API hinter eine Permissions Policy namens tools mit der Standard-Allowlist 'self', die einbettende Seite muss sie also mit allow="tools" freigeben.

Wie testet man WebMCP-Tools ohne Agenten?

Nimm die API der Seite selbst: getTools() listet die Tools auf, die dem Dokument zur Verfügung stehen, die eigenen und die von berechtigten untergeordneten Frames, und executeTool() führt eines aus. Die Browser-Konsole reicht also. Für lokale Arbeit nennt Chromes Übersicht das Flag chrome://flags/#enable-webmcp-testing. Ich habe von der Kommandozeile aus gearbeitet, und dort hat der Feature-Schalter unten die API freigelegt. Gelaufen ist das bei mir headless über das DevTools-Protokoll. Mit sichtbarem Fenster ist der Aufruf derselbe, er zeigt auf einen beliebigen statischen Server auf einem lokalen Port (die Seite braucht einen sicheren Kontext, und localhost zählt):

bash
google-chrome --version
# Google Chrome 155.0.8059.39
 
google-chrome --enable-features=WebMCPTesting \
  --user-data-dir=/tmp/webmcp-probe http://localhost:8765/

Dann, in der Konsole der getesteten Seite, dieselben Aufrufe, die ich über das Protokoll geschickt habe:

js
const tools = await document.modelContext.getTools();
console.log(tools.map((t) => t.name));
const tool = tools.find((t) => t.name === 'get_contact');
if (tool) console.log(await document.modelContext.executeTool(tool, {}));

Dieselben Aufrufe habe ich gegen drei Seiten gemacht. Ohne den Feature-Schalter existiert in diesem Build weder document.modelContext noch navigator.modelContext. Mit ihm:

SeitegetTools()
die Registrierungslogik vom April auf einer lokalen Seite[] nach 12 Sekunden
das Skript aus dem vorigen Abschnittget_contact, list_products
die Startseite dieser Website am 11. Oktober 2026[]

executeTool() hat in beiden Fällen, die ich probiert habe, einen String zurückgegeben: den JSON-Text unverändert, wenn execute einen String zurückgab, und die serialisierte Form, wenn es ein einfaches Objekt zurückgab. Chromes Seite zur imperativen API schreibt ihre Beispiele mit String-Ergebnissen, also gebe ich Strings zurück.

Wenn dir eine Oberfläche lieber ist: Chrome DevTools hat unter Application ein WebMCP-Panel, das die Tools im aktiven Tab auflistet und eines von Hand ausführt. Für diesen Test habe ich es nicht benutzt.

Was dieser Test beweist, ist eng gefasst: Der Browser hat die Registrierung angenommen, und die Funktion gibt zurück, was du erwartest. Er sagt nichts darüber, ob irgendein Agent sich entscheidet, sie aufzurufen.

Was kann ein Agent mit WebMCP-Tools tun, und was nicht?

Ein Agent kann deine Funktionen aufrufen, solange deine Seite in einem unterstützenden Browser offen ist, und in diesem Satz steckt jede Grenze. Chromes Vergleichsseite sagt es ohne Umschweife:

text
Importantly, WebMCP tools are ephemeral. They exist only when your
page is open. Once the user navigates away from your site or closes
the tab, the agent cannot access your site or take actions.

Gefunden werden die Tools nur beim Besuch der Seite. Die Übersicht nennt das als Einschränkung: Clients und Browser müssen eine Website direkt besuchen, um zu wissen, ob sie aufrufbare Tools hat. Es gibt kein Register und keine Well-known-Datei.

Außerhalb des Origin Trials gibt es nichts aufzurufen. Ein Besucher mit einem Chrome im Auslieferungszustand bekommt kein document.modelContext, außer deine Website liefert ein Trial-Token aus, über einen Origin-Trial-Response-Header oder das gleichwertige meta-Tag, wie es Chromes Leitfaden zu Origin Trials beschreibt. Diese Website hat keines ausgeliefert, als ich am 11. Oktober nachgesehen habe. Das korrigierte Skript allein würde für diesen Besucher also nichts ändern. In dem Build, den ich getestet habe, sind die Tools nur mit eingeschaltetem Testing-Feature erschienen.

Die Tool-Liste ist eine Schnittstelle, keine Grenze. Der Security-Abschnitt der Spec geht von der Annahme aus, dass der Agent die Session des Nutzers bereits hat:

text
Identity inheritance: Agents are able to inherit user identity and
authentication context from the browser. When an agent visits a
website, it carries the user’s logged-in credentials and session state.

Chromes Security-Seite ergänzt, dass eine Extension mit Host-Berechtigung die Seite auch ohne WebMCP manipulieren kann, indem sie eigenes JavaScript ausführt. Ein paar aufgeräumte Tools in einer App mit eingeloggtem Nutzer zu registrieren, schränkt also nicht ein, was ein Agent in diesem Tab erreichen kann. Es gibt einem kooperativen Agenten einen besseren Weg und lässt die Frage, wer die Session hält, genau dort, wo sie war. Die Autorisierung gehört weiterhin auf den Server hinter jedem Tool.

Beim Headless-Einsatz durch Agenten, der etwas anderes ist als der Headless-Test oben, setzen die Quellen unterschiedliche Schwerpunkte. Das Repository hat am 4. September eine Zeile ergänzt, laut der Headless-Anwendungsfälle im Scope sind (#296). Chromes Übersicht sagt, Tools headless auszuführen sei vielleicht möglich, die API sei aber in erster Linie für lokale Browser-Workflows mit einem Menschen in der Schleife entworfen. Ich lese das als: erlaubt, aber nicht das Ziel.

WebMCP oder ein Remote-MCP-Server für eine öffentliche Read-only-Website?

Für öffentliche Read-only-Daten ist der MCP-Endpunkt derjenige, den ein Agent heute erreichen kann, und WebMCP ist das, was sinnvoll wird, sobald es einen eingeloggten Nutzer und Seitenzustand gibt. Am 8. Oktober habe ich einen kleinen Read-only-MCP-Server unter /mcp auf diese Website gestellt, der dieselben Kontakt- und Produktdaten ausliefert, dazu eine Artikelsuche. Nebeneinander:

WebMCP-ToolsRemote-MCP-Server
Erreichbar, wenndie Seite in einem unterstützenden Browser offen istjederzeit, über HTTP
Discoverydurch Besuchen der Seiteeine URL in der Konfiguration eines Clients oder eine Server Card (noch ein Entwurfsvorschlag)
Identität des Aufrufersdie Browser-Session des Nutzerswas der Endpunkt verlangt (meiner: nichts, er ist öffentlich)
Sieht Seitenzustandja: DOM, Warenkorb, ungespeichertes Formularnein
VerfügbarkeitOrigin Trial, Flag, ein paar ClientsMCP-Clients, die Remote-HTTP-Server unterstützen

Chrome bringt es auf die Formel "MCP is for backend" und "WebMCP is for frontend" und empfiehlt, beides zu nutzen. Für eine App trägt diese Lesart. Ein Checkout, ein Sitzplan, ein halb ausgefülltes Formular: Dieser Zustand lebt im Tab, und ein Tool, das ihn von der Seite liest, erspart es, die Session auf einem Server nachzubauen.

Eine Website wie meine hat keinen solchen Zustand. Auf der echten Website gibt get_contact jedem dieselben sieben Felder zurück, und nichts daran braucht einen Tab. Meine Lesart: Für eine öffentliche Read-only-Website verdient sich WebMCP seinen Platz als Experiment und nicht als die Integration, denn dieselben Daten hinter einem schlichten HTTP-Endpunkt bedienen einen Agenten, der die Seite nie geöffnet hat. Jedes registrierte Tool bringt außerdem einen Namen, eine Beschreibung und ein Schema mit, und wenn ein Client diese an das Modell weiterreicht, kosten sie Kontext, denselben Preis, den jeder Tool-Katalog zahlt. Ich habe außerdem argumentiert, dass ein Agent zu dem Tool greift, dessen Ergebnis er vorhersagen kann. Das spricht für zwei klare Tools statt einer langen Liste.

Sollte eine Website WebMCP-Tools jetzt registrieren oder abwarten?

Registriere jetzt nur, wenn du auch die Wartung übernimmst, denn die Tabelle oben zählt fünf Umbauten der API in sieben Monaten, und der eine, der mein Skript erwischt hat, ist lautlos gescheitert. Hat die Website echten Zustand pro Nutzer und willst du, dass Agenten-Traffic durch Funktionen läuft, die du geschrieben hast, statt durch geratene Klicks, dann ist der Origin Trial ein vernünftiger Ort zum Lernen. Besteht die Website aus öffentlichen Inhalten, liefere zuerst den HTTP-Endpunkt aus und behandle WebMCP als optional.

So oder so: Teste gegen einen Browser und nicht gegen einen Detektor. getTools() in einem Chrome mit gesetztem Flag dauert eine Minute, und in einem frischen Profil gibt es keinen Shim, der anstelle des Browsers antwortet. Prüf per Feature Detection auf das Objekt, das der aktuelle Entwurf nennt, und auf nichts Älteres, denn in dem Build, den ich getestet habe, ist ein Fallback auf navigator.modelContext oder provideContext() toter Code, der das Skript kompatibler aussehen lässt, als es ist. Und versieh die Integration mit einem Datum. Der Entwurf trug das Datum 9. Oktober 2026, als ich ihn gelesen habe, und die deklarative API hatte die Spec am Tag davor verlassen.

Die Behauptung, die ich nur unter Vorbehalt übernehmen würde, ist die aus Chromes Februar-Post: dass ein direkter Kanal "eliminates ambiguity" (Mehrdeutigkeit beseitigt) und bei der Zuverlässigkeit die rohe DOM-Steuerung schlägt. Das mag durchaus so sein, und der Post liefert keine Messungen. Was ich gemessen habe, ist kleiner und weniger schmeichelhaft: Eine Registrierung, die ich im April für einen Scanner geschrieben habe, der nie Ja gesagt hat, die ich trotzdem ausgeliefert und stehen gelassen habe, hat nichts angeboten, als ich zum ersten Mal einen Browser gefragt habe.

Diskussion

Hier gibt es keine Kommentarspalte. Diskussionen laufen auf X.

Max Nardit

Max Nardit

@mnardit

Weitere Artikel

Claude Code Skills als Lieferkette: 2,2 Millionen Übernahmen, keine Registry und ein Scanner, der den Quelltext liest, während Python den Cache ausführt

Einen Skill zu lesen, bevor du ihn ausführst, ist der Standardrat, und er hat zwei Lücken. Ein kopierter Skill hat keinen Update-Kanal, die Kopie, die du gelesen hast, kann sich also unbemerkt von ihrer Quelle entfernen. Und bei einem Skill, der Python mitbringt, ist die Datei, die du liest, nicht immer die Datei, die der Interpreter lädt.

Claude Code vs Codex: sieben Arten, wie Entwickler die Arbeit zwischen beiden aufteilen, statt sich für eines zu entscheiden

Die Übergaben sind inzwischen von den Herstellern dokumentiert: ein Review-Befehl, ein Session-Transfer, ein Importer in jede Richtung. Welches Tool welche Aufgabe bekommen soll, steht nirgends. Sieben Aufteilungen, jede bis zu ihrem Ursprung zurückverfolgt und als dokumentiert oder berichtet markiert, dazu die Sandbox-Voreinstellungen, die sich unterscheiden, und die Integrationen, die dieses Jahr entfernt wurden.