bonnieSupport
Sign up7 days free

← Help center

MCP server integration (for developers)

This guide is for developers and technical partners. It explains how to build an MCP server that Bonnie can discover and call as external tools, so you can extend what Bonnie does on the phone and over WhatsApp.

If you are a restaurant looking to connect a reservation system, you do not need this page. See Reservation systems and Connect your reservation system instead.

Overview

What is an MCP server, and why it matters for Bonnie

A Model Context Protocol (MCP) server is a lightweight service that exposes callable "tools" over JSON-RPC, discoverable via tools/list and executable via tools/call. In our setup, Bonnie acts as the MCP client: at the start of a call or chat she lists the available tools, applies scope rules, and then invokes the right tool in-flow. This lets you cleanly package capabilities (reservations, CRM actions, payments, delivery lookups, calendars, or custom business logic) behind clear JSON Schemas and predictable responses.

How this expands Bonnie's phone and WhatsApp skills

By connecting Bonnie to one or more MCP servers, you can safely extend what she does across third-party platforms without changing Bonnie's core. Tools can be scoped to a call state (setup, in-progress, completed) and a channel (phone, whatsapp), so the right actions are available at the right moment. For example: auto-fetching customer context during setup, checking table availability mid-call, or sending a WhatsApp confirmation after a booking. Results can be structured JSON, so Bonnie can reliably summarise outcomes to the caller, update systems, or trigger follow-up messages. Authentication and headers carry restaurant- and conversation-specific context, keeping integrations secure and targeted while staying simple to adopt.

Concepts

Architecture and end-to-end flow

  1. Assistant configuration registers one or more MCP servers.
  2. On startup of an interaction, Bonnie lists tools from each configured server and normalises them to OpenAI-style function tools.
  3. Availability is computed by merging server-level defaults (from the assistant config) with per-tool meta scoping (from your server).
  4. At runtime, Bonnie selects the eligible tools for the current call state and channel, and calls them via tools/call.
  5. Responses can be plain text or structured data. Structured data is preferred when available.

Building a server (requirements)

Scopes for tools (call state and channel)

Adding tool metadata (without FastMCP)

When building an MCP server without FastMCP, you can add Bonnie-specific metadata by including a meta field in the tool definitions returned by tools/list. This enables scoping and pre-execution feedback.

JSON-RPC response format

In your tools/list response, include a meta object on each tool:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "check_reservation",
        "description": "Check an existing reservation by confirmation number",
        "inputSchema": {
          "type": "object",
          "properties": {
            "confirmation_number": {
              "type": "string",
              "description": "The reservation confirmation number"
            }
          },
          "required": ["confirmation_number"]
        },
        "meta": {
          "bonnie_feedback": [
            "Let me look that up for you",
            "One moment while I check your reservation"
          ],
          "bonnie_states": ["in-progress"],
          "bonnie_channels": ["phone", "whatsapp"]
        }
      }
    ]
  }
}

Meta field reference

Field Type Description
bonnie_states string[] Limit tool availability to specific call states: setup, in-progress, completed. If omitted, server-level defaults apply (see assistant config).
bonnie_channels string[] Limit tool availability to specific channels: phone, whatsapp. If omitted, server-level defaults apply.
bonnie_feedback string[] A list of feedback messages. One is randomly selected and may be played to the caller before the tool executes. Always provide messages in English. Bonnie auto-translates as needed. Playback is not guaranteed if the model already includes feedback in its response.

Notes

Request context your server receives

Bonnie sends request context as HTTP headers.

Servers should ignore headers they do not recognise.

Discovery for UI configuration

Configure Bonnie to use your server (without UI / discovery)

Add an mcp_servers entry to your assistant configuration:

{
  "mcp_servers": [
    {
      "url": "https://mcp.example.com/mcp",
      "alias": "example",
      "type": "streamable_http",
      "auth_token": "abcd1234",
      "headers": { "X-Tenant": "acme-eu" },
      "config": { "region": "eu-west" },
      "call-states": ["in-progress"],
      "channels": ["phone", "whatsapp"],
      "timeout-ms": 1500
    }
  ],
  "mcp_global_timeout_ms": 15000
}

Field reference:

Example server walkthrough

Location: docs/mcp/server/example/python/app.py.

Run locally (FastMCP Python example):

python -m pip install fastmcp pytz
python docs/mcp/server/example/python/app.py

Testing with MCP Inspector

UI mode:

npx @modelcontextprotocol/inspector --transport http --serverUrl http://localhost:8080/mcp

CLI mode:

