Atlas / MCP servers / theperrygroup / Follow-Up-Boss-MCP

Follow-Up-Boss-MCPCAUTION

mcp/theperrygroup/follow-up-boss-mcp

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.

Verdict
CAUTION
Grade
C
Trust score
78 /100
Exposed tools
120 55r · 45w · 20d
Transport
stdio · streamable-http
License
MIT
Stars
2
01

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
  1. Open your AI client's apps, connectors, or MCP settings.
  2. Choose Add custom connector, Create app, or Add remote MCP server.
  3. Paste https://fub.theperry.group/mcp as the server URL.
  4. If prompted, choose Streamable HTTP and OAuth.
  5. Complete the browser sign-in and authorize access to Follow Up Boss.
  6. 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

Read from source at commit 66757cd6bcd1OBSERVED · 2026-10-11
02

Connect

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-code
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
claude-desktop
{
  "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}"
      }
    }
  }
}
03

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.

ToolRiskDescription
followupboss_add_inbox_app_messagewritereturn await adapter.add_inbox_app_message(
followupboss_add_inbox_app_notewritereturn await adapter.add_inbox_app_note(
followupboss_add_inbox_app_participantwritereturn await adapter.add_inbox_app_participant(
followupboss_add_reactionwritereturn await adapter.add_reaction(_validated_request(AddReactionToolInput, locals()))
followupboss_apply_action_planwritereturn await adapter.apply_action_plan(
followupboss_check_duplicate_personreadreturn await adapter.check_duplicate_person(
followupboss_claim_personreadreturn await adapter.claim_person(_validated_request(ClaimPersonToolInput, locals()))
followupboss_create_appointment_outcomewritereturn await adapter.create_appointment_outcome(
followupboss_create_appointment_typewritereturn await adapter.create_appointment_type(
followupboss_create_custom_fieldwritereturn await adapter.create_custom_field(
followupboss_create_deal_attachmentwritereturn await adapter.create_deal_attachment(
followupboss_create_deal_custom_fieldwritereturn await adapter.create_deal_custom_field(
followupboss_create_email_campaignwritereturn await adapter.create_email_campaign(
followupboss_create_groupwritereturn await adapter.create_group(_validated_request(CreateGroupRequest, locals()))
followupboss_create_people_relationshipwritereturn await adapter.create_people_relationship(
followupboss_create_person_attachmentwritereturn await adapter.create_person_attachment(
followupboss_create_pipelinewritereturn await adapter.create_pipeline(_validated_request(CreatePipelineRequest, locals()))
followupboss_create_pondwritereturn await adapter.create_pond(_validated_request(CreatePondRequest, locals()))
followupboss_create_stagewritereturn await adapter.create_stage(_validated_request(CreateStageRequest, locals()))
followupboss_create_teamwritereturn await adapter.create_team(_validated_request(CreateTeamRequest, locals()))
followupboss_create_templatewritereturn await adapter.create_template(_validated_request(CreateTemplateRequest, locals()))
followupboss_create_text_message_templatewritereturn await adapter.create_text_message_template(
followupboss_create_webhookwritereturn await adapter.create_webhook(_validated_request(CreateWebhookRequest, locals()))
followupboss_deactivate_inbox_appreadreturn await adapter.deactivate_inbox_app(
followupboss_delete_appointmentdestructivereturn await adapter.delete_appointment(
followupboss_delete_appointment_outcomedestructivereturn await adapter.delete_appointment_outcome(
followupboss_delete_appointment_typedestructivereturn await adapter.delete_appointment_type(
followupboss_delete_custom_fielddestructivereturn await adapter.delete_custom_field(
followupboss_delete_dealdestructivereturn await adapter.delete_deal(_validated_request(DeleteDealToolInput, locals()))
followupboss_delete_deal_attachmentdestructivereturn await adapter.delete_deal_attachment(
followupboss_delete_deal_custom_fielddestructivereturn await adapter.delete_deal_custom_field(
followupboss_delete_groupdestructivereturn await adapter.delete_group(_validated_request(DeleteGroupToolInput, locals()))
followupboss_delete_notedestructivereturn await adapter.delete_note(_validated_request(DeleteNoteToolInput, locals()))
followupboss_delete_people_relationshipdestructivereturn await adapter.delete_people_relationship(
followupboss_delete_person_attachmentdestructivereturn await adapter.delete_person_attachment(
followupboss_delete_ponddestructivereturn await adapter.delete_pond(_validated_request(DeletePondToolInput, locals()))
followupboss_delete_reactiondestructivereturn await adapter.delete_reaction(_validated_request(DeleteReactionToolInput, locals()))
followupboss_delete_stagedestructivereturn await adapter.delete_stage(_validated_request(DeleteStageToolInput, locals()))
followupboss_delete_teamdestructivereturn await adapter.delete_team(_validated_request(DeleteTeamToolInput, locals()))
followupboss_delete_templatedestructivereturn await adapter.delete_template(_validated_request(DeleteTemplateToolInput, locals()))
followupboss_delete_text_message_templatedestructivereturn await adapter.delete_text_message_template(
followupboss_delete_userdestructivereturn await adapter.delete_user(_validated_request(DeleteUserToolInput, locals()))
followupboss_delete_webhookdestructivereturn await adapter.delete_webhook(_validated_request(DeleteWebhookToolInput, locals()))
followupboss_get_appointmentreadreturn await adapter.get_appointment(_validated_request(GetAppointmentToolInput, locals()))
followupboss_get_appointment_outcomereadreturn await adapter.get_appointment_outcome(
followupboss_get_appointment_typereadreturn await adapter.get_appointment_type(
followupboss_get_automationreadreturn await adapter.get_automation(_validated_request(GetAutomationToolInput, locals()))
followupboss_get_automation_personreadreturn await adapter.get_automation_person(
followupboss_get_callreadreturn await adapter.get_call(_validated_request(GetCallToolInput, locals()))
followupboss_get_custom_fieldreadreturn await adapter.get_custom_field(_validated_request(GetCustomFieldToolInput, locals()))
followupboss_get_dealreadreturn await adapter.get_deal(_validated_request(GetDealToolInput, locals()))
followupboss_get_deal_attachmentreadreturn await adapter.get_deal_attachment(
followupboss_get_deal_custom_fieldreadreturn await adapter.get_deal_custom_field(
followupboss_get_eventreadreturn await adapter.get_event(_validated_request(GetEventToolInput, locals()))
followupboss_get_groupreadreturn await adapter.get_group(_validated_request(GetGroupToolInput, locals()))
followupboss_get_people_relationshipreadreturn await adapter.get_people_relationship(
followupboss_get_personreadreturn await adapter.get_person(_validated_request(GetPersonToolInput, locals()))
followupboss_get_person_attachmentreadreturn await adapter.get_person_attachment(
followupboss_get_pipelinereadreturn await adapter.get_pipeline(_validated_request(GetPipelineToolInput, locals()))
followupboss_get_pondreadreturn await adapter.get_pond(_validated_request(GetPondToolInput, locals()))
followupboss_get_reactionreadreturn await adapter.get_reaction(_validated_request(GetReactionToolInput, locals()))
followupboss_get_smart_listreadreturn await adapter.get_smart_list(_validated_request(GetSmartListToolInput, locals()))
followupboss_get_stagereadreturn await adapter.get_stage(_validated_request(GetStageToolInput, locals()))
followupboss_get_taskreadreturn await adapter.get_task(_validated_request(GetTaskToolInput, locals()))
followupboss_get_teamreadreturn await adapter.get_team(_validated_request(GetTeamToolInput, locals()))
followupboss_get_templatereadreturn await adapter.get_template(_validated_request(GetTemplateToolInput, locals()))
followupboss_get_text_messagereadreturn await adapter.get_text_message(_validated_request(GetTextMessageToolInput, locals()))
followupboss_get_text_message_templatereadreturn await adapter.get_text_message_template(
followupboss_get_threaded_replyreadreturn await adapter.get_threaded_reply(
followupboss_get_userreadreturn await adapter.get_user(_validated_request(GetUserToolInput, locals()))
followupboss_get_webhookreadreturn await adapter.get_webhook(_validated_request(GetWebhookToolInput, locals()))
followupboss_get_webhook_eventreadreturn await adapter.get_webhook_event(
followupboss_ignore_unclaimed_personreadreturn await adapter.ignore_unclaimed_person(
followupboss_install_inbox_appwritereturn await adapter.install_inbox_app(_validated_request(InstallInboxAppRequest, locals()))
followupboss_list_action_plan_peoplereadreturn await adapter.list_action_plan_people(
followupboss_list_appointment_outcomesreadreturn await adapter.list_appointment_outcomes(
followupboss_list_appointment_typesreadreturn await adapter.list_appointment_types(
followupboss_list_automation_peoplereadreturn await adapter.list_automation_people(
followupboss_list_callsreadreturn await adapter.list_calls(_validated_request(CallListRequest, locals()))
followupboss_list_custom_fieldsreadreturn await adapter.list_custom_fields(
followupboss_list_deal_custom_fieldsreadreturn await adapter.list_deal_custom_fields(
followupboss_list_email_campaignsreadreturn await adapter.list_email_campaigns(
followupboss_list_email_eventsreadreturn await adapter.list_email_events(_validated_request(EmailEventListRequest, locals()))
followupboss_list_groupsreadreturn await adapter.list_groups(_validated_request(GroupListRequest, locals()))
followupboss_list_inbox_app_participantsreadreturn await adapter.list_inbox_app_participants(
followupboss_list_people_relationshipsreadreturn await adapter.list_people_relationships(
followupboss_list_pondsreadreturn await adapter.list_ponds(_validated_request(PondListRequest, locals()))
followupboss_list_round_robin_groupsreadreturn await adapter.list_round_robin_groups(_validated_request(GroupListRequest, locals()))
followupboss_list_stagesreadreturn await adapter.list_stages(_validated_request(StageListRequest, locals()))
followupboss_list_team_inboxesreadreturn await adapter.list_team_inboxes(_validated_request(TeamInboxListRequest, locals()))
followupboss_list_teamsreadreturn await adapter.list_teams(_validated_request(TeamListRequest, locals()))
followupboss_list_templatesreadreturn await adapter.list_templates(_validated_request(TemplateListRequest, locals()))
followupboss_list_text_message_templatesreadreturn await adapter.list_text_message_templates(
followupboss_list_timeframesreadreturn await adapter.list_timeframes(_validated_request(TimeframeListRequest, locals()))
followupboss_list_unclaimed_peoplereadreturn await adapter.list_unclaimed_people(
followupboss_list_webhooksreadreturn await adapter.list_webhooks(_validated_request(WebhookListRequest, locals()))
followupboss_merge_templatewritereturn await adapter.merge_template(_validated_request(MergeTemplateRequest, locals()))
followupboss_merge_text_message_templatewritereturn await adapter.merge_text_message_template(
followupboss_remove_inbox_app_participantdestructivereturn await adapter.remove_inbox_app_participant(
followupboss_trigger_automationwritereturn await adapter.trigger_automation(
followupboss_update_action_plan_personwritereturn await adapter.update_action_plan_person(
followupboss_update_appointment_outcomewritereturn await adapter.update_appointment_outcome(
followupboss_update_appointment_typewritereturn await adapter.update_appointment_type(
followupboss_update_automation_personwritereturn await adapter.update_automation_person(
followupboss_update_custom_fieldwritereturn await adapter.update_custom_field(
followupboss_update_deal_attachmentwritereturn await adapter.update_deal_attachment(
followupboss_update_deal_custom_fieldwritereturn await adapter.update_deal_custom_field(
followupboss_update_email_campaignwritereturn await adapter.update_email_campaign(
followupboss_update_groupwritereturn await adapter.update_group(_validated_request(UpdateGroupToolInput, locals()))
followupboss_update_inbox_app_conversationwritereturn await adapter.update_inbox_app_conversation(
followupboss_update_inbox_app_messagewritereturn await adapter.update_inbox_app_message(
followupboss_update_notewritereturn await adapter.update_note(_validated_request(UpdateNoteToolInput, locals()))
followupboss_update_people_relationshipwritereturn await adapter.update_people_relationship(
followupboss_update_person_attachmentwritereturn await adapter.update_person_attachment(
followupboss_update_pondwritereturn await adapter.update_pond(_validated_request(UpdatePondToolInput, locals()))
followupboss_update_stagewritereturn await adapter.update_stage(_validated_request(UpdateStageToolInput, locals()))
followupboss_update_teamwritereturn await adapter.update_team(_validated_request(UpdateTeamToolInput, locals()))
followupboss_update_templatewritereturn await adapter.update_template(_validated_request(UpdateTemplateToolInput, locals()))
followupboss_update_text_message_templatewritereturn await adapter.update_text_message_template(
followupboss_update_webhookwritereturn await adapter.update_webhook(_validated_request(UpdateWebhookToolInput, locals()))
04

Trust audit

CAUTIONgrade C · trust 78/100 Install with care. The audit found things worth knowing before you trust its output.

LayerWhat it checksResult
L0Provenance & inventoryPASS
L1Static analysis of the codeWARN
L2Instruction surface (what it tells the agent)PASS
L3Class-specific surfaceWARN
L4Behavioural (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)

MEDIUMNetwork egress · net.raw_ip · CWE-200, CWE-319
src/followupboss_mcp/mcp_server.py:173
"http://127.0.0.1",
MEDIUMNetwork egress · net.raw_ip · CWE-200, CWE-319
src/followupboss_mcp/mcp_server.py:174
"http://127.0.0.1:*",
MEDIUMHard-coded secrets · secret.generic · CWE-798, CWE-321
tests/unit/test_tenant_runtime.py:156
api_key="tenant-one-agent-api-key",
MEDIUMHard-coded secrets · secret.generic · CWE-798, CWE-321
tests/unit/test_tenant_runtime.py:416
credentials=[_credential_record(api_key="tenant-runtime-api-key")],
MEDIUMFilesystem / path · mcp.destructive_tools · CWE-22, CWE-59
followupboss_delete_appointment, followupboss_delete_appointment_outcome, followupboss_delete_appointment_type, followupboss_delete_custom_field, followupboss_delete_deal, followupboss_delete_deal_att
Why it matters. 20 tool(s) can delete or overwrite
Fix. prefer a read-only mode or scoped tokens; the page states the blast radius
LOWInventory / provenance · inv.hidden_file · CWE-1104
.gitleaks.toml
.gitleaks.toml
Why it matters. hidden member outside the usual dotfiles
Fix. review its purpose
LOWCode injection · code.eval_exec · CWE-78, CWE-94, CWE-95
tests/unit/test_public_oauth_workflow.py:71
exec(_script(), {})
Why it matters. evaluates text as code
Fix. remove; use a parser or a dispatch table
LOWCode injection · code.eval_exec · CWE-78, CWE-94, CWE-95
tests/unit/test_public_oauth_workflow.py:74
exec(_script(), {})
Why it matters. evaluates text as code
Fix. remove; use a parser or a dispatch table
LOWNetwork egress · net.beacon_words · CWE-200, CWE-319
tests/unit/test_hosted_oauth.py:682
callback_url = urlparse(str(continue_link["href"]))
LOWNetwork egress · net.beacon_words · CWE-200, CWE-319
tests/unit/test_hosted_oauth.py:683
assert callback_url._replace(query="").geturl() == requested_redirect
LOWNetwork egress · net.beacon_words · CWE-200, CWE-319
tests/unit/test_hosted_oauth.py:684
callback_query = parse_qs(callback_url.query)
LOWNetwork egress · net.metadata_ip · CWE-200, CWE-319
tests/unit/test_cimd.py:272
["127.0.0.1", "10.0.0.8", "169.254.169.254", "::1", "fc00::1"],
Why it matters. cloud metadata endpoint: the classic SSRF credential grab
LOWNetwork egress · net.raw_ip · CWE-200, CWE-319
README.md:201
The URL is then `http://127.0.0.1:8000/mcp`. Do not commit real credentials. See
LOWNetwork egress · net.raw_ip · CWE-200, CWE-319
docs/mcp-validation-checklist.md:149
Then connect Inspector or another client to `http://127.0.0.1:8000/mcp`.
LOWNetwork egress · net.raw_ip · CWE-200, CWE-319
tests/integration/test_hosted_auth_integration.py:452
with TestClient(server.streamable_http_app(), base_url="http://127.0.0.1:8000") as client:
LOWObfuscation / stealth · obf.base64_blob · CWE-506, CWE-94
docs/followupboss-endpoint-manifest.json:2642
"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,
LOWPrompt injection · prompt.transfer_instruction · CWE-94, CWE-1427
SECURITY.md:13
3. Email `[email protected]` to request revocation of the hosted MCP access and refresh tokens.
Why it matters. an instruction to move sensitive data to an outside destination
Fix. remove; a skill never needs the user's secrets off the machine
LOWPrompt injection · prompt.transfer_instruction · CWE-94, CWE-1427
docs/final-validation-report.md:482
- 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
Why it matters. an instruction to move sensitive data to an outside destination
Fix. remove; a skill never needs the user's secrets off the machine
LOWPrompt injection · prompt.transfer_instruction · CWE-94, CWE-1427
docs/hosted-deployment-guide.md:557
The hosted server can report callback, token exchange, identity lookup, and tenant provisioning
Why it matters. an instruction to move sensitive data to an outside destination
Fix. remove; a skill never needs the user's secrets off the machine
INFOPrompt injection · prompt.credential_read · CWE-94, CWE-1427
CONTRIBUTING.md:66
Both targets auto-load a repository-local `.env` when present, so manual export is optional for the common local workflow.
Why it matters. asks the agent to read credentials
INFOPrompt injection · prompt.credential_read · CWE-94, CWE-1427
SECURITY.md:5
Do not report vulnerabilities, access tokens, credentials, or customer data in a public GitHub
Why it matters. asks the agent to read credentials
INFOPrompt injection · prompt.credential_read · CWE-94, CWE-1427
TASKS.followupboss-mcp.md:86
- `DONE` Restore and relink the MCP validation runbook, then make `make live-identity-check` auto-load a repository-local `.env` when present.
Why it matters. asks the agent to read credentials
INFOPrompt injection · prompt.credential_read · CWE-94, CWE-1427
docs/final-validation-report.md:482
- 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
Why it matters. asks the agent to read credentials
INFOPrompt injection · prompt.credential_read · CWE-94, CWE-1427
docs/hosted-deployment-guide.md:574
1. read non-secret tenant and credential metadata from PostgreSQL
Why it matters. asks the agent to read credentials
INFOPrompt injection · prompt.fetch_and_trust · CWE-94, CWE-1427
README.md:116
Read [Security](https://github.com/theperrygroup/Follow-Up-Boss-MCP/blob/main/docs/security.md) for
Why it matters. remote text is to be obeyed as instructions

Gates applied: no_behavioural_pass.

Audited 2026-10-11 · audit v0.4.1 · source sha 66757cd6bcd1full audit observations/trust-audit/mcp-server/theperrygroup__follow-up-boss-mcp.json · Report an issue / request a re-scan
05

Audit history

Every audit this server has had. A grade with a past is a grade somebody is still checking.

DateSourceVerdictGradeScoreChange
2026-10-1166757cd6bcd1CAUTIONC78first audit
06

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.

Advertisement