Skip to main content
Glama
LucasNS231

redtail-mcp

by LucasNS231
README.md
# redtail-mcp

An agent-agnostic [FastMCP](https://github.com/jlowin/fastmcp) stdio server
that exposes the [Redtail CRM](https://redtailtechnology.com) public API
(TWAPI) as [Model Context Protocol](https://modelcontextprotocol.io) tools.

Any MCP client can drive it — Claude Desktop, Claude Code, or a custom agent
built on the Claude Agent SDK or another MCP-aware framework. The server
speaks JSON-RPC over stdio; stdout is the protocol stream, all logs go to
stderr.

Two things distinguish it from a thin wrapper:

- **Firm-identity externalization.** No advisor names, user ids, rep codes,
  or activity-code labels live in the tool modules. `config.py` loads them
  from JSON env vars (with placeholder defaults) so the same binary serves
  a different firm by pointing at a different `.env`.
- **Multi-user attribution.** Optionally set `REDTAIL_USER_KEYS` to a map of
  `{name: durable_user_key}`. When a write tool is called with an
  `acting_user` argument, the server authenticates as that user via Redtail's
  `UserKeyAuth`, so the new record's `added_by` is stamped with the real
  person — on one shared API subscription.

## Install

Requires Python 3.11 and [`uv`](https://docs.astral.sh/uv/).

```
git clone https://github.com/your-org/redtail-mcp.git
cd redtail-mcp
uv sync
cp .env.example .env    # then fill in the three required secrets
```

## Configuration

Everything the server reads at boot is documented inline in
[`.env.example`](.env.example). At minimum you must set:

| Variable | Purpose |
|---|---|
| `TWAPI_API_KEY` | TWAPI subscription key from the Redtail developer portal. |
| `TWAPI_USERNAME` | Shared-account login (fallback attribution for writes). |
| `TWAPI_PASSWORD` | Shared-account password. |

Optional — override placeholder defaults to make the server your firm's:

| Variable | Purpose |
|---|---|
| `REDTAIL_USER_KEYS` | JSON `{name: user_key}` for per-user write attribution. Secret; env-only. |
| `REDTAIL_DEFAULT_USER_ID` | User id used as the attendee floor / default owner. |
| `REDTAIL_TEAM_MAP` | JSON `{name: user_id}` — resolves friendly names in tool args. |
| `REDTAIL_REP_ADVISOR_MAP` | JSON `{rep_code: {routing}}` — drives onboarding + call defaults. |
| `REDTAIL_ACTIVITY_CODE_LEGEND` | JSON `{id: name}` — friendly names for activity codes. |
| `REDTAIL_ALLOWED_TOOLS` | CSV allowlist — trim the exposed catalog without removing capability. |
| `TWAPI_BASE_URL`, `TWAPI_MAX_RETRIES`, `TWAPI_CONCURRENCY`, `LOG_LEVEL` | Transport tuning. |

The server fails loud (`sys.exit 2`) at boot on a missing credential or a
malformed JSON map — a misconfigured deployment dies instead of silently
mis-routing writes.

## Register with Claude Desktop

Add an entry under `mcpServers` in `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "redtail": {
      "command": "uv",
      "args": [
        "--directory", "/path/to/redtail-mcp",
        "run", "python", "server.py"
      ]
    }
  }
}
```

Restart Claude Desktop; the `redtail` server should appear in the MCP panel
with its tools listed.

## Register with Claude Code

```
claude mcp add redtail -- uv --directory /path/to/redtail-mcp run python server.py
```

Or add it to a project's `.mcp.json`:

```json
{
  "mcpServers": {
    "redtail": {
      "command": "uv",
      "args": ["--directory", "/path/to/redtail-mcp", "run", "python", "server.py"]
    }
  }
}
```

## Tool reference

43 tools are implemented across four modules. The default catalog exposes
them all; narrow it with `REDTAIL_ALLOWED_TOOLS`.

### Activities (`tools_activities.py`)

| Tool | Purpose | Key args |
|---|---|---|
| `get_agenda` | One person's or the team's activities for today / a range. | `user_ids?`, `date_range?` |
| `get_overdue_activities` | Every open activity past its due date. | `user_ids?` |
| `get_activities_in_range` | Activities in an explicit ISO date range. | `start`, `end`, `user_ids?` |
| `get_activity` | Fetch a single activity by id. | `activity_id` |
| `get_contact_activity_history` | Activity history for one contact. | `contact_id`, `kind?` |
| `create_task` | Create a task activity (owner defaults resolved from name). | `subject`, `contact_id?`, `due_date?`, `user_ids?`, `acting_user?` |
| `log_call` | Log a completed phone call against a contact. | `contact_id`, `subject`, `note?`, `acting_user?` |
| `add_note` | Add a durable note as a logged activity. | `contact_id`, `note`, `acting_user?` |
| `complete_activity` | Mark an activity done with an optional result. | `activity_id`, `result?`, `acting_user?` |
| `reschedule_activity` | Move an activity's start / end. | `activity_id`, `new_start`, `new_end?`, `acting_user?` |
| `assign_activity` | Change an activity's owner / attendees. | `activity_id`, `user_ids`, `acting_user?` |
| `create_activity` | General activity create with full field control. | `subject`, `activity_code_id`, `start`, `end?`, ... |
| `update_activity` | Patch fields on an existing activity. | `activity_id`, `patch`, `acting_user?` |
| `batch_update_activities` | Apply the same patch across many activities. | `activity_ids`, `patch`, `acting_user?` |
| `delete_activity` | Delete an activity. | `activity_id`, `acting_user?` |

### Contacts (`tools_contacts.py`)

| Tool | Purpose | Key args |
|---|---|---|
| `search_contacts` | Full-text contact search. | `q`, `limit?` |
| `find_contact_by_email` | Exact-email lookup. | `email` |
| `get_contact` | Full contact record. | `contact_id` |
| `get_contact_notes` | Notes attached to a contact. | `contact_id` |
| `get_important_info` | Memberships / important-info sheet. | `contact_id` |
| `get_professional_contacts` | Attorneys / CPAs / other advisors linked to a contact. | `contact_id` |
| `get_family` | Family + spouse links. | `contact_id` |
| `get_contact_photo` | Profile photo URL. | `contact_id` |
| `get_household` | Household record. | `household_id` |
| `resolve_contact` | Fuzzy identity resolution — "which contact do you mean?". | `q` |
| `create_contact` | Create a new contact. | `first_name`, `last_name`, ... |
| `update_contact` | Patch fields on an existing contact. | `contact_id`, `patch`, `acting_user?` |
| `add_phone` | Add a phone number. | `contact_id`, `number`, `type?`, `acting_user?` |
| `add_email` | Add an email address. | `contact_id`, `email`, `type?`, `acting_user?` |
| `add_address` | Add a mailing address. | `contact_id`, `street`, `city`, `state`, `zip`, `type?`, `acting_user?` |
| `link_family` | Link a family relationship. | `contact_id`, `related_id`, `relation`, `acting_user?` |
| `add_tag_member` | Put a contact on a tag (cohort). | `tag`, `contact_id`, `acting_user?` |
| `add_contact_keyword` | Add a keyword to a contact. | `contact_id`, `keyword`, `acting_user?` |
| `update_important_info` | Patch important-info fields. | `contact_id`, `patch`, `acting_user?` |
| `remove_contact_subresource` | Remove a phone / email / address / etc. | `contact_id`, `resource`, `resource_id`, `acting_user?` |

### Onboarding (`tools_onboarding.py`)

| Tool | Purpose | Key args |
|---|---|---|
| `create_onboarding_tasks` | Example workflow: create a bundle of onboarding tasks for a new client, routed off the rep code. Override the plan by passing `tasks=[...]`. | `contact_id`, `rep_code`, `effective_date?`, `tasks?`, `acting_user?` |

### Reference (`tools_reference.py`)

| Tool | Purpose | Key args |
|---|---|---|
| `list_database_users` | Enumerate every user in the Redtail database. | — |
| `list_activity_codes` | Enumerate activity codes. | — |
| `list_contact_categories` | Enumerate contact categories. | — |
| `list_tags` | Enumerate tags. | — |
| `list_tag_members` | List contacts on a tag (cohort). | `tag` |
| `list_contacts` | Filtered contact list (category / status / updated-since). | `category_id?`, `status_id?`, `updated_since?` |
| `remove_tag_member` | Take a contact off a tag. | `tag`, `contact_id` |

## Usage examples

Once the server is registered, natural-language prompts flow through the
tools. All examples below use the fictional client `Jane Sample`.

**Read.** "Who's on my calendar this week?" — the client calls `get_agenda`
with `user_ids` resolved from the caller's name via `REDTAIL_TEAM_MAP`.

**Write with default attribution.** "Log a call with Jane Sample: reviewed
her rollover paperwork, will send the DocuSign tomorrow." — the client
calls `log_call(contact_id=..., subject="Rollover paperwork review",
note="...")`. The record's `added_by` is the shared TWAPI user.

**Write with per-user attribution.** With `REDTAIL_USER_KEYS` populated and
an agent that tags the inbound user (e.g. `[alice|100001]`), pass
`acting_user="alice"` on any write tool. The server switches to
`UserKeyAuth` for that call and Redtail stamps the record as coming from
Alice — even though the API subscription is shared.

**Onboarding.** "Start onboarding for Jane Sample, rep code REP01." —
`create_onboarding_tasks(contact_id=..., rep_code="REP01")` creates the
example task bundle, routed to the owner mapped in
`REDTAIL_REP_ADVISOR_MAP`.

## Development

```
uv sync --extra dev
uv run pytest
uv run python tests/smoke_stdio.py
```

`DESIGN.md` covers the architecture — request path, auth model, per-user
attribution decision, truncation contract, and the firm-identity
externalization pattern.

## License

MIT. See [LICENSE](LICENSE).