# MCP server

> Connect an MCP client to Fondaro with OAuth or a scoped fdr_mcp_ key, and call 95 tools for leads, deals, properties, brochures, documents and the help docs.

Fondaro runs a hosted Model Context Protocol (MCP) server so an AI client can work with your leads, deals, tasks, notes, property sources, interactive brochures and documents as you. Every call runs with your own access: nothing over MCP reaches more than you can reach in the dashboard. The server exposes 95 tools from twenty explicitly allowlisted tool groups, and the tools are listed below.

To work with the same data from your own code instead of an AI client, use the [REST API](https://www.fondaro.com/docs/api/overview.md) with an API key. Nearly every tool here has a matching `/v1` route.

## Endpoint

```
https://api.fondaro.com/mcp/v1
```

Use it exactly as written, with no trailing slash. The server prefers MCP `2026-07-28` over Streamable HTTP and also negotiates `2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05` and `2024-10-07`. It is stateless and answers JSON: only `POST` carries MCP traffic, there is no session and no SSE stream.

## Authenticate

The server accepts two bearer credentials. Anything else, including a dashboard session token, returns `401`.

| Credential | Shape | How you get it |
|---|---|---|
| OAuth access token | `fdr_oat_…` | Your client runs the browser sign-in for you. You never copy it. |
| MCP API key | `fdr_mcp_` + 32 lowercase hex characters | Settings › Integrations › Fondaro MCP, or `POST /mcp-keys` |

```
Authorization: Bearer fdr_mcp_0123456789abcdef0123456789abcdef
```

### OAuth

Clients discover OAuth from the endpoint, then use authorization code with S256 PKCE and refresh their own tokens.

| Document | URL |
|---|---|
| Authorization server metadata | `https://api.fondaro.com/.well-known/oauth-authorization-server` |
| Protected resource metadata | `https://api.fondaro.com/.well-known/oauth-protected-resource/mcp/v1` |

The resource is `https://api.fondaro.com/mcp/v1`. A client that asks for no scope gets the four read scopes (`crm:read`, `properties:read`, `brochures:read`, `documents:read`); write scopes must be requested and approved.

### API keys

A key belongs to the person who created it. The full key is shown once and stored only as a SHA-256 hash; if you lose it, revoke it and create another. Keys can expire after 1 to 3650 days.

## Scopes

| Scope | Allows |
|---|---|
| `crm:read` | Read leads, tasks, notes, deals, calls, email, messages, your Inbox, your calendar and records, and search the help docs |
| `crm:write` | Create and change leads, tasks, notes and deals |
| `properties:read` | Search property sources, read listings, viewings, open houses, collections and buyer requests |
| `brochures:read` | List and read interactive brochures |
| `brochures:write` | Create, edit, renew, revoke and delete interactive brochures |
| `documents:read` | List and read documents in your library |
| `documents:write` | Create and edit documents, share or revoke links, attach them to leads |

A tool whose scope the credential lacks is left out of `tools/list` and refused at call time. Creating a brochure, adding or refreshing a listing on one, and renewing one also need `properties:read`, because they read the listing from its source.

## Who sees what

- Members reach only the leads assigned to them; organization admins reach the whole organization.
- Members see and change only the brochures they created; admins see the organization's.
- Documents follow the library: organization-wide and shared documents plus your own private ones, and everything for an admin. Anyone who can see a Team document can edit it; a private document only its creator or an admin. Sharing, moving and deleting stay with the creator and admins.
- The admin tools (`set_lead_assignees`, `add_lead_assignees`, `bulk_update_lead_status` and every lead routing tool except `set_routing_away`) are absent from a member's tool list.
- CRM write tools and every document tool need an active Fondaro subscription. Brochure tools do not.

Membership is checked on every request, with a role cache of about five minutes. Removing someone from the organization cuts off their keys and OAuth connections within that window.

## Call a tool

Every `POST` is independent. With `2026-07-28`, send these on each request:

| Header or field | Value |
|---|---|
| `Mcp-Protocol-Version` | `2026-07-28` |
| `Mcp-Method` | The JSON-RPC `method` |
| `Mcp-Name` | The tool name, on `tools/call` |
| `params._meta` | `io.modelcontextprotocol/protocolVersion`, `io.modelcontextprotocol/clientInfo`, `io.modelcontextprotocol/clientCapabilities` |

A missing or mismatched header returns a JSON-RPC error with HTTP `400`. Do not send `Mcp-Session-Id` or `Last-Event-ID`.

```bash
curl -X POST https://api.fondaro.com/mcp/v1 \
  -H "Authorization: Bearer $FONDARO_MCP_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Mcp-Protocol-Version: 2026-07-28" \
  -H "Mcp-Method: tools/call" \
  -H "Mcp-Name: list_leads" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "list_leads",
      "arguments": { "limit": 10 },
      "_meta": {
        "io.modelcontextprotocol/protocolVersion": "2026-07-28",
        "io.modelcontextprotocol/clientInfo": { "name": "your-client", "version": "1.0.0" },
        "io.modelcontextprotocol/clientCapabilities": {}
      }
    }
  }'
```

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "complete",
    "content": [{ "type": "text", "text": "{\"leads\":[…],\"total\":42}" }]
  }
}
```

Use `server/discover` (same headers, `Mcp-Method: server/discover`) to read the server's identity, and `tools/list` to read the tools you may call. The server advertises tools only: no prompts, resources or subscriptions. `tools/list` is sorted by name and carries a private five-minute cache hint, because it depends on the caller; never share one person's list with another.

### Earlier protocol revisions

A client on `2025-11-25` or earlier may send `initialize` and plain JSON-RPC without the `Mcp-*` headers. No session is created, so `tools/list` and `tools/call` also work without a prior `initialize`. The tools, scopes and input schemas are the same.

## Connect a client

| Client | Sign-in | Setup |
|---|---|---|
| ChatGPT | OAuth | Add `https://api.fondaro.com/mcp/v1` as a custom app, then sign in to Fondaro and choose **Allow**. |
| Claude Code | OAuth | Add the server without a header, open `/mcp`, choose **Authenticate** and sign in. |
| Codex CLI | API key | Point `--bearer-token-env-var` at a variable holding the key. |
| Cursor, Visual Studio Code | API key | Add the URL and an environment-backed `Authorization` header to your user-level MCP config. |
| Scripts | API key | Send `Authorization: Bearer fdr_mcp_…`. |

