MCP Integration Guide
This guide is for developers who want to connect their e-commerce or business platform to Webnav.ai using the Model Context Protocol (MCP) — an open standard for exposing tools over JSON-RPC 2.0 over HTTP.
MCP mode is an Enterprise feature.
Why MCP?
MCP is the integration method for AI Actions: you run one MCP tool server and Webnav.ai discovers your tools automatically via tools/list, drives confirmation logic from their annotations (destructiveHint, readOnlyHint, etc.), and calls them over an open standard. The same confirmation flow, SKU validation, call logging, and SSRF protections apply to every tool.
Architecture
Visitor → Widget → Webnav.ai ──JSON-RPC 2.0──► Your MCP Server
◄──────────────── (tool result)- Webnav.ai calls
initializeon your MCP endpoint when the domain first loads. - It calls
tools/listto discover your available tools (with annotations). - When a visitor triggers an action, Webnav.ai calls
tools/callwith the tool name and arguments. - Write tools (
create_order,create_refund) go through a confirmation card step — the visitor must click Confirm before execution.
Canonical tools (5 standard tools)
These are the five tools Webnav.ai understands natively. You may implement a subset; all five are recommended for full e-commerce coverage.
Tool summary
| Tool | Type | Confirmation | Purpose |
|---|---|---|---|
list_products | read | no | Return product catalogue for SKU validation |
query_order | read | no | Look up order status / details |
query_logistics | read | no | Track shipment |
create_order | write | yes | Place a new order (items array, one call) |
create_refund | write | yes | Start a refund request |
list_productsis used internally by Webnav.ai to validate SKUs before showing a confirmation card. If a visitor asks for a SKU that does not appear inlist_products, the assistant asks them to pick from the real catalogue — no confirmation card is shown for invalid SKUs.
Tool specifications
list_products
{
"name": "list_products",
"description": "Return the product catalogue available for purchase.",
"inputSchema": { "type": "object", "properties": {} },
"annotations": { "readOnlyHint": true }
}Response data.products:
[
{ "sku": "PB-10000", "name": "Power Bank 10000 mAh", "price": "19.99" },
{ "sku": "CB-USBC", "name": "USB-C Cable 2m", "price": "7.99" }
]query_order
{
"name": "query_order",
"description": "Look up order status and details by order ID.",
"inputSchema": {
"type": "object",
"properties": { "order_id": { "type": "string", "description": "Order identifier" } },
"required": ["order_id"]
},
"annotations": { "readOnlyHint": true }
}Response data:
{
"order_id": "SO20260613",
"status": "paid",
"amount": "199.00",
"currency": "USD",
"items": [{ "sku": "PB-10000", "name": "Power Bank", "qty": 2 }]
}query_logistics
{
"name": "query_logistics",
"description": "Track shipment status for an order.",
"inputSchema": {
"type": "object",
"properties": { "order_id": { "type": "string" } },
"required": ["order_id"]
},
"annotations": { "readOnlyHint": true }
}Response data:
{
"order_id": "SO20260613",
"carrier": "SF Express",
"tracking_no": "SF123456789",
"status": "in_transit",
"tracks": [
{ "time": "2026-06-12 14:30", "desc": "Arrived at Shenzhen hub" },
{ "time": "2026-06-13 09:10", "desc": "Out for delivery" }
]
}create_order
{
"name": "create_order",
"description": "Place a new order. Each item must be a real product from list_products.",
"inputSchema": {
"type": "object",
"properties": {
"items": {
"type": "array",
"description": "List of items to order",
"items": {
"type": "object",
"properties": {
"sku": { "type": "string", "description": "Product SKU" },
"quantity": { "type": "integer", "minimum": 1 },
"name": { "type": "string", "description": "Product name (optional)" }
},
"required": ["sku", "quantity"]
}
},
"address": { "type": "string", "description": "Delivery address (optional)" }
},
"required": ["items"]
},
"annotations": { "destructiveHint": true, "idempotentHint": false, "title": "Place Order" }
}Response data:
{ "order_id": "SO20260614", "pay_url": "https://pay.example.com/SO20260614", "amount": "39.98" }
itemsis an array — the visitor can add multiple products in one conversation turn, and Webnav.ai callscreate_orderonce with all items. Only one confirmation card is shown.
create_refund
{
"name": "create_refund",
"description": "Start a refund request for an order.",
"inputSchema": {
"type": "object",
"properties": {
"order_id": { "type": "string" },
"reason": { "type": "string", "description": "Reason for refund (optional)" }
},
"required": ["order_id"]
},
"annotations": { "destructiveHint": true, "title": "Request Refund" }
}Response data:
{ "refund_id": "RF20260613", "status": "pending", "amount": "199.00" }Tool annotations
Annotations are metadata in tools/list that tell Webnav.ai how to handle each tool:
| Annotation | Type | Effect |
|---|---|---|
destructiveHint | bool | true → Webnav.ai always shows a confirmation card before calling this tool |
readOnlyHint | bool | true → no confirmation needed; call directly |
idempotentHint | bool | false → extra caution; may enforce stricter single-use pending |
openWorldHint | bool | true → tool may have unpredictable side effects |
title | string | Human-readable label shown on the confirmation card |
Trust model: destructiveHint is self-reported by your server. Webnav.ai does not trust annotations alone — it also applies name-based rules (create_*, refund_*, cancel_*, pay_* → always confirms) and your mcpConfirmTools list configured in the dashboard. All three layers are OR-ed together, so a write tool is never accidentally skipped even if it forgets to set destructiveHint.
Wire format
Handshake (initialize)
Webnav.ai sends this when the domain first connects:
{
"jsonrpc": "2.0",
"id": "1",
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"clientInfo": { "name": "webnav-ai", "version": "1.0" },
"capabilities": {}
}
}Your server responds:
{
"jsonrpc": "2.0",
"id": "1",
"result": {
"protocolVersion": "2024-11-05",
"serverInfo": { "name": "my-shop-mcp", "version": "0.1.0" },
"capabilities": {}
}
}If the protocol versions differ, Webnav.ai logs a versionMismatch warning (visible in Dashboard → AI Actions → Test Connection) but continues to operate.
Tool discovery (tools/list)
// request
{ "jsonrpc": "2.0", "id": "2", "method": "tools/list" }
// response
{
"jsonrpc": "2.0",
"id": "2",
"result": {
"tools": [
{
"name": "list_products",
"description": "Return product catalogue.",
"inputSchema": { "type": "object", "properties": {} },
"annotations": { "readOnlyHint": true }
},
{
"name": "create_order",
"description": "Place a new order.",
"inputSchema": { "type": "object", "properties": { "items": { "type": "array" } } },
"annotations": { "destructiveHint": true, "title": "Place Order" }
}
]
}
}Tool call (tools/call)
// request
{
"jsonrpc": "2.0",
"id": "3",
"method": "tools/call",
"params": {
"name": "query_order",
"arguments": { "order_id": "SO20260613" }
}
}
// response — canonical shape (preferred)
{
"jsonrpc": "2.0",
"id": "3",
"result": {
"ok": true,
"data": { "order_id": "SO20260613", "status": "paid", "amount": "199.00" },
"display": {
"title": "Order SO20260613",
"fields": [{ "label": "Status", "value": "Paid" }]
}
}
}
// response — MCP content-block shape (also accepted)
{
"jsonrpc": "2.0",
"id": "3",
"result": {
"content": [{ "type": "text", "text": "{\"order_id\":\"SO20260613\",\"status\":\"paid\"}" }],
"isError": false
}
}Both shapes are accepted. The canonical {ok, data, display} shape gives you richer widget card rendering.
Display rendering capabilities
The optional display object on a tool result controls how the widget renders a structured card. All fields are optional; mix them freely.
"display": {
"title": "Order SO20260613",
"subtitle": "Short helper line under the title.",
"fields": [{ "label": "Status", "value": "Paid" }],
"items": [{ "title": "Arrived at depot", "time": "2026-06-12 14:30" }],
"buttons": [{ "label": "Open order", "url": "/orders/SO20260613", "style": "primary" }],
"options": [{ "label": "AlipayPlus", "value": "alipay", "hint": "Local e-wallets" }],
"submit": { "label": "Generate order", "url": "/checkout?method={value}", "target": "_self" }
}| Field | Type | Renders as |
|---|---|---|
title | string | Card heading |
subtitle | string | Muted helper line under the title |
fields | [{label, value}] | Key–value rows |
items | [{title|desc, time?}] | Timeline list (e.g. logistics tracks) |
products | […] | Horizontally scrollable product cards |
buttons | [{label, url, style?}] | Action buttons. url opens in a new tab; a root-relative /path is prefixed with your data-base-url. style: primary (default) / secondary |
options + submit | see below | A single-choice card (radio list + one submit button) |
Single-choice card (options + submit)
Use this to let the visitor pick one option and act on it without typing. The widget renders options as radio rows; the submit button stays disabled until one is selected.
options:[{ label, value, hint? }]submit: what happens on click (urltakes precedence overprompt):submit.url— navigate the host window to this URL.{value}/{label}are substituted from the chosen option. A root-relative/pathresolves against the embedding page's origin (notdata-base-url).submit.target:_self(default) or_blank.submit.prompt— send this text as the visitor's next chat message (model-driven path;{value}/{label}substituted). Use when the next step should go back through the assistant.submit.label— button text; if omitted the widget shows a localized default.
Example: a top-up tool returns the enabled payment methods as
optionsand asubmit.urllike"/wallet?amount=5&method={value}&autopay=1". The visitor picks a method, taps the button, and the host page places the order and opens checkout — no extra chat turn or confirmation.
Authorization card (AUTH_REQUIRED)
When a user-scoped tool has no valid user token, return a business error with a display instead of failing silently:
{ "ok": false, "error_code": "AUTH_REQUIRED", "message": "Please sign in.",
"display": { "title": "Connect your account",
"subtitle": "Sign in to continue.",
"buttons": [{ "label": "Sign in", "url": "/login", "style": "primary" }] } }The widget renders this as an authorization card — both for read tools and when a write-tool confirmation fails — so the visitor gets a sign-in button rather than a bare error line.
Visitor identity forwarding (user token)
For user-specific tools (an account's orders, balance, API keys, usage…), your site issues a short-lived user token for the logged-in visitor and hands it to the widget:
window.WebnavWidget.setUserToken('YOUR_SIGNED_USER_TOKEN')Webnav.ai does not parse or store this token. On every tools/call it forwards it opaquely to your MCP endpoint as an HTTP header — by default:
X-Webnav-User-Token: <your user token>You can override the header name in Dashboard → AI Actions (e.g. to Authorization). Your MCP server verifies the token itself (e.g. validates a JWT signature) and resolves the real user.
Write tools / confirmation: the token is also re-sent on the confirmation call and bound by hash to the pending action, so a confirmed create_* executes with the same identity that initiated it. A missing/altered token at confirm time is rejected (TOKEN_MISMATCH) without executing.
Read tools that need a user but receive no valid token should return a business error (e.g.
{ok:false, error_code:"AUTH_REQUIRED"}) — the widget renders an authorization card prompting the visitor to sign in, rather than failing silently.
Error normalization
Webnav.ai maps all MCP transport and protocol errors to a unified error_code:
| Scenario | error_code |
|---|---|
| HTTP status non-2xx | MCP_HTTP_<status> (e.g. MCP_HTTP_503) |
JSON-RPC error field | MCP_RPC_ERROR |
isError: true in result | MCP_TOOL_ERROR |
| Connection timeout | MCP_TIMEOUT |
| Host unreachable / refused | MCP_UNREACHABLE |
These codes appear in Dashboard → AI Actions → Call Logs and in the error message the assistant relays to the visitor.
Your own business errors should use descriptive codes in the data.error_code field (e.g. ORDER_NOT_FOUND, OUT_OF_STOCK).
Dashboard configuration
- Go to Dashboard → AI Actions.
- Switch Mode to MCP.
- Enter your MCP Endpoint (e.g.
https://mcp.yourshop.com/mcp). - Optionally add Auth Headers (key/value pairs — values are stored encrypted and never returned in plaintext).
- Click Test Connection — Webnav.ai probes your endpoint via
initialize+tools/listand shows the discovered tool list with annotations. - In the Confirm tools panel, review the pre-selected write tools. Add any additional tool names that should require a confirmation card.
- Click Save.
Note on Test Connection: The probe is routed through the Webnav.ai backend — your MCP endpoint is never contacted directly from the browser. The same SSRF protections apply (no private/loopback addresses unless
ACTIONS_ALLOW_INSECUREis set locally).
Security
| Requirement | Why |
|---|---|
| Serve over HTTPS | Webnav.ai pins the resolved IP and verifies the certificate. |
| Use a public host | Webnav.ai refuses private/loopback/metadata addresses (SSRF protection). |
| Validate auth headers | Use the mcp_headers field to pass a bearer token or API key that your server validates on every request. |
| Make write tools idempotent | Webnav.ai sends a single confirmed call, but network retries can happen — design create_order/create_refund to be safe to receive twice. |
Do not rely on destructiveHint alone | Webnav.ai also applies name-pattern rules and mcpConfirmTools as additional safety layers. |
Demo MCP servers
The repository ships two demo MCP servers. Both implement the five canonical tools with proper annotations.
Option A — Mock shop integrated endpoint (recommended)
The demo shop (examples/shop/shop_server.py, port 4000) exposes a /mcp endpoint that shares the same order store as the storefront. Orders placed by the AI over MCP appear instantly in the shop's "My Orders" panel — the best way to see the full visitor → widget → MCP → order flow end to end.
# starts the storefront + /mcp on http://localhost:4000
WEBNAV_SECRET=webnav-mock-secret-001 ./.venv/bin/python examples/shop/shop_server.pyOptional bearer auth: set MCP_AUTH_TOKEN=<token> when starting, then add Authorization: Bearer <token> under Auth Headers in the dashboard. Unset = no auth (demo default).
Then in Dashboard → AI Actions, set mode = MCP, endpoint = http://localhost:4000/mcp, and click Test Connection.
Option B — Standalone reference server
examples/mcp_server.py (port 4100) is a minimal standalone reference with its own in-memory order store (not connected to the storefront panel). Useful for protocol-only testing.
Warning: Both are reference/demo implementations. The standalone server has no authentication or idempotency. Do not deploy them to production as-is.
./.venv/bin/python examples/mcp_server.py # starts on http://localhost:4100/mcpTest the wire protocol directly
# Handshake (replace 4000 with 4100 for the standalone server)
curl -s -X POST http://localhost:4000/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":"1","method":"initialize","params":{"protocolVersion":"2024-11-05","clientInfo":{"name":"test","version":"1"}}}'
# Discover tools
curl -s -X POST http://localhost:4000/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":"2","method":"tools/list"}'
# Call a tool
curl -s -X POST http://localhost:4000/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":"3","method":"tools/call","params":{"name":"list_products","arguments":{}}}'