Skip to content
All field notes

WebMCP / Tool Design / AI Agents / Developer Guide / JSON Schema / Best Practices

WebMCP Tool Design: Contracts, Permissions, and Verified Results

Design WebMCP tools around explicit intent, constrained inputs, permission boundaries, observable postconditions, and failures an agent can handle.

Matheus Reis

/ 5 min read

Updated

Code editor showing a structured browser-side tool contract

A WebMCP tool is an action contract between a page, an agent, and the signed-in user. The contract should make five things inspectable: what the action does, which current context it applies to, what inputs it accepts, which permissions gate it, and what observable result counts as success.

Clear schemas help, but schema polish alone does not make an agent reliable. The surrounding state, authorization, failure behavior, and verification matter just as much.

1. Name the action, not the interface gesture

Names should describe the supported job rather than the button or layout that happens to implement it.

Good:

  • search_records
  • set_record_status
  • add_team_member
  • request_export

Avoid:

  • click_blue_button
  • open_sidebar
  • submit_modal

An action-oriented name survives interface changes and gives the agent a clearer match to user intent.

2. State the context and limits

A useful description answers:

  1. What does the tool do?
  2. Which current record, workspace, or screen does it affect?
  3. What does it return?
  4. Which important preconditions or exclusions apply?

For example:

Change the status of the record currently open in this page. Only transitions allowed for the signed-in user are accepted. Returns the previous and resulting status after verification.

This is more useful than “Updates a record” because it defines scope and a postcondition.

3. Constrain inputs to the real action

Use JSON Schema constraints that reflect the application contract:

  • required for fields the action cannot run without;
  • enum for closed value sets;
  • minimum and maximum for bounded numbers;
  • pattern or format when the application validates them;
  • additionalProperties: false when unexpected fields should be rejected.

Constraints should come from the product’s actual rules, not a universal WebMCP style guide.

inputSchema: {
  type: "object",
  properties: {
    status: {
      type: "string",
      enum: ["active", "paused", "archived"],
      description: "The requested resulting status."
    }
  },
  required: ["status"],
  additionalProperties: false
}

4. Make the permission boundary explicit

The page must not treat an agent call as authorization. The execution handler should reuse the same server-side permissions and business rules as the human-facing interface.

The current draft supports annotations such as readOnlyHint and untrustedContentHint. These are hints, not replacements for enforcement:

annotations: {
  readOnlyHint: false,
  untrustedContentHint: false
}

Consequential, destructive, or ambiguous actions need a clear user-intent or confirmation policy outside the schema itself.

5. Define the postcondition before implementation

For a read tool, success may mean returning a bounded set of records. For a mutation, success should be an observable state after the action.

Examples:

  • requested status equals the re-read status;
  • the current interface reports the selected filter;
  • the new member appears in the authorized member list;
  • a created resource can be retrieved by its returned identifier.

If the application cannot observe the postcondition, the tool should not claim verified success.

6. Return failures an agent can act on

A useful failure includes:

  • the action that failed;
  • the stable reason or error class;
  • whether anything changed;
  • the safe next step.

Avoid converting every exception into a friendly success-like sentence. Preserve the difference between rejected, failed, and completed-but-unverified.

return {
  ok: false,
  code: "TRANSITION_NOT_ALLOWED",
  changed: false,
  message: "The signed-in user cannot archive this record."
};

Complete example

if ("modelContext" in document) {
  document.modelContext.registerTool({
    name: "set_record_status",
    title: "Set record status",
    description:
      "Change the status of the record currently open in this page. " +
      "Uses the signed-in user's existing permissions and returns the " +
      "previous and verified resulting state.",
    inputSchema: {
      type: "object",
      properties: {
        status: {
          type: "string",
          enum: ["active", "paused", "archived"]
        }
      },
      required: ["status"],
      additionalProperties: false
    },
    annotations: {
      readOnlyHint: false
    },
    async execute({ status }) {
      const before = app.currentRecord();

      if (!app.allowedTransitions(before).includes(status)) {
        return {
          ok: false,
          code: "TRANSITION_NOT_ALLOWED",
          changed: false
        };
      }

      await api.records.setStatus(before.id, status);
      const after = await api.records.get(before.id);

      return {
        ok: after.status === status,
        changed: before.status !== after.status,
        before: { id: before.id, status: before.status },
        after: { id: after.id, status: after.status },
        verified: after.status === status
      };
    }
  });
}

Test tool selection and execution separately

Handler unit tests do not show whether an agent will select the right tool. Maintain a prompt set that covers:

  • clear positive cases;
  • near-miss cases that should select a different tool;
  • requests missing required user intent;
  • invalid or adversarial arguments;
  • permission failures;
  • post-action verification failures.

Run the set repeatedly against the models and clients you actually support. Record dates and versions because agent selection behavior can change.

Anti-patterns

One tool for an entire product area

A large tool with many optional modes makes authorization, selection, and failure handling harder to inspect. Split it when the actions have different permissions or postconditions.

Hidden page assumptions

If a tool depends on the current record or visible screen, say so and scope its registration to that context.

UI control without state verification

A click or function call is an attempt. Verification requires observing the resulting state.

Tool output as trusted truth

Results may contain untrusted data. Label and handle that boundary rather than feeding arbitrary content into later agent decisions without review.

Universal heuristics

There is no evidence-based rule that every description must have a fixed sentence count or every page must expose fewer than a fixed number of tools. Measure selection quality in the real context.

kn8 product boundary

kn8 is not evidence that these WebMCP conventions produce a commercial outcome. Its own runtime uses a separate host contract: current interface state and visible targets are supplied by the connected host, only supported actions can be invoked, and the resulting state is inspected. See that bounded interaction model →

Key takeaways

  • A tool contract includes context, permissions, failures, and postconditions, not only a schema.
  • Page tools should describe supported jobs rather than interface gestures.
  • Hints do not replace server-side authorization.
  • A completed call is not a verified state change.
  • Selection quality needs repeated evaluation against representative prompts.

Primary sources

Further reading

Written by

Matheus Reis Co-founder at kn8 · Ecommerce AI

Matheus Reis is a product executive and co-founder at kn8, building the Storefront Agent for ecommerce brands. He writes about AI in retail, agentic commerce, and the future of the buying experience.

Private beta / hands-on demo

See kn8 on your storefront.

Bring us one customer request. We’ll show how kn8 answers in chat and completes the task in your storefront.

  1. 01Your storefront
  2. 02A customer request
  3. 03Live walkthrough