lofty-mcp

lofty-mcp

MCP server for Lofty CRM. Enables managing leads, tasks, calendar, communications, and notes via the Lofty REST API.

Category
访问服务器

README

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.

Why REST, not lofty-cli

This project started as a wrapper around Lofty's official CLI (@loftyai/lofty-cli), on the reasoning that the CLI would save us from reimplementing OAuth and from handling 64-bit lead/task IDs ourselves. That design was abandoned after hitting a real, irreconcilable credential mismatch — documented here so it isn't rediscovered the hard way:

  • The only credential available for this project is a Lofty personal-access API key (generated in the CRM under Settings → Integrations → API).
  • That credential authenticates over HTTP as Authorization: token <key> — confirmed empirically against https://api.lofty.com (GET /v1.0/leads → 200 OK with real data).
  • lofty-cli only speaks OAuth Bearer tokens. Every one of its auth methods (Direct Token, Token URL, Browser OAuth, Client Credentials) ends in an Authorization: Bearer <token> header. Feeding it the API key produces a real rejection from Lofty's own server: Error 400: {"code":200058,"message":"User in token does not exist."} — not a config problem, a protocol mismatch. The same request succeeds instantly with Authorization: token <key> instead of Bearer.
  • Getting a CLI-compatible credential would mean either running a full browser OAuth flow to mint an access/refresh token pair, or registering an OAuth app in Lofty's Developer Portal for the Client Credentials flow — neither of which was available.

Given the working credential is REST-only, wrapping the CLI was never going to work, so the server now calls https://api.lofty.com directly (see src/lofty_mcp/rest_client.py). This turned out to be a simplification, not just a workaround:

  • No token refresh logic. A personal-access API key doesn't expire on the short-lived OAuth cycle an access/refresh pair does — there's nothing to refresh. If it does stop working, the fix is regenerating it in the CRM, not a retry loop (see LoftyApiError in rest_client.py).
  • No 64-bit ID precision handling. Lofty's lead/task IDs are 64-bit integers, which is a real hazard in JavaScript (Number loses precision above 2^53). Python's int is arbitrary-precision, so this was a genuine source of care in an earlier TypeScript/CLI prototype and simply isn't a concern here.
  • Full, authoritative API surface for free. Lofty publishes a complete OpenAPI spec (specs/openapi.json, 102 operations across 27 resources) — a far more reliable source than scraping the CLI's --help text and prose docs, which is what the earlier prototype had to do. specs/rest/*.json is generated straight from it (see specs/README.md).

Authentication

Single env var: LOFTY_API_KEY, sent as Authorization: token <key>. Get it from the Lofty CRM under Settings → Integrations → API. Put it in a .env file at the repo root (not committed — see .gitignore):

LOFTY_API_KEY=your-key-here

If it stops working, calls fail with a clear LoftyApiError pointing back at that same settings page — there is no refresh path, so regenerating the key is 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)

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

Setup

Requires Docker. Everything — build, run, test — happens inside containers; nothing from this project's dependency stack is installed on the host.

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)

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

Remote connectors (Anthropic's Cowork "Add custom connector" dialog, or anything else that talks to an MCP server over HTTP rather than spawning a local process) need a real HTTPS URL, since they run in the cloud and can't spawn processes on your machine. make http-up runs the server bound to 0.0.0.0:8000 and fronts it with cloudflared tunnel --url (no account needed, but the URL is random and changes every run).

⚠️ This has no authentication — do not leave it running

The HTTP endpoint accepts requests from anyone who has the URL. There is currently no bearer token, no OAuth, nothing checking who's calling — only LOFTY_API_KEY baked into the container's own environment. Whoever holds the tunnel URL can call every tool in this server against the real Lofty account, including sends (SMS/email) and deletes.

This is acceptable for the way make http-up is meant to be used: a short-lived local test, torn down with make http-down right after. It is not acceptable as a standing deployment. The "Add custom connector" dialog itself has OAuth Client ID / OAuth Client Secret fields for exactly this reason — before this server is exposed anywhere longer-lived than a quick local tunnel, it needs an actual authorization layer in front of the Streamable HTTP endpoint. Two ways to get there, roughly in order of effort:

  1. A bearer token gate. Cheapest option: require a shared secret in an Authorization header on every request to /mcp, checked in an ASGI middleware before it reaches the MCP app. Not OAuth, but closes the "anyone with the URL" hole.
  2. A real OAuth 2.1 authorization server, per the MCP Authorization spec. This is what the Cowork dialog's OAuth fields expect: the MCP server acts as an OAuth resource server, validating bearer tokens issued by a separate authorization server (self-hosted, or a hosted provider like Auth0/WorkOS/Stytch). This is the correct answer for anything beyond solo local testing, but it's a genuinely separate piece of infrastructure — an OAuth server, client registration, token issuance and validation — not a config flag. Nothing in this repo implements it yet.

Also worth knowing: run_streamable_http_async's DNS-rebinding protection (transport_security) is left at the SDK's default, which is disabled unless explicitly configured (see src/lofty_mcp/server.py) — another gap that matters once this is reachable from anywhere untrusted.

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.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选