Why MCP
MCP is the emerging standard interface between AI assistants and external systems. Before MCP, "integrating your product with an AI assistant" meant writing a custom plugin for every assistant — a Claude plugin, a Cursor plugin, a ChatGPT action — each with its own schema, auth, and lifecycle.
MCP collapses that into a protocol. Implement the server once. Any MCP-compatible client discovers your tools and resources automatically. People API's MCP server is the integration — there isn't a Claude-specific or Cursor-specific variant.
The six tools
Each tool is a thin wrapper over a REST endpoint. The shape is deliberate: an agent should be able to read the tool list and understand the protocol without reading the API reference. Params use snake_case because MCP idioms favor it; the underlying REST API uses camelCase.
create_intentDeclare what you want. Creates a Tier-1 intent in the registry.
category: string·reqdescription: string·reqattributes: objectbudget_min: numberbudget_max: numbercurrency: stringdefaults to USDurgency: enumbrowsing | planning | ready_to_buymetro: stringzip: stringtags: string[]autoApproveBelow: numberauto-accept qualifying offers at or below this price
{
"category": "plumbing_fixtures",
"description": "Pink undermount kitchen sink, porcelain, 30\"",
"budget_min": 200,
"budget_max": 400,
"urgency": "ready_to_buy",
"metro": "NYC",
"autoApproveBelow": 300
}list_intentsView your intents. Filter by status or category.
status: enumactive | fulfilled | expired | cancelledcategory: string
{ "status": "active" }view_offersSee offers for an intent, ranked by score.
intentId: string·req
{ "intentId": "int_7f..." }respond_to_offerAccept, reject, or counter an offer.
offerId: string·reqaction: enum·reqaccept | reject | countercounterMessage: stringrequired when action = counter
{ "offerId": "off_9a...", "action": "accept" }cancel_intentCancel an active intent.
intentId: string·req
{ "intentId": "int_7f..." }browse_registryVendor-perspective browse of the public registry. Filter by category, metro, urgency, budget.
category: stringmetro: stringurgency: enumbrowsing | planning | ready_to_buybudgetMin: numberlimit: numberdefaults to 20
{ "category": "plumbing_fixtures", "metro": "NYC", "urgency": "ready_to_buy" }The one resource
MCP resources are read-only data the client can fetch. People API exposes one:
MCP resource# URI
peopleapi://protocol
# Returns
{
"name": "People API",
"tagline": "Services got APIs. Now people get them too.",
"tiers": [
{ "number": 1, "name": "Public", "access": "none" },
{ "number": 2, "name": "Engaged", "access": "session_token" },
{ "number": 3, "name": "Transactional", "access": "session_token" }
],
"capabilities": {
"scoring": true,
"auto_accept": true,
"auto_reject": true,
"sse_events": true
}
}The resource is a self-describing manifest. An agent that has never seen People API can read it and understand the privacy model and server capabilities before calling any tool.
Transport
Stdio. The MCP client spawns the server as a subprocess and communicates over standard input/output framed as newline-delimited JSON-RPC. This is the canonical local-MCP transport — it matches how Claude Desktop and Cursor launch servers.
Config snippet
Drop this into ~/Library/Application Support/Claude/claude_desktop_config.json (or the Cursor equivalent) and restart the client. The tools appear automatically.
claude_desktop_config.json{
"mcpServers": {
"peopleapi": {
"command": "node",
"args": ["/absolute/path/to/peopleapi/dist/mcp/server.js"]
}
}
}A real agent conversation
The end-to-end pink-sink flow, played through Claude as an MCP client. Tool calls are shown inline; the agent orchestrates all three of create_intent, view_offers, and respond_to_offer without a second prompt.
- You
I need a pink undermount kitchen sink, porcelain, 30 inches, in NYC. Budget $200–400. Auto-approve anything under $300. Go find me offers.
- Claude
I'll publish an intent to the People API registry now, then check for offers in a moment.
- Tool call
create_intent{ "category": "plumbing_fixtures", "description": "Pink undermount kitchen sink, porcelain, 30\"", "attributes": { "material": "porcelain", "color": "pink", "size_inches": 30, "mount": "undermount" }, "budget_min": 200, "budget_max": 400, "urgency": "ready_to_buy", "metro": "NYC", "autoApproveBelow": 300 }Result{ "intent": { "id": "int_7f3a1b2c", "status": "active" } } - Claude
Intent published. Let me check what vendors have come back with.
- Tool call
view_offers{ "intentId": "int_7f3a1b2c" }Result{ "offers": [ { "id": "off_9a2b4c8d", "vendor": "Premium Sink Co", "price": 280, "deliveryDays": 2, "score": 82, "status": "accepted" }, { "id": "off_3c5d7e9f", "vendor": "Bathrooms Direct", "price": 340, "deliveryDays": 4, "score": 73, "status": "pending" }, { "id": "off_5e7f9h1j", "vendor": "Budget Plumbing", "price": 150, "deliveryDays": 7, "score": 69, "status": "pending" } ] } - Claude
You already have one auto-accepted: Premium Sink Co at $280 in 2 days, score 82. Two more are pending review — Bathrooms Direct at $340 (score 73), and Budget Plumbing at $150 (score 69, a touch below the auto-accept threshold). Should I accept the second offer, counter it, or hold?
- You
Just accept Premium Sink. Reject the others.
- Tool call
respond_to_offer{ "offerId": "off_3c5d7e9f", "action": "reject" }Result{ "offer": { "status": "rejected" } } - Tool call
respond_to_offer{ "offerId": "off_5e7f9h1j", "action": "reject" }Result{ "offer": { "status": "rejected" } } - Claude
Done. Premium Sink Co remains accepted. You'll get a session token when they confirm the ship date.