Skip to main content
Glama
Alexey0424

MCP Connector - Follow Up Boss

by Alexey0424

MCP Connector - Follow Up Boss

TypeScript Cloudflare Workers MCP OAuth 2.0 Hono Zod Tests

A remote MCP server on Cloudflare Workers that lets a real-estate sales team talk to their CRM (Follow Up Boss) from Claude — web and mobile, no install, no API keys in anyone's hands.

A closer asks "How did I do today?" and Claude answers from live CRM data: their calls (with the AI call summaries already logged on each call), the notes they wrote, their texts, an exact numeric summary. Two managers get an account-wide view — team leaderboard, per-closer drill-down, live pipeline by stage — from the same connector, gated by role. Everything is read-only and every request is scoped server-side to the signed-in user.

Built for the same production engagement as real-estate-crm-automation-suite. This ran (and runs) in production. Client identifiers, staff names, account IDs, and live URLs have been replaced with fictional equivalents so the engineering can be shown publicly; the code, tests, and design docs are the real thing.

How it works

sequenceDiagram
    participant C as Claude (claude.ai / mobile)
    participant W as Cloudflare Worker
    participant G as Google Sign-In
    participant F as Follow Up Boss API

    C->>W: Connect (OAuth /authorize)
    W->>G: Redirect, restricted to the company Workspace domain
    G-->>W: Verified email
    W->>F: GET /v1/users → map email to FUB userId (or deny)
    W-->>C: MCP token — session carries { fubUserId, isManager }
    C->>W: Tool call, e.g. eod_report(period: "today")
    W->>F: Scoped GETs — userId injected from the session, never from arguments
    F-->>W: Calls / notes / texts / people
    W-->>C: Only the caller's own data (re-validated before returning)

One Worker plays three roles: an OAuth server toward Claude (via workers-oauth-provider), an OAuth client toward Google for identity, and an MCP server (a Durable Object running McpAgent) exposing the tools. The single Follow Up Boss API key lives only as a Worker secret; users never see or handle credentials.

Related MCP server: my-mcp-server

The security model

The client's hard requirement: a closer must never be able to see another closer's data — not by asking nicely, not by crafting arguments. That is enforced by construction, not by prompt:

  • Identity comes from the session, never from tool input. Every tool reads fubUserId from the OAuth session props written at login. No tool accepts a user id (closer tools) — there is nothing to spoof.

  • Scoping is applied server-side and re-validated. Queries are filtered by the session's userId and every returned record is checked again before it leaves the Worker (defense in depth against API quirks).

  • Ownership checks before drill-down. get_lead_activity refuses any lead not assigned to the caller.

  • Role gating by tool registration. Manager tools are only registered on sessions whose verified email is on the MANAGER_EMAILS allowlist — for everyone else the tools don't exist, so they can't be called, discovered, or confused into use.

  • Domain-restricted, verified sign-in. Google Sign-In with email_verified + company-domain enforcement server-side (plus the hd hint and an Internal consent screen), then a further check that the email maps to an actual FUB user.

  • Read-only by construction. The connector implements GETs only. There is no write tool to misuse.

Tools

Everyone (scoped to self)

What it answers

list_my_calls

"Show me my calls this week" — each with the AI-summary note (score, topics, sentiment)

my_call_summary

Exact counts: total, answered, no-answer, talk time, distinct leads — aggregated server-side so the numbers aren't model-counted

list_my_notes

Notes the caller authored, by period

find_my_leads

Search only the caller's assigned leads

get_lead_activity

Full timeline (calls, notes, texts) of one owned lead

eod_report

The end-of-day bundle a closer actually files: my calls + my notes + my texts on the leads I touched

Managers only

What it answers

list_team

Roster with ids, emails, roles

team_activity

Per-closer leaderboard for a period, plus a no-activity list

closer_activity

"Show me everything Ethan did today" — resolves names, handles ambiguity

team_pipeline

Live lead counts, stage × closer, active stages only

find_leads

Account-wide lead search

lead_activity

Any lead's full timeline

Repository tour

src/
  index.ts            OAuthProvider wiring: /mcp API route, /authorize, /token, /register
  google-handler.ts   Google Sign-In round-trip: domain check, FUB user mapping, session props
  mcp.ts              The MCP agent: 6 closer tools + 6 manager tools (registered only if isManager)
  tools-manifest.ts   Single source of truth for which tools exist per role
  fub/                Follow Up Boss API client + per-domain query modules (calls, notes, people, team…)
  lib/                Pure helpers: period math (account TZ), formatting, manager allowlist, classification
test/                 46 unit tests (vitest) — scoping, gating, aggregation, pagination, edge cases
docs/
  design.md                Phase 1 design: requirements, architecture, alternatives considered
  implementation-plan.md   Phase 1 build plan, task-by-task with tests
  manager-view-design.md   Phase 2 design: manager role, gating decision, deferred scope
  manager-view-plan.md     Phase 2 build plan
wrangler.toml         Worker config (vars are examples — see Deploy)

Engineering details worth a look:

  • Texts have no global per-agent feed in the FUB API — the connector reconstructs "my texts" per lead from the leads the closer touched that period, and documents the one edge case that misses (design.md §8).

  • Server-side aggregation for anything numeric. Claude gets exact totals, not raw rows to count.

  • Period math in the account's timezone, not the server's — "today" means the sales team's today.

  • Pagination with caps + honesty: a stage too large to fully page returns partial counts and says so.

  • The tool manifest is tested — the set of registered manager tools must equal the manifest, so role gating can't silently drift.

Deploy your own

npm install
npm test

npx wrangler kv namespace create OAUTH_KV      # put the id in wrangler.toml
npx wrangler secret put FUB_API_KEY            # your FUB API key (read access)
npx wrangler secret put GOOGLE_CLIENT_ID       # Google OAuth web client (Internal consent screen)
npx wrangler secret put GOOGLE_CLIENT_SECRET
npx wrangler secret put COOKIE_ENCRYPTION_KEY  # any long random string
npx wrangler deploy

Set ALLOWED_EMAIL_DOMAIN, ACCOUNT_TZ, and MANAGER_EMAILS in wrangler.toml, add https://<your-worker>/callback as the Google client's redirect URI, then add https://<your-worker>/mcp as a custom connector in claude.ai (Team: Settings → Organization → Connectors). Each user clicks Connect and signs in with their company Google account — that's the whole onboarding.

Design docs

The full decision trail is in docs/: why a remote MCP connector over a local server or a hosted third party, why identity is delegated to Google, what "my data" means precisely, the FUB API capabilities verified live before building, and the phase-2 manager view with its explicitly deferred scope.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    A remote MCP server deployed on Cloudflare Workers that provides AI agents with authenticated, permission-aware access to Discord servers. It enables agents to read, search, and post messages while maintaining secure Role-Based Access Control via OAuth2.
    16
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    A remote MCP server deployed on Cloudflare Workers with OAuth, enabling tools like math operations and integration with Claude Desktop via SSE.
    -
  • A
    license
    C
    quality
    D
    maintenance
    A high-performance MCP server providing AI assistants with read-only access to the Cloudflare ecosystem. Query your entire Cloudflare infrastructure using natural language through Claude, Cursor, or any MCP-compatible client.
    390
    5
    MIT