HomeHome
FavoritesFavorites
Tech Blog
HomeHome
FavoritesFavorites
Tech Blog
← Back to Tech Blog

Tonita is for Agents, too!

We built Tonita to make shopping search feel natural for humans: describe what you want, refine it in conversation, search by image, and get results grounded in real apparel inventory. Now we are opening the same deep search engine to AI agents.

Tonita MCP lets LLM clients tap into Tonita's apparel search through the Model Context Protocol. Instead of relying on brittle web browsing or generic product guesses, agents can call a purpose-built shopping search stack and receive structured, inventory-grounded JSON results. This guide covers the Generic MCP server: tools, requests, responses, and JSON. Our ChatGPT app uses a separate server with interactive product card widgets.

What is Tonita MCP?

Tonita MCP is a Model Context Protocol server that exposes Tonita's apparel search to LLM clients. This guide covers the Generic MCP server (tools + JSON). Our ChatGPT app uses a separate server with interactive product card widgets.

Quick reference (for agents)

yaml
service: tonita-generic-mcp
protocol: MCP 2025-03-26 over Streamable HTTP
endpoint: https://mcp-generic.tonita.co/mcp
auth:
  header: Authorization
  format: "Bearer <your Tonita API key>"
  example: "tnk_live_kp_abc12345_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
tools:
  - tonita-search
  - tonita-get-listing-details
rate_limit:
  default_per_day: 100
  reset: UTC midnight
contact: hello+mcp@tonita.co
privacy: https://tonita.co/privacy

Get an API key

Every POST /mcp request requires one Tonita API key. There is no separate key per environment — create one key in Settings and use it everywhere below.

  1. Sign in at tonita.co
  2. Open Settings → Connect Tonita to your AI assistant
  3. Click Create a key and copy it immediately

Keys look like tnk_live_kp_abc12345_…. One active key per account. Default limit: 100 requests per UTC day.

Higher limits or partner access: hello+mcp@tonita.co

Quick Start

The fastest way to verify your key is our Python sample client. It mimics what an LLM client (like Claude) does: initializetools/listtonita-searchtonita-get-listing-details.

terminal setup
pip install requests

export MCP_API_KEY="tnk_live_kp_your_key_id_your_secret"
export MCP_URL="https://mcp-generic.tonita.co"

# Save the sample_client.py block below, then:
python sample_client.py
sample_client.py (copy and run)
#!/usr/bin/env python3
"""Minimal Tonita MCP client — get started without Claude or Cursor.

Mimics what an LLM client does on connect:
  initialize → tools/list → tonita-search → tonita-get-listing-details

Usage:
    pip install requests
    export MCP_API_KEY='tnk_live_kp_...'
    python3 sandbox/tonita/api_server/tonita_mcp/sample_client.py

Optional env vars:
    MCP_URL   Base URL (default: generic MCP on Cloud Run)
    MCP_QUERY Override the sample search query

Requires: pip install requests
"""

from __future__ import annotations

import json
import os
import sys
import uuid

import requests

DEFAULT_MCP_URL = (
    "https://mcp-generic.tonita.co"
)


def parse_sse_body(text: str) -> dict:
    for line in text.splitlines():
        if not line.startswith("data: "):
            continue
        msg = json.loads(line[len("data: ") :])
        if "result" in msg or "error" in msg:
            return msg
    raise ValueError(f"No JSON-RPC message in SSE body:\n{text[:500]}")


def mcp_call(
    base_url: str,
    api_key: str,
    method: str,
    params: dict | None = None,
    timeout: int = 120,
) -> dict:
    payload: dict = {
        "jsonrpc": "2.0",
        "id": str(uuid.uuid4()),
        "method": method,
    }
    if params is not None:
        payload["params"] = params

    resp = requests.post(
        f"{base_url.rstrip('/')}/mcp",
        json=payload,
        headers={
            "Content-Type": "application/json",
            "Accept": "application/json, text/event-stream",
            "Authorization": f"Bearer {api_key}",
        },
        timeout=timeout,
    )

    if resp.status_code == 401:
        raise SystemExit("401 unauthorized — check MCP_API_KEY")
    if resp.status_code == 429:
        body = resp.json()
        raise SystemExit(
            "429 rate limit — "
            f"used={body.get('used')} limit={body.get('limit')} "
            f"retry_after_seconds={body.get('retry_after_seconds')}"
        )
    resp.raise_for_status()

    content_type = resp.headers.get("Content-Type", "")
    if "text/event-stream" in content_type:
        return parse_sse_body(resp.text)
    return resp.json()


