Skip to main content
Glama

lofty-mcp

An MCP server that gives Claude tools to work with a Lofty CRM account — leads, tasks/appointments, calendar, communications, and notes — by calling Lofty's REST API directly.

For the reasoning behind how this project is built (why REST instead of lofty-cli, the auth-scheme quirk, the HTTP-mode security tradeoffs), see DESIGN.md. This document is about what the project does and how to run it.

Concepts

  • MCP (Model Context Protocol) is the protocol Claude uses to discover and call tools exposed by an external server. This project is one such server: it doesn't talk to Claude directly, it registers tools (e.g. lofty_leads_list, lofty_notes_create) that a client (Claude Code, Cowork, etc.) can call on your behalf.

  • One tool module per Lofty resource. src/lofty_mcp/tools/ has one file per resource — leads.py, tasks.py, calendar.py, communication.py, notes.py, whoami.py — each registering the MCP tools for that resource's REST endpoints. 35 tools total today; see specs/rest/index.json for what's implemented vs. pending.

  • Tool annotations. Every tool carries MCP ToolAnnotations (readOnlyHint/destructiveHint/idempotentHint) — reads are marked read-only, creates/sends are non-idempotent, updates/deletes are destructive — so a well-behaved client can decide what to auto-approve vs. confirm with you. These are hints, not enforcement: nothing in the server itself blocks a destructive call.

  • Two transports, two use cases:

    • stdio (default) — Claude Code spawns the server as a local subprocess per session. This is how you'd normally use it day to day.

    • Streamable HTTP — the server runs as a long-lived process bound to a port, for remote clients (e.g. Cowork's "Add custom connector") that can't spawn a process on your machine. Needs a real HTTPS URL in front of it; see "Connecting to a remote client" below.

  • Auth is a single API key, not OAuth. LOFTY_API_KEY is a personal-access token from the Lofty CRM, sent as Authorization: token <key> on every request. There's no refresh flow — if it stops working, you regenerate it in the CRM.

Related MCP server: Rentalot MCP Server

Initial setup

You need:

  1. Docker. Everything — build, run, test — happens inside containers; nothing from this project's Python dependency stack is installed on your host. Install Docker Desktop (macOS/Windows) or Docker Engine (Linux), and confirm it's running with docker info.

  2. A Lofty API key. In the Lofty CRM, go to Settings → Integrations → API and generate a personal-access key.

  3. jq, used by several Makefile targets to edit JSON config in place: brew install jq (macOS) or your distro's package manager.

  4. (Only if you'll use HTTP mode / remote connectors) cloudflared: brew install cloudflare/cloudflare/cloudflared.

Then create a .env file at the repo root (already in .gitignore — never commit it):

LOFTY_API_KEY=your-key-here

If the key stops working, tool calls fail with a clear LoftyApiError pointing back at Settings → Integrations → API — there's no refresh path, so regenerating the key is always the fix.

Project layout

src/lofty_mcp/
  server.py         entrypoint: MCPServer, lifespan-injected httpx client, transport selection
  rest_client.py     HTTP client: base URL, auth header, error mapping
  tools/             one module per resource (leads, tasks, calendar, communication, notes, whoami)
specs/
  openapi.json        Lofty's full OpenAPI spec (source of truth)
  rest/*.json          generated per-resource summaries (see specs/README.md)
scripts/
  generate_rest_specs.py   regenerates specs/rest/*.json from specs/openapi.json
  mcp_test_harness.py       reliable stdio request/response harness for smoke-testing
Dockerfile             plain python:3.12-slim, no CLI binary needed
Makefile               build/run/register/teardown commands (see below)
DESIGN.md              why the project is built this way (not how to run it)

Building and testing

make help          # list all commands
make build          # docker build the image
make test           # run scripts/mcp_test_harness.py against it (real API calls)

Connecting to Claude Code

make up             # builds the image, registers it in .mcp.json

This writes a lofty entry into the project's .mcp.json (command: docker, args: [run, -i, --rm, --env-file, <path>, lofty-mcp:py]). Claude Code auto-detects project-scoped .mcp.json files — restart the session (or run /mcp) and approve the new server when prompted. make down removes the entry.

Each session spawns a fresh docker run -i --rm process over stdio; Docker tears it down automatically when the session ends, so there's no separate container lifecycle to manage here.

Connecting to a remote client (e.g. Cowork)

Remote connectors need a real HTTPS URL, since they run in the cloud and can't spawn processes on your machine. There are two ways to get one, depending on how long-lived and trusted the deployment needs to be.

Quick tunnel — short-lived local testing

make http-up         # runs the server in HTTP mode + a cloudflared quick tunnel
make http-down        # tears both down

make http-up binds the server to 0.0.0.0:8000 and fronts it with cloudflared tunnel --url — no Cloudflare account needed, but the URL is random and changes every run. By default this has no authentication beyond LOFTY_API_KEY baked into the container: anyone with the tunnel URL can call every tool, including sends and deletes.

To close that gap for a quick tunnel session, set a shared secret before starting it:

MCP_HTTP_AUTH_TOKEN=some-long-random-string make http-up

With MCP_HTTP_AUTH_TOKEN set, every request must include Authorization: Bearer <that token> or the server returns 401 before the request reaches any tool. Without it, the server prints a loud warning on startup and runs unauthenticated — fine for a few minutes of local testing torn down right after, not for anything left running.

Persistent tunnel — fixed hostname, Cloudflare Access OAuth

For anything longer-lived, use a named tunnel at a hostname you control, and put Cloudflare Access in front of it for real OAuth login (Google, GitHub, Okta, etc.) — unauthenticated requests never reach your machine at all.

One-time setup, requires a domain added to your Cloudflare account:

make tunnel-login     # opens a browser, authorizes cloudflared against your account (once per machine)

Add to .env:

TUNNEL_HOSTNAME=lofty-mcp.yourdomain.com
TUNNEL_NAME=lofty-mcp        # optional, defaults to lofty-mcp

Then:

make tunnel-create    # creates (or reuses) the named tunnel, routes TUNNEL_HOSTNAME's DNS to it

In the Cloudflare Zero Trust dashboard → Access → Applications, add a Self-hosted application for TUNNEL_HOSTNAME, pick an identity provider under Settings → Authentication, and add a policy allowing the people who should have access. Then:

make http-up-persistent     # runs the server + the named tunnel at TUNNEL_HOSTNAME
make http-down-persistent   # tears both down
make http-status-persistent # check what's running

Once Access is configured, MCP_HTTP_AUTH_TOKEN (above) is still worth setting as defense-in-depth at the application layer, but Access is what actually stops unauthenticated traffic from reaching the container.

Adding a new tool

Every resource in specs/rest/*.json not yet wired up (see specs/rest/index.json's resources list for what's implemented vs. pending) follows the same pattern as the existing ones:

  1. Read the resource's spec file for exact paths/params (or specs/openapi.json directly for full request/response schemas).

  2. Add src/lofty_mcp/tools/<resource>.py with a register(mcp) function, one @mcp.tool(annotations=ToolAnnotations(...))-decorated function per operation, calling rest_client.request().

  3. Register it in src/lofty_mcp/tools/__init__.py.

  4. make test to confirm it works against the real API.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    MCP server for the Rentalot API. Manage rental properties, contacts, showings, conversations, and more from any AI assistant.
    8 npm
    1
    MIT
  • A
    license
    B
    quality
    D
    maintenance
    MCP server for GoHighLevel API v2 that provides 50+ tools for CRM, billing, marketing, and operations workflows, enabling natural language interaction with contacts, opportunities, conversations, and more.
    50
    1
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    A production-ready MCP server for Method CRM API integration. It enables LLMs to interact with Method CRM data through tools for tables, files, users, events, and API key management.
    3
    MIT