FUB MCP
A self-hosted MCP server wrapping the Follow Up Boss CRM API with 21 tools that encode the CRM's own quirks — event-based lead dedupe, an isCompleted task field — directly into…
- 21
- MCP tools implemented over one CRM API
- 100
- page-size cap enforced client-side, matching the CRM's own limit
- 01Edge
- cloudflared
- 02Application
- Python
- 03Operations
- launchd
- 04Supporting
- FastMCP
- httpx
- uv
- macOS Keychain
FUB MCP holds live data, so this shows the verified technology stack by layer rather than a screenshot. Layers, not connections — which service calls which is not something a dependency list can prove. Hosts, ports and topology are deliberately absent.
Problem
Giving an AI agent write access to a production CRM is unsafe if the agent only has the CRM's public REST docs to work from. Follow Up Boss's API looks like a conventional resource API but hides operational traps that only surface through live testing: creating a lead by posting directly to the people endpoint bypasses the CRM's dedupe-by-email/phone logic and never fires the action plans (automated follow-up sequences) a real lead is supposed to trigger; writing a task's completion state under the field name a read response seems to imply returns a 400, because writes require a different field name entirely; and a client that ignores the rate limit or the API's own pagination-link scheme silently drops requests or misses records. An agent given raw HTTP access repeats mistakes a human integrator already made and fixed once, and that knowledge is worthless if it lives only in a person's memory instead of somewhere the agent reads before it acts.
What was built
The server wraps the CRM's v1 API with one FastMCP tool per action, grouped by resource: identity and account metadata, people (search, get, update, delete, and lead-event creation), notes, tasks, calls, deals, webhooks, and a read-only escape hatch for any documented endpoint without a dedicated tool. Tools carry annotations — a read-only hint on every GET-backed tool, a destructive hint on deletes — so a calling agent gets machine-readable risk signaling instead of having to infer it from a tool's name. The lead-creation tool is the one write path meant for new leads: it posts to the events endpoint instead of the people endpoint, and it turns the CRM's silent empty response (returned when a lead source's flow is set to archive incoming events) into an explicit warning back to the caller instead of a false success. The server runs two ways from one codebase: over stdio for a local Claude Code session, and as an HTTP service, kept running by a launchd job and reached through a tunnel, registered as a custom connector so a claude.ai session can operate the same CRM.
Technical approach
Every call routes through one retry-aware request function: it clamps list-page limits to the CRM's own 100-record cap, strips empty parameters before sending, retries up to three times on HTTP 429 by honoring the response's own retry-after header capped at 15 seconds, and raises a structured tool error carrying the response body on any other failure status. Pagination tools accept the API's own next-page link back as an argument and call it directly rather than reconstructing query parameters, so paging state lives entirely in the CRM's response instead of being re-derived client-side. Authentication resolves in order — an environment variable first, then a macOS Keychain lookup via the system security CLI — so the same binary runs unattended as a service without a shell environment to source credentials from. The HTTP transport went through a real design change captured in git history: the first commit shipped a plain HTTP endpoint with no additional access control beyond the tunnel in front of it; a follow-up commit added an unguessable secret path prefix, also resolved from Keychain, after discovering that the claude.ai custom-connector integration sends no bearer token unless the server implements full OAuth — a scope well beyond a single-account internal tool. The secret path substitutes for a token: knowing the URL is the credential, and the transport also declares an explicit allowed-hosts list so it won't respond to a Host header outside the ones it expects. The escape-hatch tool is deliberately read-only and rejects any path containing a directory-traversal sequence or a double leading slash, so it can expose the CRM's full read surface without becoming a write-anything backdoor. A single shared HTTP client is initialized lazily and reused across calls rather than opened per request, keeping connection reuse and auth-header construction in one place.
Creative approach
Craft
The interface design treats each tool's docstring as a behavioral contract rather than a description. The person-update tool's docstring states inline that a phones array replaces the existing list rather than merging with it, that tags merge by default with a flag to replace instead, and that the source field cannot change after creation — facts that would otherwise require reading the CRM's separate API documentation before writing a single line of calling code. The lead-creation tool's docstring lists which event types actually trigger an automation rather than pointing at an external reference, because getting that list wrong means a call that looks successful but quietly notifies no one. Read-only and destructive tools carry explicit annotations so an agent, or a human approving its actions, can tell a lookup from an irreversible delete without inferring it from a name.
Reframe
The non-obvious idea is treating a CRM's undocumented operational behavior as interface design material rather than as a wiki entry. Most of what makes this server useful isn't the HTTP wrapping, which is mechanical; it's that facts discovered only through live testing — dedupe happens on events, not on people; a completion field is spelled differently on write than a caller might assume from reading a record; newly created people can be briefly invisible to other endpoints before lead routing assigns an agent — live inside the same docstrings the calling agent reads before acting, so the correct behavior and the documentation of that behavior are the same artifact and cannot drift apart. The secret-path auth decision applies the same instinct to security: rather than building OAuth to satisfy a connector protocol's assumption, the constraint was re-read as being about what the client checks, not who it authenticates as, and solved with a property already available — an unguessable path — instead of a new subsystem.
Process and what failed
The HTTP transport's first commit shipped a working flag with no additional access control beyond whatever the tunnel in front of it provided — a reasonable default for a session running on the same machine, but insufficient the moment the plan expanded to a remote custom connector. The reversal came from discovering, only after wiring up that connector, that the claude.ai side sends no bearer token to a custom MCP connector unless the server speaks full OAuth, an authentication flow this single-account internal tool had no reason to build for one user. Rather than stand up OAuth, the follow-up commit replaced the bare endpoint path with a Keychain-resolved secret segment and moved the API-key lookup itself to check Keychain as a fallback to the environment variable, so the same change that closed the connector's auth gap also removed the requirement that the background service have an exported environment variable to read a key from.
Outcome
The server runs as the CRM-facing tool layer behind Claude sessions working the Evolve Estates pipeline: 21 tools cover every resource an agent needs to search, read, and update leads without ever calling the raw CRM API directly. It runs two ways from one codebase — locally over stdio for the machine's own Claude Code sessions, and as a standing HTTP service, launchd-managed and reached through a tunnel with secret-path auth, registered as a custom connector so a claude.ai session can operate the same CRM. Because every operational fact discovered through live use — dedupe behavior, the completion field name, rate-limit handling — is encoded in a tool's contract rather than a separate document, an agent calling these tools inherits the correct behavior automatically instead of needing to be told.
Related work
- Internal2026WebshooterA site crawler that walks every page and interaction of a website and generates a documentation-quality HTML gallery and printable PDF manual, with a before/after screenshot…
- Internal2026Brand ExtractorA CLI that turns a website URL into a frozen-schema brand profile through exactly one Claude call, then generates on-voice content packs whose every claim is code-verified…
- Prototype2026StoryFlowA gated, six-stage pipeline that turns raw property walkthrough footage into three branded edits automatically, with FCP-grade finishing applied by code instead of an editor.