Developer quickstart
An agent that wants to book a Sydney plumber calls one address instead of opening a website: frontlatch.com/mcp. This page is the reference for wiring that call yourself — the endpoint, a client config, curl for initialize and tools/list, all four tools with their schemas, a worked example, and the exact refusal every caller gets back until a business has verified the action.
The endpoint
One address answers for every business in the Sydney index: https://frontlatch.com/mcp, JSON-RPC 2.0 over HTTP POST. There is no per-business URL, no API key and nothing to provision — the same endpoint that answers initialize answers do.
server/discover, the protocol’s own mandatory RPC, is not implemented. That is a named gap, not a silent one: the specification allows a client to skip it and call a method directly, and every method below still answers.
Connect a client
A generic MCP client config
{
"mcpServers": {
"frontlatch": {
"type": "http",
"url": "https://frontlatch.com/mcp"
}
}
}mcpServers map. frontlatch.com/mcp is the one transport Frontlatch’s own server card names, and the server card and the ARD catalogue entry it sits inside are both real, fetchable files, not diagrams: `/.well-known/mcp/server-card.json` and `/.well-known/ai-catalog.json`.initialize and tools/list, by curl
No client required to check the endpoint is alive: two calls, both read-only.
initialize
curl -s -X POST https://frontlatch.com/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2026-07-28","capabilities":{},
"clientInfo":{"name":"quickstart","version":"0.1.0"}}}'
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2026-07-28",
"capabilities":{"tools":{"listChanged":true}},
"serverInfo":{"name":"frontlatch-gateway","title":"Frontlatch agent gateway","version":"0.1.0"}}}tools/list
curl -s -X POST https://frontlatch.com/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"resultType": "complete",
"tools": [
{ "name": "find_business", "title": "Find a Sydney business", ... },
{ "name": "list_actions", "title": "List a business's detected actions", ... },
{ "name": "describe_action", "title": "Describe one detected action", ... },
{ "name": "do", "title": "Run an action on a business …", ... }
]
}
}inputSchema, untrimmed, is below; the real response also carries a title, a longer description and the annotations object quoted with each schema.The four tools
Three read the index and can never run anything. The fourth, do, is the only one that could act on a business’s behalf, and it refuses almost every call by construction — see Verification and the trust gate for the mechanism and the worked example below for the exact refusal.
| Tool | Answers | Required arguments | Runs anything? |
|---|---|---|---|
find_business | Search the index by free text, suburb and/or category. All optional; none returns a bounded browse list. | None | No |
list_actions | Every action the crawler detected on one business’s site, verbatim from the index. | business | No |
describe_action | The detected action(s) of one kind — booking, quote, contact, callback, phone, widget, unclassified — on one business. | business, kind | No |
do | Attempts a named action on a business. Refused, with the business’s own website and phone in the reply, unless the business is claimed and has switched that action on for email-confirm routing. | business, action | Only a pending, logged attempt — never an executed one |
find_business
inputSchema + annotations, verbatim
{
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "A natural query, e.g. \"plumber in Sydney\" or \"somewhere for coffee in Surry Hills\"."
},
"suburb": {
"type": "string",
"description": "Exact suburb, e.g. \"Marrickville\"."
},
"category": {
"type": "string",
"description": "Category or a common synonym, e.g. \"plumber\" or \"sparky\"."
}
},
"additionalProperties": false
},
"annotations": {
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": false
}
}list_actions
inputSchema + annotations, verbatim
{
"inputSchema": {
"type": "object",
"properties": {
"business": {
"type": "string",
"description": "Business name or domain, as returned by find_business."
}
},
"required": ["business"],
"additionalProperties": false
},
"annotations": {
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": false
}
}business is required — a name or domain, as find_business returns it.describe_action
inputSchema + annotations, verbatim
{
"inputSchema": {
"type": "object",
"properties": {
"business": {
"type": "string",
"description": "Business name or domain, as returned by find_business."
},
"kind": {
"type": "string",
"enum": ["booking", "quote", "contact", "callback", "phone", "widget", "unclassified"],
"description": "The action kind to describe."
}
},
"required": ["business", "kind"],
"additionalProperties": false
},
"annotations": {
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": false
}
}business and kind are both required; kind is a closed enum of the seven action kinds the crawler classifies into.do
inputSchema + annotations, verbatim
{
"inputSchema": {
"type": "object",
"properties": {
"business": {
"type": "string",
"description": "Business name or domain, as returned by find_business."
},
"action": {
"type": "string",
"description": "The action kind to attempt, e.g. \"booking\"."
},
"params": {
"type": "object",
"description": "Ignored today — nothing is executed."
}
},
"required": ["business", "action"],
"additionalProperties": false
},
"annotations": {
"readOnlyHint": false,
"destructiveHint": true,
"idempotentHint": false,
"openWorldHint": true
}
}business and action are required; params is accepted but ignored — nothing this tool is given is executed. destructiveHint and openWorldHint both read true: the spec’s own conservative default for a tool whose target is arbitrary, not a promise about what the tool does today.Worked example: find a business, read what it offers, ask it to act
Three calls, chained the way an agent would chain them, all run against production on 17 Sep 2026.
1. find_business
curl -s -X POST https://frontlatch.com/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"find_business","arguments":{"query":"plumber in Sydney"}}}'
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"resultType": "complete",
"structuredContent": {
"results": [
{
"business": "Aussie Home Services",
"suburb": "Sydney",
"category": "plumber",
"domain": "aussieservices.com.au",
"website": "https://aussieservices.com.au/",
"status": "scanned",
"score": 56,
"grade": "partially ready",
"actionKinds": ["booking", "phone", "contact"],
"unreachable": false,
"unreachableDetail": null
}
],
"interpretation": { "category": "plumber", "categoryTerm": "plumber", "suburb": "Sydney", "freeText": null },
"suburbHadNoMatch": false
}
}
}2. list_actions
curl -s -X POST https://frontlatch.com/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call",
"params":{"name":"list_actions","arguments":{"business":"aussieservices.com.au"}}}'
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"resultType": "complete",
"structuredContent": {
"found": true,
"business": "Aussie Home Services",
"domain": "aussieservices.com.au",
"website": "https://aussieservices.com.au/",
"status": "scanned",
"actions": [
{ "kind": "booking", "params": { "url": "https://aussieservices.com.au/", "label": "booking form", "detail": "22 fields", "agentCallable": false } },
{ "kind": "phone", "params": { "url": "https://aussieservices.com.au/", "label": "Phone link", "detail": "tel:1300164206", "agentCallable": false } },
{ "kind": "contact", "params": { "url": "https://aussieservices.com.au/contact-us", "label": "contact form", "detail": "22 fields", "agentCallable": false } }
],
"detail": null,
"suburb": "Sydney",
"category": "plumber"
}
}
}The refusal, and why it is a feature
3. do
curl -s -X POST https://frontlatch.com/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":5,"method":"tools/call",
"params":{"name":"do","arguments":{"business":"aussieservices.com.au","action":"booking"}}}'
{
"jsonrpc": "2.0",
"id": 5,
"result": {
"resultType": "complete",
"isError": false,
"structuredContent": {
"authorised": false,
"reason": "not-verified",
"business": "Aussie Home Services",
"domain": "aussieservices.com.au",
"detail": "Aussie Home Services has not verified any action type with Frontlatch, so this gateway will not run \"booking\" on their behalf — no code path here executes a real action on an unverified business. Reach them directly using howToReach below.",
"howToReach": {
"website": "https://aussieservices.com.au/",
"phone": "tel:1300164206"
}
}
}
}detail string names its internal rule file by number and this site does not print a repo path on a customer page, so that parenthetical is removed and nothing else is — same business, same phone number, same wording either side of the cut.That refusal is not a bug waiting on a fix. No business anywhere in the index has verified an action type with Frontlatch yet, and the gateway will not let an agent run one on their behalf until they have — that is the trust invariant, applied at the one place code could otherwise fire something nobody agreed to.
One path is wired for when a business does verify: claimed and switched on for email-confirm routing, the same call would come back "authorised": true, "reason": "pending-confirmation" — a pending attempt logged and the owner emailed a one-tap confirm link, still nothing executed until they tap it. Build against the refusal shown here; it is what every caller gets today.