DocsDevelopers / Developer quickstart

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"
    }
  }
}

Select the text above and copy it — the button needs JavaScript, this does not.

Paste this into whichever client reads a 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"}}}
Live. Run 17 Sep 2026; the response below the command is copied unmodified.

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 …", ... }
    ]
  }
}
Live, same run. Trimmed to the four tool names — each one’s full 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.

What each tool answers, what it needs, and whether it can run anything
ToolAnswersRequired argumentsRuns anything?
find_businessSearch the index by free text, suburb and/or category. All optional; none returns a bounded browse list.NoneNo
list_actionsEvery action the crawler detected on one business’s site, verbatim from the index.businessNo
describe_actionThe detected action(s) of one kind — booking, quote, contact, callback, phone, widget, unclassified — on one business.business, kindNo
doAttempts 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, actionOnly 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
  }
}
From the tools/list response above, 17 Sep 2026. Nothing is required; calling it with no arguments returns a bounded browse list.

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
  }
}
From the same response. 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
  }
}
From the same response. 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
    }
  }
}
Live. The real call returned 7 plumbers; trimmed here to the first so the next two calls follow one business.

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"
    }
  }
}
Live, full response. Three ways in on this business’s own site, none of them agent-callable yet — that flag turns on once a business verifies.

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"
      }
    }
  }
}
Live, full response, one clause cut: the real 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.