# List tools
npx @modelcontextprotocol/inspector --cli --transport http http://localhost:8080/mcp --method tools/list | cat

# Call a tool
npx @modelcontextprotocol/inspector --cli --transport http http://localhost:8080/mcp --method tools/call \
  --tool-name current_datetime --tool-arg timezone=Europe/Amsterdam | cat

Best practices

Appendices

Example well-known discovery JSON:

{
  "name": "Bonnie MCP Example Server",
  "endpoints": { "mcp": "https://example.com/mcp" },
  "auth": { "required": false },
  "config_schema": {
    "type": "object",
    "properties": {
      "restaurant_id": { "type": "string", "description": "Unique identifier of the restaurant" }
    },
    "required": ["restaurant_id"]
  }
}

Scope defaults: if no per-tool meta is provided, Bonnie defaults to state in-progress and channels phone and whatsapp. The assistant config can further constrain availability.

Example app (Python)

from datetime import datetime, timezone as dt_timezone
from typing import Optional, Any, Dict

import json
from urllib.parse import urlencode
from urllib.request import urlopen
import pytz
from fastmcp import FastMCP
from fastmcp.server.dependencies import get_http_request, get_http_headers
from starlette.requests import Request
from starlette.responses import JSONResponse

mcp = FastMCP("Bonnie MCP Example Server (Python)")

@mcp.tool
def current_datetime(timezone: Optional[str] = None) -> str:
    """Return the current date/time in ISO format."""
    if timezone:
        try:
            tz = pytz.timezone(timezone)
            now = datetime.now(tz)
        except Exception:
            now = datetime.now(dt_timezone.utc)
    else:
        now = datetime.now(dt_timezone.utc)
    return now.isoformat()


@mcp.tool
def request_context() -> Dict[str, Any]:
    """Return request headers."""
    try:
        req = get_http_request()
        headers = dict(req.headers) if req else {}
        return {"headers": headers}
    except Exception:
        return {"error": "no_request"}


@mcp.tool
def validate_api_key() -> Dict[str, Any]:
    """Validate API key by checking if Bearer token matches 'test-token'."""
    try:
        headers = get_http_headers()
        auth_header = headers.get("authorization", "")

        if not isinstance(auth_header, str) or not auth_header.lower().startswith("bearer "):
            return {
                "valid": False,
                "error": "missing_bearer_token",
                "message": "Authorization header with Bearer token is required."
            }

        # Extract token from "Bearer <token>"
        token = auth_header.split(" ", 1)[1].strip()

        if token == "test-token":
            return {
                "valid": True,
                "message": "API key is valid.",
                "token": token
            }
        else:
            return {
                "valid": False,
                "error": "invalid_token",
                "message": "Invalid API key provided.",
                "received_token": token
            }

    except Exception as exc:  # noqa: BLE001
        return {
            "valid": False,
            "error": "validation_failed",
            "message": f"Failed to validate API key: {str(exc)}"
        }


def _get_city_coordinates(city: str) -> Optional[Dict[str, Any]]:
    """Get latitude and longitude for a city using Open-Meteo geocoding."""
    params = {"name": city.strip(), "count": 1, "language": "en", "format": "json"}
    url = "https://geocoding-api.open-meteo.com/v1/search?" + urlencode(params)

    try:
        with urlopen(url, timeout=10) as resp:
            body = resp.read().decode("utf-8")
        data = json.loads(body)

        if data.get("results") and len(data["results"]) > 0:
            result = data["results"][0]
            return {
                "latitude": result.get("latitude"),
                "longitude": result.get("longitude"),
                "name": result.get("name"),
                "country": result.get("country"),
            }
    except Exception:
        pass

    return None