def call_tool(base_url: str, api_key: str, name: str, arguments: dict) -> dict:
    envelope = mcp_call(
        base_url,
        api_key,
        "tools/call",
        {"name": name, "arguments": arguments},
    )
    if "error" in envelope:
        raise RuntimeError(f"JSON-RPC error: {envelope['error']}")
    result = envelope["result"]
    if result.get("isError"):
        raise RuntimeError(f"Tool {name} failed: {result.get('content')}")
    return result


def main() -> int:
    base_url = os.environ.get("MCP_URL", DEFAULT_MCP_URL).rstrip("/")
    api_key = os.environ.get("MCP_API_KEY", "").strip()
    if not api_key:
        print(
            "Set MCP_API_KEY to your Tonita API key (tnk_live_kp_...).",
            file=sys.stderr,
        )
        return 2

    query = os.environ.get("MCP_QUERY", "white leather sneakers")

    print(f"MCP URL: {base_url}/mcp\n")

    print("1) initialize")
    init = mcp_call(
        base_url,
        api_key,
        "initialize",
        {
            "protocolVersion": "2025-03-26",
            "capabilities": {},
            "clientInfo": {"name": "sample-client", "version": "1.0"},
        },
    )
    server = init["result"]["serverInfo"]
    print(f"   server: {server.get('name')} {server.get('version', '')}")

    print("2) tools/list")
    tools_msg = mcp_call(base_url, api_key, "tools/list")
    tools = tools_msg["result"]["tools"]
    print(f"   tools: {[t['name'] for t in tools]}")

    print("3) tonita-search")
    search = call_tool(
        base_url,
        api_key,
        "tonita-search",
        {
            "product_type": "sneakers",
            "gender": "Mens",
            "retrieval_queries": [query],
        },
    )
    sc = search["structuredContent"]
    cards = sc.get("listingCards") or []
    print(f"   title: {sc.get('title')!r}")
    print(f"   results: {len(cards)}")
    for card in cards[:2]:
        print(
            f"   - {card.get('listingId')}: "
            f"{(card.get('title') or '')[:60]}"
        )
    if len(cards) > 2:
        print(f"   ... and {len(cards) - 2} more")

    if cards:
        listing_id = cards[0]["listingId"]
        print("4) tonita-get-listing-details")
        details = call_tool(
            base_url,
            api_key,
            "tonita-get-listing-details",
            {"listing_ids": [listing_id]},
        )
        entry = details["structuredContent"]["listing_details"][listing_id]
        thumb = entry.get("thumbnail_image") or {}
        print(f"   url: {entry.get('url', '')[:80]}")
        print(f"   thumbnail image_id: {thumb.get('image_id')}")

    print("\nDone.")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Example output

Illustrative only — your listings will differ.

stdout
MCP URL: https://mcp-generic.tonita.co/mcp

1) initialize
   server: tonita-mcp 1.25.0

2) tools/list
   tools: ['tonita-search', 'tonita-get-listing-details']

3) tonita-search
   title: 'white leather sneakers'
   results: 20
   - seavees.com!360921c9ade74097: Mens - Diamond Cup - White
   - greats.com!34b88f1fab7ee769: The Kingston - White White
   ... and 18 more

4) tonita-get-listing-details
   url: https://seavees.com/products/mens-diamond-cup-white?variant=42288352067658
   thumbnail image_id: 9143b224a1934df1

Done.

About the system prompt

When your MCP client connects, Tonita sends server instructions that tell the model how to use our tools. Much of this text is shared with the interactive tonita.co chat, so some lines assume a shopper is browsing product carousels on our website.

