Follow-Up-Boss-MCPCAUTION
Connect the Follow Up Boss real estate CRM to ChatGPT, Claude, Cursor, and other AI assistants with one hosted MCP URL. OAuth-enabled server and typed Python SDK.
Overview
From the repository's own README, as read at the audited commit. Badges and raw HTML are left out.
<img src="https://raw.githubusercontent.com/theperrygroup/Follow-Up-Boss-MCP/main/src/followupboss_mcp/assets/follow-up-boss-logo.png" width="112" alt="Follow Up Boss MCP server logo"
Connect ChatGPT, Claude, Cursor, and other AI assistants to the Follow Up Boss real estate CRM through the Model Context Protocol (MCP). Search leads, review activity, manage tasks and appointments, work with deals, and run CRM workflows using natural language.
[](https://github.com/theperrygroup/Follow-Up-Boss-MCP/actions/workflows/ci.yml) [](https://www.python.org/) [](https://modelcontextprotocol.io/) [](https://github.com/theperrygroup/Follow-Up-Boss-MCP/blob/main/LICENSE)
Connect with one URL
Most people only need the hosted MCP server. You do not need to clone this repository, install Python, run a server, or paste a Follow Up Boss API key into your AI client.
MCP server URL
https://fub.theperry.group/mcp
- Open your AI client's apps, connectors, or MCP settings.
- Choose Add custom connector, Create app, or Add remote MCP server.
- Paste
https://fub.theperry.group/mcpas the server URL. - If prompted, choose Streamable HTTP and OAuth.
- Complete the browser sign-in and authorize access to Follow Up Boss.
- Enable the new connector in a conversation and ask: “Who am I in Follow Up Boss?”
The client should return your Follow Up Boss identity. That confirms the connection and authorization flow are working.
The endpoint supports MCP 2026-07-28 discovery and sessionless requests while retaining the legacy initialization handshake for c
66757cd6bcd1OBSERVED · 2026-10-11Connect
Built from this server's own package name, version and transport as found in its source — not copied from anyone's documentation, so it cannot drift against a page we do not control. Replace the environment placeholders with a token scoped to the least it needs.
claude mcp add followupboss-mcp --env ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} --env FOLLOWUPBOSS_BATTLE_TEST_ANTHROPIC_API_KEY=${FOLLOWUPBOSS_BATTLE_TEST_ANTHROPIC_API_KEY} --env FOLLOWUPBOSS_BATTLE_TEST_OPENAI_API_KEY=${FOLLOWUPBOSS_BATTLE_TEST_OPENAI_API_KEY} --env OPENAI_API_KEY=${OPENAI_API_KEY} -- uvx followupboss-mcp{
"mcpServers": {
"followupboss-mcp": {
"command": "uvx",
"args": [
"followupboss-mcp"
],
"env": {
"ANTHROPIC_API_KEY": "${ANTHROPIC_API_KEY}",
"FOLLOWUPBOSS_BATTLE_TEST_ANTHROPIC_API_KEY": "${FOLLOWUPBOSS_BATTLE_TEST_ANTHROPIC_API_KEY}",
"FOLLOWUPBOSS_BATTLE_TEST_OPENAI_API_KEY": "${FOLLOWUPBOSS_BATTLE_TEST_OPENAI_API_KEY}",
"OPENAI_API_KEY": "${OPENAI_API_KEY}"
}
}
}
}Exposed tools (120)
55 read · 45 write · 20 destructive. Blast radius: 20 tools can delete or overwrite — an agent that can be talked into calling a tool can be talked into calling this one.
| Tool | Risk | Description |
|---|---|---|
followupboss_add_inbox_app_message | write | return await adapter.add_inbox_app_message( |
followupboss_add_inbox_app_note | write | return await adapter.add_inbox_app_note( |
followupboss_add_inbox_app_participant | write | return await adapter.add_inbox_app_participant( |
followupboss_add_reaction | write | return await adapter.add_reaction(_validated_request(AddReactionToolInput, locals())) |
followupboss_apply_action_plan | write | return await adapter.apply_action_plan( |
followupboss_check_duplicate_person | read | return await adapter.check_duplicate_person( |
followupboss_claim_person | read | return await adapter.claim_person(_validated_request(ClaimPersonToolInput, locals())) |
followupboss_create_appointment_outcome | write | return await adapter.create_appointment_outcome( |
followupboss_create_appointment_type | write | return await adapter.create_appointment_type( |
followupboss_create_custom_field | write | return await adapter.create_custom_field( |
followupboss_create_deal_attachment | write | return await adapter.create_deal_attachment( |
followupboss_create_deal_custom_field | write | return await adapter.create_deal_custom_field( |
followupboss_create_email_campaign | write | return await adapter.create_email_campaign( |
followupboss_create_group | write | return await adapter.create_group(_validated_request(CreateGroupRequest, locals())) |
followupboss_create_people_relationship | write | return await adapter.create_people_relationship( |
followupboss_create_person_attachment | write | return await adapter.create_person_attachment( |
followupboss_create_pipeline | write | return await adapter.create_pipeline(_validated_request(CreatePipelineRequest, locals())) |
followupboss_create_pond | write | return await adapter.create_pond(_validated_request(CreatePondRequest, locals())) |
followupboss_create_stage | write | return await adapter.create_stage(_validated_request(CreateStageRequest, locals())) |
followupboss_create_team | write | return await adapter.create_team(_validated_request(CreateTeamRequest, locals())) |
followupboss_create_template | write | return await adapter.create_template(_validated_request(CreateTemplateRequest, locals())) |
followupboss_create_text_message_template | write | return await adapter.create_text_message_template( |
followupboss_create_webhook | write | return await adapter.create_webhook(_validated_request(CreateWebhookRequest, locals())) |
followupboss_deactivate_inbox_app | read | return await adapter.deactivate_inbox_app( |
followupboss_delete_appointment | destructive | return await adapter.delete_appointment( |
followupboss_delete_appointment_outcome | destructive | return await adapter.delete_appointment_outcome( |
followupboss_delete_appointment_type | destructive | return await adapter.delete_appointment_type( |
followupboss_delete_custom_field | destructive | return await adapter.delete_custom_field( |
followupboss_delete_deal | destructive | return await adapter.delete_deal(_validated_request(DeleteDealToolInput, locals())) |
followupboss_delete_deal_attachment | destructive | return await adapter.delete_deal_attachment( |
followupboss_delete_deal_custom_field | destructive | return await adapter.delete_deal_custom_field( |
followupboss_delete_group | destructive | return await adapter.delete_group(_validated_request(DeleteGroupToolInput, locals())) |
followupboss_delete_note | destructive | return await adapter.delete_note(_validated_request(DeleteNoteToolInput, locals())) |
followupboss_delete_people_relationship | destructive | return await adapter.delete_people_relationship( |
followupboss_delete_person_attachment | destructive | return await adapter.delete_person_attachment( |
followupboss_delete_pond | destructive | return await adapter.delete_pond(_validated_request(DeletePondToolInput, locals())) |
followupboss_delete_reaction | destructive | return await adapter.delete_reaction(_validated_request(DeleteReactionToolInput, locals())) |
followupboss_delete_stage | destructive | return await adapter.delete_stage(_validated_request(DeleteStageToolInput, locals())) |
followupboss_delete_team | destructive | return await adapter.delete_team(_validated_request(DeleteTeamToolInput, locals())) |
followupboss_delete_template | destructive | return await adapter.delete_template(_validated_request(DeleteTemplateToolInput, locals())) |
followupboss_delete_text_message_template | destructive | return await adapter.delete_text_message_template( |
followupboss_delete_user | destructive | return await adapter.delete_user(_validated_request(DeleteUserToolInput, locals())) |
followupboss_delete_webhook | destructive | return await adapter.delete_webhook(_validated_request(DeleteWebhookToolInput, locals())) |
followupboss_get_appointment | read | return await adapter.get_appointment(_validated_request(GetAppointmentToolInput, locals())) |
followupboss_get_appointment_outcome | read | return await adapter.get_appointment_outcome( |
followupboss_get_appointment_type | read | return await adapter.get_appointment_type( |
followupboss_get_automation | read | return await adapter.get_automation(_validated_request(GetAutomationToolInput, locals())) |
followupboss_get_automation_person | read | return await adapter.get_automation_person( |
followupboss_get_call | read | return await adapter.get_call(_validated_request(GetCallToolInput, locals())) |
followupboss_get_custom_field | read | return await adapter.get_custom_field(_validated_request(GetCustomFieldToolInput, locals())) |
followupboss_get_deal | read | return await adapter.get_deal(_validated_request(GetDealToolInput, locals())) |
followupboss_get_deal_attachment | read | return await adapter.get_deal_attachment( |
followupboss_get_deal_custom_field | read | return await adapter.get_deal_custom_field( |
followupboss_get_event | read | return await adapter.get_event(_validated_request(GetEventToolInput, locals())) |
followupboss_get_group | read | return await adapter.get_group(_validated_request(GetGroupToolInput, locals())) |
followupboss_get_people_relationship | read | return await adapter.get_people_relationship( |
followupboss_get_person | read | return await adapter.get_person(_validated_request(GetPersonToolInput, locals())) |
followupboss_get_person_attachment | read | return await adapter.get_person_attachment( |
followupboss_get_pipeline | read | return await adapter.get_pipeline(_validated_request(GetPipelineToolInput, locals())) |
followupboss_get_pond | read | return await adapter.get_pond(_validated_request(GetPondToolInput, locals())) |
followupboss_get_reaction | read | return await adapter.get_reaction(_validated_request(GetReactionToolInput, locals())) |
followupboss_get_smart_list | read | return await adapter.get_smart_list(_validated_request(GetSmartListToolInput, locals())) |
followupboss_get_stage | read | return await adapter.get_stage(_validated_request(GetStageToolInput, locals())) |
followupboss_get_task | read | return await adapter.get_task(_validated_request(GetTaskToolInput, locals())) |
followupboss_get_team | read | return await adapter.get_team(_validated_request(GetTeamToolInput, locals())) |
followupboss_get_template | read | return await adapter.get_template(_validated_request(GetTemplateToolInput, locals())) |
followupboss_get_text_message | read | return await adapter.get_text_message(_validated_request(GetTextMessageToolInput, locals())) |
followupboss_get_text_message_template | read | return await adapter.get_text_message_template( |
followupboss_get_threaded_reply | read | return await adapter.get_threaded_reply( |
followupboss_get_user | read | return await adapter.get_user(_validated_request(GetUserToolInput, locals())) |
followupboss_get_webhook | read | return await adapter.get_webhook(_validated_request(GetWebhookToolInput, locals())) |
followupboss_get_webhook_event | read | return await adapter.get_webhook_event( |
followupboss_ignore_unclaimed_person | read | return await adapter.ignore_unclaimed_person( |
followupboss_install_inbox_app | write | return await adapter.install_inbox_app(_validated_request(InstallInboxAppRequest, locals())) |
followupboss_list_action_plan_people | read | return await adapter.list_action_plan_people( |
followupboss_list_appointment_outcomes | read | return await adapter.list_appointment_outcomes( |
followupboss_list_appointment_types | read | return await adapter.list_appointment_types( |
followupboss_list_automation_people | read | return await adapter.list_automation_people( |
followupboss_list_calls | read | return await adapter.list_calls(_validated_request(CallListRequest, locals())) |
followupboss_list_custom_fields | read | return await adapter.list_custom_fields( |
followupboss_list_deal_custom_fields | read | return await adapter.list_deal_custom_fields( |
followupboss_list_email_campaigns | read | return await adapter.list_email_campaigns( |
followupboss_list_email_events | read | return await adapter.list_email_events(_validated_request(EmailEventListRequest, locals())) |
followupboss_list_groups | read | return await adapter.list_groups(_validated_request(GroupListRequest, locals())) |
followupboss_list_inbox_app_participants | read | return await adapter.list_inbox_app_participants( |
followupboss_list_people_relationships | read | return await adapter.list_people_relationships( |
followupboss_list_ponds | read | return await adapter.list_ponds(_validated_request(PondListRequest, locals())) |
followupboss_list_round_robin_groups | read | return await adapter.list_round_robin_groups(_validated_request(GroupListRequest, locals())) |
followupboss_list_stages | read | return await adapter.list_stages(_validated_request(StageListRequest, locals())) |
followupboss_list_team_inboxes | read | return await adapter.list_team_inboxes(_validated_request(TeamInboxListRequest, locals())) |
followupboss_list_teams | read | return await adapter.list_teams(_validated_request(TeamListRequest, locals())) |
followupboss_list_templates | read | return await adapter.list_templates(_validated_request(TemplateListRequest, locals())) |
followupboss_list_text_message_templates | read | return await adapter.list_text_message_templates( |
followupboss_list_timeframes | read | return await adapter.list_timeframes(_validated_request(TimeframeListRequest, locals())) |
followupboss_list_unclaimed_people | read | return await adapter.list_unclaimed_people( |
followupboss_list_webhooks | read | return await adapter.list_webhooks(_validated_request(WebhookListRequest, locals())) |
followupboss_merge_template | write | return await adapter.merge_template(_validated_request(MergeTemplateRequest, locals())) |
followupboss_merge_text_message_template | write | return await adapter.merge_text_message_template( |
followupboss_remove_inbox_app_participant | destructive | return await adapter.remove_inbox_app_participant( |
followupboss_trigger_automation | write | return await adapter.trigger_automation( |
followupboss_update_action_plan_person | write | return await adapter.update_action_plan_person( |
followupboss_update_appointment_outcome | write | return await adapter.update_appointment_outcome( |
followupboss_update_appointment_type | write | return await adapter.update_appointment_type( |
followupboss_update_automation_person | write | return await adapter.update_automation_person( |
followupboss_update_custom_field | write | return await adapter.update_custom_field( |
followupboss_update_deal_attachment | write | return await adapter.update_deal_attachment( |
followupboss_update_deal_custom_field | write | return await adapter.update_deal_custom_field( |
followupboss_update_email_campaign | write | return await adapter.update_email_campaign( |
followupboss_update_group | write | return await adapter.update_group(_validated_request(UpdateGroupToolInput, locals())) |
followupboss_update_inbox_app_conversation | write | return await adapter.update_inbox_app_conversation( |
followupboss_update_inbox_app_message | write | return await adapter.update_inbox_app_message( |
followupboss_update_note | write | return await adapter.update_note(_validated_request(UpdateNoteToolInput, locals())) |
followupboss_update_people_relationship | write | return await adapter.update_people_relationship( |
followupboss_update_person_attachment | write | return await adapter.update_person_attachment( |
followupboss_update_pond | write | return await adapter.update_pond(_validated_request(UpdatePondToolInput, locals())) |
followupboss_update_stage | write | return await adapter.update_stage(_validated_request(UpdateStageToolInput, locals())) |
followupboss_update_team | write | return await adapter.update_team(_validated_request(UpdateTeamToolInput, locals())) |
followupboss_update_template | write | return await adapter.update_template(_validated_request(UpdateTemplateToolInput, locals())) |
followupboss_update_text_message_template | write | return await adapter.update_text_message_template( |
followupboss_update_webhook | write | return await adapter.update_webhook(_validated_request(UpdateWebhookToolInput, locals())) |
Trust audit
CAUTIONgrade C · trust 78/100 Install with care. The audit found things worth knowing before you trust its output.
| Layer | What it checks | Result |
|---|---|---|
| L0 | Provenance & inventory | PASS |
| L1 | Static analysis of the code | WARN |
| L2 | Instruction surface (what it tells the agent) | PASS |
| L3 | Class-specific surface | WARN |
| L4 | Behavioural (sandbox) | SKIPPED |
What the source does
- Filesystem
- declared (1 observation(s))
- Network
- declared (3 observation(s))
- Shell
- none-observed
- Dependencies
- pinned
- Secrets in source
- found
Findings (25)
"http://127.0.0.1",
"http://127.0.0.1:*",
api_key="tenant-one-agent-api-key",
credentials=[_credential_record(api_key="tenant-runtime-api-key")],
followupboss_delete_appointment, followupboss_delete_appointment_outcome, followupboss_delete_appointment_type, followupboss_delete_custom_field, followupboss_delete_deal, followupboss_delete_deal_att
.gitleaks.toml
exec(_script(), {})exec(_script(), {})callback_url = urlparse(str(continue_link["href"]))
assert callback_url._replace(query="").geturl() == requested_redirect
callback_query = parse_qs(callback_url.query)
["127.0.0.1", "10.0.0.8", "169.254.169.254", "::1", "fc00::1"],
The URL is then `http://127.0.0.1:8000/mcp`. Do not commit real credentials. See
Then connect Inspector or another client to `http://127.0.0.1:8000/mcp`.
with TestClient(server.streamable_http_app(), base_url="http://127.0.0.1:8000") as client:
"URL Safe Base64 Decode: For languages like Ruby, make sure to use the [urlsafe_decode64](https://ruby-doc.org/stdlib-2.4.1/libdoc/base64/rdoc/Base64.html#method-i-urlsafe_decode64) method. Otherwise,
3. Email `[email protected]` to request revocation of the hosted MCP access and refresh tokens.
- remaining live blockers: owner-only webhook access on the current credential still downgrades the automated webhook CRUD path to a skip, inbox app fixture setup, email marketing write fixtures, and
The hosted server can report callback, token exchange, identity lookup, and tenant provisioning
Both targets auto-load a repository-local `.env` when present, so manual export is optional for the common local workflow.
Do not report vulnerabilities, access tokens, credentials, or customer data in a public GitHub
- `DONE` Restore and relink the MCP validation runbook, then make `make live-identity-check` auto-load a repository-local `.env` when present.
- remaining live blockers: owner-only webhook access on the current credential still downgrades the automated webhook CRUD path to a skip, inbox app fixture setup, email marketing write fixtures, and
1. read non-secret tenant and credential metadata from PostgreSQL
Read [Security](https://github.com/theperrygroup/Follow-Up-Boss-MCP/blob/main/docs/security.md) for
Gates applied: no_behavioural_pass.
66757cd6bcd1full audit observations/trust-audit/mcp-server/theperrygroup__follow-up-boss-mcp.json · Report an issue / request a re-scanAudit history
Every audit this server has had. A grade with a past is a grade somebody is still checking.
| Date | Source | Verdict | Grade | Score | Change |
|---|---|---|---|---|---|
| 2026-10-11 | 66757cd6bcd1 | CAUTION | C | 78 | first audit |
Questions
What is the Follow-Up-Boss-MCP MCP server?
Connect the Follow Up Boss real estate CRM to ChatGPT, Claude, Cursor, and other AI assistants with one hosted MCP URL. OAuth-enabled server and typed Python SDK.
What tools does Follow-Up-Boss-MCP expose?
120 in total: 55 read-only, 45 that write, and 20 that can delete or overwrite (followupboss_delete_appointment, followupboss_delete_appointment_outcome, followupboss_delete_appointment_type, followupboss_delete_custom_field, followupboss_delete_deal). Every one is listed on this page with its risk.
Is Follow-Up-Boss-MCP safe to connect to an agent?
With care. The audit graded it C (78/100) and found 25 things worth knowing before you trust this server, listed below with the exact line each was found on. Separately from the audit: 20 of its tools can destroy data, so scope the token you give it to what you actually need.
What credentials does Follow-Up-Boss-MCP need?
It reads ANTHROPIC_API_KEY, FOLLOWUPBOSS_BATTLE_TEST_ANTHROPIC_API_KEY, FOLLOWUPBOSS_BATTLE_TEST_OPENAI_API_KEY, OPENAI_API_KEY and TENANT_DATABASE_URL_SECRET_ARN from the environment. Give it a token scoped to the least it needs — an agent that can be talked into calling a tool can be talked into calling it with your credentials.
How does Follow-Up-Boss-MCP run?
It speaks stdio and streamable-http, so it runs as a local process your client starts. It is published on PyPI as followupboss-mcp.
How current is this page?
The grade is for one exact copy of the source (66757cd6bcd1), read on 2026-10-11. The repository is watched and re-audited when it changes.