> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mcp-b.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Build your first tool

> Create a single WebMCP tool on a plain HTML page and verify it works, with no build step or framework.

In this tutorial, we will create a plain HTML page that registers one WebMCP tool and then verify that the tool works by calling it from the browser console. By the end, you will have a working page with a tool that browser-side WebMCP consumers can discover and call. Desktop agents need a bridge such as the local relay.

## Prerequisites

* A text editor
* A modern web browser (Chrome, Edge, Firefox, or Safari)
* A local static server, such as Python's built-in HTTP server

No build step or framework is required. Serve the page from `localhost` because
WebMCP rejects tool execution from opaque `file:` origins.

## What we will build

A single HTML page that:

1. Loads the tool-only `@mcp-b/webmcp-polyfill` runtime via a script tag
2. Registers a tool called `get-page-title`
3. Displays confirmation in the page when the tool is ready

<Steps>
  <Step title="Create the HTML file">
    Create a new file called `index.html` and paste in this starting point:

    ```html title="index.html" theme={null}
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <title>My First WebMCP Tool</title>
        <script src="https://unpkg.com/@mcp-b/webmcp-polyfill@latest/dist/index.iife.js"></script>
      </head>
      <body>
        <h1>My First WebMCP Tool</h1>
        <p id="status">Loading...</p>

        <script>
          // We will register our tool here in the next step.
        </script>
      </body>
    </html>
    ```

    The `<script>` tag in the `<head>` loads `@mcp-b/webmcp-polyfill`, which installs its WebMCP-compatible tool surface on `document.modelContext`. No import statements or bundler needed.
  </Step>

  <Step title="Register a tool">
    Replace the empty `<script>` block at the bottom of the page with:

    ```html title="index.html" theme={null}
    <script>
      void document.modelContext
        .registerTool({
          name: 'get-page-title',
          description: 'Get the current page title',
          inputSchema: { type: 'object', properties: {} },
          async execute() {
            return {
              content: [{ type: 'text', text: document.title }],
            };
          },
        })
        .then(() => {
          document.getElementById('status').textContent = 'Tool "get-page-title" registered.';
        })
        .catch((error) => {
          document.getElementById('status').textContent = `Registration failed: ${error.message}`;
        });
    </script>
    ```

    This registers a single tool on `document.modelContext`. The tool returns the current page title. The `execute` function is what runs when a consumer calls the tool.
  </Step>

  <Step title="Serve the page on localhost">
    From the directory containing `index.html`, start a static server:

    ```bash title="Terminal" theme={null}
    python3 -m http.server 8000
    ```

    Open `http://localhost:8000` in your browser. You should see:

    ```text title="Expected page" theme={null}
    My First WebMCP Tool
    Tool "get-page-title" registered.
    ```

    If the status still says "Loading...", open the browser console (F12) and check for errors.
  </Step>

  <Step title="Verify the tool from the console">
    Open the browser console (F12, then click the Console tab). Type the following:

    ```javascript title="Browser console" theme={null}
    const modelContext = document.modelContext;
    const tools = await modelContext.getTools();
    console.log(tools);
    ```

    You should see an array containing your tool:

    ```javascript title="Browser console" theme={null}
    [
      {
        name: 'get-page-title',
        description: 'Get the current page title',
        inputSchema: { type: 'object', properties: {} },
      },
    ];
    ```

    Now call the tool:

    ```javascript title="Browser console" theme={null}
    const tool = tools.find((candidate) => candidate.name === 'get-page-title');
    if (!tool) throw new Error('get-page-title is not registered');

    const executeTool = modelContext.executeTool;
    if (typeof executeTool !== 'function') {
      throw new Error('This runtime does not expose a compatible executeTool method');
    }

    const result = await executeTool.call(modelContext, tool, '{}');
    console.log(result === null ? null : JSON.parse(result));
    ```

    The output should be an object containing your tool's response:

    ```json title="Tool result" theme={null}
    {
      "content": [{ "type": "text", "text": "My First WebMCP Tool" }]
    }
    ```

    You have registered a WebMCP tool, discovered it through `getTools()`, and executed it through the polyfill's serialized-JSON compatibility method.
  </Step>

  <Step title="Try changing the tool">
    Back in your editor, change the tool so it returns a fixed message instead of `document.title`:

    ```javascript title="Tool implementation" theme={null}
    async execute() {
      return {
        content: [{ type: "text", text: "Tool is working" }]
      };
    }
    ```

    Refresh the browser and run the `executeTool` call again from the console. Notice the text field in the response now says `"Tool is working"` instead of the page title.
  </Step>
</Steps>

## The complete page

Here is the full `index.html` for reference:

```html title="index.html" theme={null}
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>My First WebMCP Tool</title>
    <script src="https://unpkg.com/@mcp-b/webmcp-polyfill@latest/dist/index.iife.js"></script>
  </head>
  <body>
    <h1>My First WebMCP Tool</h1>
    <p id="status">Loading...</p>

    <script>
      void document.modelContext
        .registerTool({
          name: 'get-page-title',
          description: 'Get the current page title',
          inputSchema: { type: 'object', properties: {} },
          async execute() {
            return {
              content: [{ type: 'text', text: document.title }],
            };
          },
        })
        .then(() => {
          document.getElementById('status').textContent = 'Tool "get-page-title" registered.';
        })
        .catch((error) => {
          document.getElementById('status').textContent = `Registration failed: ${error.message}`;
        });
    </script>
  </body>
</html>
```

## What you learned

* `@mcp-b/webmcp-polyfill` installs its WebMCP-compatible tool surface when loaded via a script tag
* `registerTool()` returns a promise that resolves after registering a tool with a name, description, input schema, and execute function
* `document.modelContext.getTools()` discovers registered tools; this polyfill executes them through its feature-detected, serialized-JSON `executeTool()` signature
* A WebMCP callback can return any serializable value; this example returns an MCP-shaped `content` array

## Next steps

* [Build Your First React Tool](/tutorials/first-react-tool) to register tools from React components
* [Choose a Runtime](/how-to/choose-runtime) to compare the tool-only and full MCP-B runtimes
* [WebMCP resources and status](/explanation/design/spec-status-and-limitations) for current native browser guidance
* [Connect a Desktop Agent](/tutorials/desktop-agent-relay) to let Claude or Cursor call your tools