```bash
# Claude Code
claude mcp add --transport http --scope user fondaro https://api.fondaro.com/mcp/v1

# Codex CLI
codex mcp add fondaro --url https://api.fondaro.com/mcp/v1 \
  --bearer-token-env-var FONDARO_MCP_API_KEY
```

Keep a key out of config files, source control and shell history; reference an environment variable instead. The [Fondaro MCP guide](https://www.fondaro.com/docs/dashboard/fondaro-mcp.md) walks through each client. Every key and OAuth connection on your account is listed under **Your connections** on that page, where each can be revoked.

## Manage keys

These endpoints take your dashboard session (`Authorization: Bearer <session token>`), not an `fdr_mcp_` key.

### POST /mcp-keys

Create a key for yourself.

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | 1 to 255 characters |
| `scopes` | string[] | No | Any of the seven scopes, no repeats. Omit for all seven. |
| `expiresInDays` | integer | No | 1 to 3650. Omit for a key that does not expire. |

```bash
curl -X POST https://api.fondaro.com/mcp-keys \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Claude Desktop", "scopes": ["crm:read", "properties:read"], "expiresInDays": 90 }'
```

```json
{
  "id": "3f1c2a9e-8b7d-4c6e-9a1f-2b3c4d5e6f70",
  "key": "fdr_mcp_0123456789abcdef0123456789abcdef",
  "keyPrefix": "fdr_mcp_012345",
  "scopes": ["crm:read", "properties:read"],
  "expiresAt": "2026-12-31T10:00:00.000Z"
}
```

`key` appears in this response only.

### GET /mcp-keys

List keys, never with their value. A member sees their own; an admin sees every key in the organization.

### DELETE /mcp-keys/:id

Revoke a key at once. Members revoke their own; admins revoke any. Answers `{ "success": true }`.

## Tools

95 tools. Writes marked *entitled* need an active subscription; *admin* tools are for organization admins only.

### Leads

| Tool | Scope | Purpose |
|---|---|---|
| `list_leads` | `crm:read` | List leads with filters, paginated |
| `search_leads` | `crm:read` | Fuzzy search by name, email or phone |
| `get_lead` | `crm:read` | One lead's contact details, tags, owners and totals |
| `get_lead_timeline` | `crm:read` | A lead's activity: notes, tasks, calls, email, messages, viewings, documents |
| `get_lead_counts` | `crm:read` | Number of leads in each pipeline stage |
| `list_tags` | `crm:read` | The organization's tags, with lead counts |
| `list_org_members` | `crm:read` | Members (id, name, email, role), for assigning |
| `list_org_teams` | `crm:read` | Teams and their member ids, for assigning |
| `create_lead` | `crm:write`, entitled | Create a lead, owned by you unless an admin names others |
| `update_lead_contact` | `crm:write`, entitled | Change name, email, phone or company |
| `update_lead_status` | `crm:write`, entitled | Move a lead to another stage |
| `set_lead_tags` | `crm:write`, entitled | Replace a lead's tags |
| `set_lead_assignees` | `crm:write`, admin | Replace a lead's owners |
| `add_lead_assignees` | `crm:write`, admin | Add owners without removing any |
| `bulk_update_lead_status` | `crm:write`, admin | Move many leads to one stage |

### Lead routing

Routes are checked top to bottom; the first one that matches and has someone to give the lead to wins, then Smart routing, then Unassigned. See [Lead routing](https://www.fondaro.com/docs/api/lead-routing.md).

| Tool | Scope | Purpose |
|---|---|---|
| `get_lead_routing` | `crm:read`, admin | The routes in order, their people, Smart routing, problems and 30-day counts |
| `preview_lead_routing` | `crm:read`, admin | Which route a lead would take and who gets it, without assigning it |
| `create_lead_routing_rule` | `crm:write`, admin | Add a route: conditions, people or live teams, how it shares leads, office hours, a daily cap |
| `update_lead_routing_rule` | `crm:write`, admin | Change a route; fields you leave out stay as they are |
| `delete_lead_routing_rule` | `crm:write`, admin | Delete a route |
| `reorder_lead_routing_rules` | `crm:write`, admin | Put the routes in a new order |
| `set_smart_routing` | `crm:write`, admin | Switch Smart routing on or off |
| `route_unassigned_leads` | `crm:write`, admin | Route the leads waiting in Unassigned now (up to 500) |
| `set_routing_away` | `crm:write`, entitled | Mark yourself (or, as an admin, anyone) away until a date |

### Queues

A queue is a saved view with lanes: who is in it, which lane a lead lands in (the first lane that matches wins), the order inside each lane, and whether everyone else shows last or not at all. You change or delete only queues you made; an admin also chooses who a queue is given to.

| Tool | Scope | Purpose |
|---|---|---|
| `list_queues` | `crm:read` | The queues you can open, each in plain words and as a full definition |
| `get_queue` | `crm:read` | One queue by id |
| `preview_queue` | `crm:read` | Lane counts and the first ten leads of each lane, with why each one is there, for a saved queue or an unsaved definition |
| `create_queue` | `crm:write`, entitled | Create a queue; use `$me` as the owner to make it personal for whoever opens it |
| `update_queue` | `crm:write`, entitled | Change a queue you made; fields you leave out stay as they are |
| `set_queue_audience` | `crm:write`, entitled, admin | Set who a queue is given to, on any queue you can open: `add`, `remove` or `replace` people (`list_org_members`) and teams (`list_org_teams`). It goes into their Leads sidebar and a team includes people who join later |
| `delete_queue` | `crm:write`, entitled | Delete a queue you made |

### Inbox, calendar and tasks

| Tool | Scope | Purpose |
|---|---|---|
| `list_inbox` | `crm:read` | Your Inbox in page order (Waiting, New or All) |
| `get_calendar` | `crm:read` | Your meetings, viewings and tasks for up to 42 days in a time zone |
| `get_free_times` | `crm:read` | Free slots of a given length, up to 14 days ahead |
| `list_tasks` | `crm:read` | Your tasks, or one lead's |
| `create_task` | `crm:write`, entitled | Add a task to a lead |
| `update_task` | `crm:write`, entitled | Change title, description, due date or done state |
| `delete_task` | `crm:write`, entitled | Delete a task |

### Notes

| Tool | Scope | Purpose |
|---|---|---|
| `list_notes` | `crm:read` | A lead's notes, newest first |
| `create_note` | `crm:write`, entitled | Add a note to a lead |
| `update_note` | `crm:write`, entitled | Change a note |
| `delete_note` | `crm:write`, entitled | Delete a note (your own, or any as admin) |

### Deals

| Tool | Scope | Purpose |
|---|---|---|
| `list_deals` | `crm:read` | One lead's deals, or up to 1,000 recent deals with filters |
| `get_deal` | `crm:read` | One deal with its commission participants |
| `create_deal` | `crm:write`, entitled | Create a deal on a lead |
| `update_deal` | `crm:write`, entitled | Change amount, listing, dates, commission, invoice or owner |
| `change_deal_stage` | `crm:write`, entitled | Move a deal to another stage |
| `close_deal_won` | `crm:write`, entitled | Mark a deal won |
| `close_deal_lost` | `crm:write`, entitled | Mark a deal lost, with an optional reason |
| `reopen_deal` | `crm:write`, entitled | Reopen a won or lost deal |

### Calls, email and messages

| Tool | Scope | Purpose |
|---|---|---|
| `get_call_history` | `crm:read` | A lead's calls: direction, outcome, duration, recorded caller |
| `get_call_transcript` | `crm:read` | The stored transcript of a call |
| `get_call_analysis` | `crm:read` | The stored call review: sentiment, objections, next steps |
| `get_email_history` | `crm:read` | A lead's email |
| `get_conversation` | `crm:read` | A lead's WhatsApp, Instagram, LinkedIn and Telegram messages |

### Search and records

| Tool | Scope | Purpose |
|---|---|---|
| `search_crm_semantic` | `crm:read` | Find leads by meaning across notes, call transcripts and email |
| `match_leads_to_listing` | `crm:read` | Leads whose history fits a listing |
| `resolve_records` | `crm:read` | Summaries of up to 20 records of any kind, as you see them |

### Properties

| Tool | Scope | Purpose |
|---|---|---|
| `list_property_sources` | `properties:read` | Sources, whether each is connected, and their facets |
| `list_property_search_options` | `properties:read` | Exact values for one search facet |
| `autocomplete_location` | `properties:read` | Resolve a place to ids for `search_properties` |
| `search_properties` | `properties:read` | Search one source (Fondaro: your stock or the network) |
| `get_property` | `properties:read` | Full detail for one listing |
| `list_viewings` | `properties:read` | Registered viewings of your listings |
| `get_viewing` | `properties:read` | One viewing |
| `list_open_houses` | `properties:read` | Network open houses, or your own |
| `get_open_house` | `properties:read` | One open house |
| `list_collections` | `properties:read` | Listing collections you are in |
| `get_collection` | `properties:read` | One collection and its homes |
| `list_buyer_requests` | `properties:read` | Buyer requests on the network, or your agency's |

### Interactive brochures

| Tool | Scope | Purpose |
|---|---|---|
| `list_brochures` | `brochures:read` | Brochures you can see, newest first |
| `get_brochure` | `brochures:read` | One brochure with its listing summaries and `publicUrl` |
| `create_brochure` | `brochures:write`, `properties:read` | Create a brochure from 1 to 25 listings |
| `update_brochure` | `brochures:write` | Change title, recipient, theme, accent or location visibility |
| `add_brochure_listing` | `brochures:write`, `properties:read` | Add one listing (25 at most) |
| `remove_brochure_listing` | `brochures:write` | Remove one listing |
| `reorder_brochure_listings` | `brochures:write` | Set the full listing order |
| `refresh_brochure_listing` | `brochures:write`, `properties:read` | Read one listing again from its source |
| `set_brochure_listing_location` | `brochures:write` | Change one listing's address and coordinates |
| `update_brochure_listing_details` | `brochures:write` | Override one listing's details and visibility |
| `revoke_brochure` | `brochures:write` | Turn off the link, keeping the brochure |
| `renew_brochure` | `brochures:write`, `properties:read` | Restore an expired or revoked link, optionally refreshing listings |
| `delete_brochure` | `brochures:write` | Delete a brochure for good |

Pass property identities (`source`, `id`, optional `country`) exactly as `search_properties` or `get_property` returned them. Listing edits take the brochure-listing id from `create_brochure` or `get_brochure`.

### Documents

All document tools are entitled.

| Tool | Scope | Purpose |
|---|---|---|
| `list_documents` | `documents:read` | Documents you can see, most recently changed first |
| `list_document_folders` | `documents:read` | Folders, with how many items you can see in each |
| `get_document` | `documents:read` | One document with its Markdown body (a whiteboard as a plain-text outline) |
| `list_lead_documents` | `documents:read` | Documents attached to a lead |
| `create_markdown_document` | `documents:write` | Create a private Markdown document |
| `update_document` | `documents:write` | Replace a document's title or body |
| `share_document` | `documents:write` | Publish a link and return its `shareUrl` |
| `revoke_document_share` | `documents:write` | Take the link down |
| `attach_document_to_lead` | `documents:write` | Attach a document to a lead |
| `detach_document_from_lead` | `documents:write` | Remove that attachment |

Attaching needs access to both the document and the lead, and is not a grant: a private document stays private.

### Help docs

How to use Fondaro, from these docs. Nothing here is your data, so the two tools need no subscription and no admin role.

| Tool | Scope | Purpose |
|---|---|---|
| `search_docs` | `crm:read` | Up to 10 articles for a question, best first, each with the paragraph that matches |
| `get_doc` | `crm:read` | One article as Markdown, by the path `search_docs` returned |

A long article comes back cut at a paragraph with `truncated: true`; the whole page is at its `markdownUrl` (every docs page also answers as Markdown at its URL plus `.md`). An unknown path answers `DOCS_NOT_FOUND`. From your own code, read the pages over HTTPS with no key: `https://www.fondaro.com/llms.txt` indexes them.

## Results and paging

- Responses have a size budget. A long lead timeline returns the entries that fit, marks shortened text with `_preview`, and gives `nextOffset`; continue from `nextOffset`, not from your offset plus `limit`. `totalIsExact: false` means the total is a lower bound.
- `get_call_history` accepts `limit` (default 20, at most 100) and `offset` and answers `{ calls, total, hasMore, nextOffset }`. A history too large to return whole without them fails with `CALL_HISTORY_PAGINATION_REQUIRED`.
- A call's `ownerId` is the person recorded as making or taking it, which can differ from the lead's current owners.
- `search_crm_semantic` takes a query of up to 500 characters and returns 1 to 30 matches (10 by default). An empty answer means nothing relevant is indexed, not that no such lead exists.
- Other lists that would exceed the budget fail with an error rather than drop rows: ask for fewer results.

## Limits and errors

| Limit | Value |
|---|---|
| Requests per key or OAuth connection | 120 per minute |
| Failed sign-ins per IP | 120 per minute |
| Property portal reads | 300 uncached portal calls per organization per UTC day |

When a limit is reached the server answers `429` with `Retry-After`. Once the portal allowance is spent, uncached portal reads return an error until midnight UTC; the Fondaro network and Resales Online keep working.

| Status | Meaning |
|---|---|
| `400` | Protocol headers missing or disagreeing with the body |
| `401` | Missing, unknown, revoked or expired credential (with a `WWW-Authenticate` challenge) |
| `403` | No longer a member of the organization, or the organization is disabled (`code: "ORG_DISABLED"`) |
| `405` | `GET` or `DELETE` on the endpoint |
| `429` | Rate limit reached |

A tool refused for scope, role or access returns a JSON-RPC error with no data from the record.

Source: https://www.fondaro.com/docs/api/mcp