GuidanceMCP?Notes
One product type per searchYesRequired for good results
Literal, self-contained queriesYes
Call listing details before follow-upsYesModel must track listing IDs from prior tool results — no on-screen carousel
"Previously shown listing" flowsPartiallyWorks if the model saved IDs from earlier tool calls
Widget / carousel hintsNoGeneric MCP returns JSON; your client presents results

For v1, treat the instructions as search discipline (how to call the tools well), not as UI behavior.

Expected responses

Disclaimer: Catalog contents change constantly. Examples below are illustrative — your actual titles, prices, IDs, and counts will differ.

tonita-search (first 2 listings)

json excerpt — tonita-search
{
  "retrieval_queries": ["white leather low-top sneakers"],
  "total_results": 20,
  "listings": [
    {
      "listingId": "greats.com!548716d2c2d39029",
      "title": "The Royale 2.0 - Blanco",
      "currentPrice": "$118.97",
      "brandName": "GREATS",
      "storeName": "greats.com",
      "url": "https://greats.com/products/the-royale-2-0-blanco-mens?...",
      "availableSizes": ["7", "8", "8.5", "9", "10"]
    },
    {
      "listingId": "seavees.com!360921c9ade74097",
      "title": "Mens - Diamond Cup - White",
      "currentPrice": "$85.00",
      "brandName": "SeaVees",
      "storeName": "seavees.com",
      "url": "https://seavees.com/products/mens-diamond-cup-white?...",
      "availableSizes": ["8", "9", "10", "11"]
    }
  ]
}

tonita-get-listing-details

json excerpt — tonita-get-listing-details
{
  "listing_details": {
    "greats.com!548716d2c2d39029": {
      "url": "https://greats.com/products/the-royale-2-0-blanco-mens?...",
      "title": "The Royale 2.0 - Blanco",
      "one_line_description": "Minimal white leather low-top sneaker with embossed branding...",
      "current_price": {"value": 118.97, "currency": "USD"},
      "available_sizes": ["7", "8", "8.5", "9", "10", "11"],
      "thumbnail_image": {
        "image_id": "89e667d28336d70f",
        "image_url": "https://cdn.shopify.com/..."
      }
    }
  }
}

The tool's text content for listing details is a short summary (Fetched details for 1 listings). The JSON above lives in structuredContent.listing_details.

Rate limits

SettingValue
Default daily cap100 requests / key / UTC day
Counter resetUTC midnight
When exceededHTTP 429 with Retry-After

Connect your client

endpoint
https://mcp-generic.tonita.co/mcp

Cursor

.cursor/mcp.json
{
  "mcpServers": {
    "tonita": {
      "url": "https://mcp-generic.tonita.co/mcp",
      "headers": {
        "Authorization": "Bearer tnk_live_kp_your_key_id_your_secret"
      }
    }
  }
}

curl examples

setup
export TONITA_MCP_KEY="tnk_live_kp_your_key_id_your_secret"

Auth check — expected 401 without a key

curl — auth check
curl -sS -X POST \
  "https://mcp-generic.tonita.co/mcp" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

# Expected: {"error": "unauthorized"}

Search — expected listingCards

curl — tonita-search
curl -sS -X POST \
  "https://mcp-generic.tonita.co/mcp" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer $TONITA_MCP_KEY" \
  -d '{
    "jsonrpc": "2.0",
    "id": "search-1",
    "method": "tools/call",
    "params": {
      "name": "tonita-search",
      "arguments": {
        "product_type": "sneakers",
        "gender": "Mens",
        "retrieval_queries": ["white leather low-top sneakers"]
      }
    }
  }'

Listing details — use a listingId from search

curl — tonita-get-listing-details
curl -sS -X POST \
  "https://mcp-generic.tonita.co/mcp" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer $TONITA_MCP_KEY" \
  -d '{
    "jsonrpc": "2.0",
    "id": "details-1",
    "method": "tools/call",
    "params": {
      "name": "tonita-get-listing-details",
      "arguments": {
        "listing_ids": ["LISTING_ID_FROM_SEARCH"]
      }
    }
  }'

Privacy & contact

Chats
No chat history yet