@mcp.tool(
    meta={
        "bonnie_feedback": [
            "Let me check the weather for you",
            "One moment while I get the forecast",
        ]
    }
)
def get_weather(city: Optional[str] = None) -> Dict[str, Any]:
    """Get current weather for a city using Open-Meteo API."""
    # - Free API, no API key required
    # - Uses Open-Meteo weather service: https://open-meteo.com/
    # - Only parameter is `city` (e.g., "Amsterdam")
    if not city or not isinstance(city, str) or not city.strip():
        return {"error": "missing_city", "message": "Provide a city name."}

    # Get coordinates for the city
    location_data = _get_city_coordinates(city.strip())
    if not location_data:
        return {"error": "city_not_found", "message": f"Could not find coordinates for city: {city.strip()}"}

    # Get weather data using coordinates
    params = {
        "latitude": location_data["latitude"],
        "longitude": location_data["longitude"],
        "current": (
            "temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,rain,"
            "showers,snowfall,weather_code,cloud_cover,pressure_msl,surface_pressure,wind_speed_10m,"
            "wind_direction_10m,wind_gusts_10m"
        ),
        "timezone": "auto"
    }
    url = "https://api.open-meteo.com/v1/forecast?" + urlencode(params)

    try:
        with urlopen(url, timeout=10) as resp:
            body = resp.read().decode("utf-8")
        raw = json.loads(body)
    except Exception as exc:  # noqa: BLE001
        return {"error": "weather_fetch_failed", "message": str(exc)}

    current = raw.get("current") or {}

    # Map weather codes to descriptions
    weather_code = current.get("weather_code", 0)
    weather_descriptions = {
        0: "Clear sky",
        1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast",
        45: "Fog", 48: "Depositing rime fog",
        51: "Light drizzle", 53: "Moderate drizzle", 55: "Dense drizzle",
        56: "Light freezing drizzle", 57: "Dense freezing drizzle",
        61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain",
        66: "Light freezing rain", 67: "Heavy freezing rain",
        71: "Slight snow fall", 73: "Moderate snow fall", 75: "Heavy snow fall",
        77: "Snow grains",
        80: "Slight rain showers", 81: "Moderate rain showers", 82: "Violent rain showers",
        85: "Slight snow showers", 86: "Heavy snow showers",
        95: "Thunderstorm", 96: "Thunderstorm with slight hail", 99: "Thunderstorm with heavy hail"
    }

    condition = weather_descriptions.get(weather_code, "Unknown")

    return {
        "city": location_data["name"],
        "country": location_data["country"],
        "temperature_c": current.get("temperature_2m"),
        "apparent_temperature_c": current.get("apparent_temperature"),
        "humidity": current.get("relative_humidity_2m"),
        "condition": condition,
        "weather_code": weather_code,
        "wind_speed_kmh": current.get("wind_speed_10m"),
        "wind_direction": current.get("wind_direction_10m"),
        "pressure_hpa": current.get("pressure_msl"),
        "is_day": bool(current.get("is_day", 0)),
        "precipitation_mm": current.get("precipitation", 0),
        "cloud_cover": current.get("cloud_cover"),
    }

@mcp.tool(
    meta={
        "bonnie_channels": ["whatsapp"],
    }
)
def get_random_number(min_value: int = 0, max_value: int = 100) -> Dict[str, Any]:
    """Return a random integer between min_value and max_value (inclusive)."""
    # - Example of a tool that will only be listed for WhatsApp
    if min_value > max_value:
        min_value, max_value = max_value, min_value
    import random
    return {"number": random.randint(min_value, max_value)}


@mcp.tool(
    meta={
        "bonnie_states": ["setup"],
    }
)
def get_user_profile() -> Dict[str, Any]:
    """Return static prompt variables for pre-call setup."""
    req = get_http_request()
    headers = dict(req.headers) if req else {}


    return {
        "prompt_variables": {
            "first_name": "Bonnie",
            "last_name": "Clyde",
            "caller_id": headers.get("x-bonnie-caller-id", ""),
            "destination_id": headers.get("x-bonnie-destination-id", ""),
        }
    }


# Well-known discovery endpoint
@mcp.custom_route(path="/.well-known/mcp-server-info", methods=["GET"])
async def mcp_server_info(request: Request) -> JSONResponse:  # noqa: ARG001
    return JSONResponse(
        {
            "name": "Bonnie MCP Example Server (Python)",
            "description": (
                "This is an example MCP server for Bonnie using Open-Meteo weather API. "
                "It is used to test the MCP integration."
            ),
            "alias": "bonnie-mcp",
            "author": "Bonnie",
            "icon": "https://bonnie.co/wp-content/uploads/2025/08/logomark-bonnie-default.svg",
            "tags": ["mcp", "example", "python"],
            "config_schema": {
                "type": "object",
                "properties": {
                    "restaurant_id": {
                        "type": "string",
                        "description": "Unique identifier of the restaurant",
                    }
                },
                "required": ["restaurant_id"],
            },
            "endpoints": {"mcp": "/mcp"},
            "auth": {"required": False},
        }
    )


if __name__ == "__main__":
    # Streamable HTTP transport (single MCP endpoint)
    mcp.run(transport="http", host="0.0.0.0", port=8080, path="/mcp")

Need help or want this enabled?

If you want to register an MCP server for your assistant, or you are building one and need a hand, reach our team from Get help.