labs | 01 | hello
lab 01 | ~5 min | segment 1

Fifteen lines. Your page grows a tool.

WebMCP turns a plain JavaScript function into an agent-callable tool with one call: navigator.modelContext.registerTool. No SDK, no imports, no server. You feature-detect the API, register a say_hello tool, and the same greeting a human triggers with a button is now a tool an agent can call. This page below is the lab. It is running the exact code you are about to read.

Checking for WebMCP...
step 1

Feature-detect before you rely on it.

WebMCP is early-preview. Most browsers do not have it. The first rule is: never assume navigator.modelContext exists. Check for it, and keep the human path working either way. This is the single trap that breaks the naive tutorial.

feature-detect
if (navigator.modelContext && typeof navigator.modelContext.registerTool === 'function') {
  // WebMCP is present. Register your tools.
} else {
  // Not present. The page must still work for humans.
  // Show a banner: enable chrome://flags/#enable-webmcp-testing
}
step 2

Write the fifteen lines.

Drop this in a <script> tag at the bottom of any HTML page. greet() is a normal function that updates the DOM. The button calls it. The tool's execute calls the same function and returns a result the agent can read. One function, two callers.

the whole thing
// The shared function. A human button calls it; the agent tool calls it too.
function greet(name) {
  const text = "Hello, " + (name || "world") + ".";
  document.getElementById("greeting").textContent = text;
  return text;
}

// Register the tool ONLY when WebMCP is present. Always feature-detect first.
if (navigator.modelContext && typeof navigator.modelContext.registerTool === "function") {
  navigator.modelContext.registerTool({
    name: "say_hello",
    description: "Greet a person by name and show the greeting on the page.",
    inputSchema: {
      type: "object",
      properties: { name: { type: "string", description: "The name to greet." } },
      required: ["name"]
    },
    execute: async (args) => {
      const text = greet(args && args.name);
      return { content: [{ type: "text", text: text }] };
    }
  });
}
  • greet()A plain DOM function. Nothing agent-specific about it. This is the code you already have.
  • feature-detectThe navigator.modelContext guard means this page loads fine in every browser. Only Canary-with-the-flag registers the tool.
  • nameThe server name an agent reads is the tool name. Make it a verb an agent would search for: say_hello, not handler.
  • descriptionThis is the agent's only signal for when to call the tool. Write it for the smallest model you expect to serve. Verbose is fine. Ambiguous is fatal.
  • inputSchemaA JSON Schema object. The agent reads it to know what arguments to send. required tells it which are mandatory.
  • executeAn async function. It receives the arguments, does the work by calling greet(), and returns an MCP result: { content: [{ type: "text", text }] }. That text is what the agent reads back.
step 3

Try it as a human.

Type a name and click. This is the ordinary page. No agent involved. Watch the activity log record the call with a human source tag.

live demo | this page is the labsay_hello: not registered
Say hello.

activity log

  • No calls yet. Click Greet, or invoke say_hello from the Tool Inspector.
step 4

Now call it as an agent.

Two ways to reach the agent path. If you installed the WebMCP Tool Inspector extension, open it on this page: say_hello appears in the tool list. Fire it with { "name": "Ada" } and watch the greeting change with an agent tag in the log. No extension yet? The Simulate agent call button above runs the tool's own execute locally, so you can see the agent path before Canary is set up.

Or prove registration straight from the console, no extension required:

DevTools console
// Is the API here at all?
'modelContext' in navigator          // true in Canary with the flag

// Register succeeded without throwing? Then the agent sees say_hello.
// The Tool Inspector extension is how you invoke it by name from outside.
what the agent reads back from execute
{ "content": [ { "type": "text", "text": "Hello, Ada." } ] }

what just happened

You registered a tool with fifteen lines of vanilla JavaScript and no agent SDK on the page. The browser now exposes say_hello to any agent connected through navigator.modelContext. The agent calls it inside your page, in the human's own session, with no separate auth and no scraping. Same DOM, two callers. That is the entire premise of WebMCP, and you just shipped it.

the trap you just dodged

Skipping the feature-detect. The naive version calls navigator.modelContext.registerTool at the top of the script. In any browser without WebMCP, that throws a TypeError, the script dies, and your human page breaks for everyone. The navigator.modelContext guard is not optional politeness. It is the difference between progressive enhancement and a page that only works in one preview build of one browser.