SOPSellers' Open Protocol

Business and service data

An agent has to understand what you sell before it can find you and book you.

Business and service data: Live. Product feed: Planned.

Agents read structured data

SOP turns each business into structured data: the business profile, booking and cancellation rules, and each service's price and duration. The data goes live only after you confirm each item, and you decide which services and time slots are public.

What agents read from SOP's listings
AgentSOPSellerProfile, services,prices, schedule andcancellation rulesPublishes the listingafter the businessconfirms itsearch_businessesMatching businessesand a few servicesget_businessProfile, serviceswith prices andschedules, policiesget_availabilityOpen slots with prices
Steps
  1. Seller → SOP: Profile, services, prices, schedule and cancellation rules
  2. SOP → SOP: Publishes the listing after the business confirms it
  3. Agent → SOP: search_businesses
  4. SOP → Agent: Matching businesses and a few services
  5. Agent → SOP: get_business
  6. SOP → Agent: Profile, services with prices and schedules, policies
  7. Agent → SOP: get_availability
  8. SOP → Agent: Open slots with prices

Business data

get_business returns the following:

FieldDescription
business_id, name, categories, cityBusiness ID, name, categories and city
profilePublic address, opening hours, booking policy, payment notes, parking, notes and contact
faqFrequently asked questions
quantityThe name and unit for people or places, and the maximum per booking
online_cancellationThe online cancellation cutoff
paymentPayment mode: pay at the venue or online, and the payment window
booking_moderequest_here (book here) or external_link (use the merchant's own booking system)

Get a business and its services: get_business

curl

curl -sS -X POST https://mcp.srct.tw/mcp \
  -H "User-Agent: my-agent/1.0" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"get_business","arguments":{"business_id":"coach-dunken"}}}'

Python

import json
import urllib.error
import urllib.request

BASE = "https://mcp.srct.tw"


def call(method, path, body=None, headers=None):
    req = urllib.request.Request(
        BASE + path,
        data=None if body is None else json.dumps(body).encode(),
        method=method,
        headers={"User-Agent": "my-agent/1.0", "Content-Type": "application/json", **(headers or {})},
    )
    try:
        with urllib.request.urlopen(req) as res:
            return res.status, json.loads(res.read())
    except urllib.error.HTTPError as err:  # 4xx and 5xx responses still carry a JSON body
        return err.code, json.loads(err.read())


def show(status, body):
    print("HTTP", status)
    print(json.dumps(body, ensure_ascii=False, indent=2))


MCP = {"Accept": "application/json, text/event-stream"}  # the server requires both types

show(*call("POST", "/mcp", {"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"name": "get_business", "arguments": {"business_id": "coach-dunken"}}}, MCP))

Node.js

const BASE = "https://mcp.srct.tw";

async function call(method, path, body, headers = {}) {
  const res = await fetch(BASE + path, {
    method,
    headers: { "User-Agent": "my-agent/1.0", "Content-Type": "application/json", ...headers },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  return { status: res.status, body: await res.json() };
}

function show({ status, body }) {
  console.log("HTTP", status);
  console.log(JSON.stringify(body, null, 2));
}

const MCP = { Accept: "application/json, text/event-stream" }; // the server requires both types

(async () => {
  show(await call("POST", "/mcp", { jsonrpc: "2.0", id: 4, method: "tools/call", params: { name: "get_business", arguments: { business_id: "coach-dunken" } } }, MCP));
})();

Example response

HTTP 200
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": …
      }
    ],
    "structuredContent": {
      "ok": true,
      "business": {
        "business_id": "coach-dunken",
        "name": "Dunken教練",
        "categories": [
          "coaching"
        ],
        "booking_mode": "request_here",
        "city": "台北市信義區(Taipei Xinyi)",
        "profile": {
          "city": "台北市信義區(Taipei Xinyi)",
          "address": "…",
          "hours": "每天08:00至20:00",
          "booking_policy": "取消與改期需在課程開始24小時前(經AI助理線上取消也是開始前24小時截止)。遲到15分鐘將直接結束當次課程,且無法退費。",
          "payment": "現金或轉帳。課程一次購買10堂NT$18,000(每堂折合NT$1,800)。",
          "notes": "請自行準備合適的訓練服裝,至少提早10分鐘抵達教室。",
          "contact": "…"
        },
        "faq": [],
        "quantity": {
          "label": "人數",
          "unit": "位",
          "max_per_booking": 1
        },
        "online_cancellation": {
          "closes_minutes_before_start": 1440,
          "text": "Online cancellation closes 24 hours before the start time; after that, contact the business directly."
        },
        "pending_hold": {
          "minutes": 10,
          "text": "Bookings that need the business's confirmation are held for 10 minutes. If the business does not confirm by then, the hold is released automatically and the customer can choose another slot."
        },
        "payment": {
          "mode": "venue",
          "text": "Pay at the venue."
        }
      },
      "services": [
        {
          "service_id": "first_assessment",
          "name": "初次體能檢查(含InBody一次)",
          "category": "一對一",
          "summary": "第一次上課:基本動作與體能檢查,並免費做一次InBody身體組成量測(非醫療評估)。",
          "price": {
            "amount": 0,
            "currency": "TWD",
            "unit": "每次",
            "text": "免費"
          },
          "duration_min": 60,
          "schedule": [
            {
              "weekday": "sun",
              "time": "08:00",
              "teacher": "Dunken"
            },
            {
              "weekday": "sun",
              "time": "09:00",
              "teacher": "Dunken"
            },
            …
          ],
          "capacity_per_slot": 1,
          "teachers": [
            "Dunken"
          ],
          "credit_cost": 0
        },
        …
      ],
      "payment": "Paid services are paid at the venue; services priced at 0 are free. This connector does not take payments."
    },
    "isError": false
  },
  "jsonrpc": "2.0",
  "id": 4
}

Run against the live service (read-only): Shortened for display; … marks omitted parts. result.content[0].text (the same JSON as a string) and the business's address and contact details are omitted.

Service data

Each bookable service has service_id, name, category, summary, a price (amount, currency, unit, text) and a duration duration_min. An agent uses service_id to check availability and create a booking.

Product feed

Retail products need a different kind of data: products, stock, prices and promotions, supplied in batches by the merchant. The status of this part is Planned: there is no product feed import yet, so no format or example is given here.

Compared with a product feed

Product feed conceptIn SOP's service data
Basic product dataservice_id, name and the business profile
Priceprice (TWD; booking amounts are in the smallest unit)
Availability and inventoryOpen slots from get_availability, and capacity per slot
FulfillmentAddress, hours, and the booking and cancellation policy
VariantsSeparate services, each with its own service_id
Media, reviews, promotionsNot provided