lm-mcp-server
lm-mcp-server
The LiquidMind MCP server. It exposes a small set of READ and PROPOSE tools to Claude (Cowork) and translates each tool call into an HTTP call against the existing Liquid-Mind-CRM API. It holds no business logic and no platform credentials — it is a thin, authenticated router that sits in front of tools that already exist.
Claude Cowork ──HTTPS (bearer)──▶ lm-mcp ──HTTP (service token)──▶ Liquid-Mind-CRM API
(connector) (this) (truth ledger + AI_Action_Queue)Scope (v1)
READ tools — pull from the truth ledger via the CRM API. No side effects.
list_clients,resolve_client,get_series_performance,get_campaign_performance,list_tasks,run_ad_review
PROPOSE tools — write a row to the CRM
AI_Action_Queue(status = 'proposed'). They never touch a campaign.propose_bid_change,propose_budget_shift,propose_audience_rebuild,propose_campaign_state,propose_new_campaign
There is no EXECUTE in v1. Nothing this server does moves money — the worst a
compromised token can do is read data and insert proposal rows a human must still
approve. EXECUTE (apply_approved_action) is deliberately deferred (see Roadmap).
Attribution rule for run_db_query (DSP & AMG)
describe_schema with no argument proxies to the CRM's GET /api/db/schema, which
serves Liquid-Mind-CRM/src/db/SCHEMA.md verbatim. That file is the schema
contract for every agent on this server — when the data model changes, edit it
there (outside the <!-- BEGIN/END GENERATED --> markers) rather than restating
anything here. This section exists only because the rule below is the one most
likely to produce a confidently wrong number.
Client_DSP_Orders and Client_AMG_Line_Items each carry clientId and
seriesASIN as independent columns, and the two do not always agree. seriesASIN
is the attribution key; clientId records whose ad account the entity runs on.
-- WRONG: credits the ad-account owner
JOIN Client_DSP_Orders cdo ON cdo.orderId = p.orderId AND cdo.clientId = ?
-- RIGHT: credits the suite owner
JOIN Client_DSP_Orders cdo ON cdo.orderId = p.orderId
JOIN Client_Series cs ON cs.seriesASIN = cdo.seriesASIN AND cs.clientId = ?The LMP imprint holds all four DSP advertisers on one house client while its nine
author-clients own the suites and hold none, so every DSP order and AMG line item
for those series is filed under LMP. Reading clientId silently credits LMP with
~$5.5k/month of DSP and ~$3.6k/month of AMG that belongs to the authors. Matching
the two columns (AND cs.clientId = cdo.clientId) is worse — it drops those rows
from the result entirely.
The dedicated tools (get_series_performance, get_campaign_performance) already
route through the corrected CRM endpoints. This applies to hand-written
run_db_query SQL.
Layout
src/
index.ts Express + streamable-HTTP transport + incoming bearer auth
server.ts Builds the McpServer and registers tools
config.ts Env loading/validation (all endpoints are config vars)
auth.ts Bearer check for the /mcp endpoints
clients/
crmClient.ts Thin HTTP client for the CRM API
webToolsClient.ts Thin client for the WebPages PHP tools (reserved for EXECUTE)
tools/
read.ts READ tools
propose.ts PROPOSE tools
index.ts, util.ts
Dockerfile, docker-compose.yml, cloudflared/config.example.yml, .env.exampleRun locally
cp .env.example .env # set MCP_AUTH_TOKEN and CRM_BASE_URL / CRM_SERVICE_TOKEN
npm install
npm run build
npm start # listens on :8930
curl localhost:8930/healthRun with Docker
cp .env.example .env # fill in values
docker compose up -d --buildBind stays on 127.0.0.1:8930; put Cloudflare in front for the public hostname.
Cloudflare
Give it its own subdomain — lm-mcp.website.com — not a path under the website.
Preferred: a Cloudflare Tunnel so the droplet needs no open inbound port. Either run the token-based sidecar (uncomment
cloudflaredindocker-compose.yml, setTUNNEL_TOKEN) or a locally-installedcloudflaredwithcloudflared/config.example.yml.Do not put interactive Cloudflare Access / SSO in front of
/mcp— the client is Anthropic-hosted, not a browser; an interactive login breaks the connector. Use the app's bearer token for identity and Cloudflare for the non-interactive layers (WAF, rate limiting, optional IP allowlist).Disable caching for
lm-mcp.*and confirm streaming works end-to-end through the proxy.
The connector URL Claude uses is https://lm-mcp.website.com/mcp, with
Authorization: Bearer <MCP_AUTH_TOKEN>.
Configuration
Var | Required | Purpose |
| yes | Bearer token the MCP client must present. |
| yes | Base URL of the CRM API (localhost when co-located; private VPC IP when split out). |
| yes* | Service credential sent to the CRM as |
| no | Reserved for EXECUTE. |
| no | Reserved for EXECUTE. |
| no | Default |
* Required in practice because the CRM API is behind requireAuth. See below.
CRM-side contract (what this server depends on)
This repo assumes two things on the CRM. They are not in this repo — they live
in Liquid-Mind-CRM:
A service credential. The CRM routes are gated by
requireAuth(...). The MCP server sendsCRM_SERVICE_TOKENas a bearer token; the CRM must accept a non-interactive service identity (a long-lived service JWT, or a dedicated middleware branch) with a role that permits the READ routes and the actions route.The AI_Action_Queue endpoint + table. The PROPOSE tools
POST /api/actionswith this envelope:{ "actionType": "bid_change", "clientId": 42, "seriesASIN": "B0...", "platform": "AMG", "rationale": "…", "expectedImpactUsd": 430, "modelBasis": "…", "currentValue": { "…": "…" }, "proposedValue": { "…": "…" }, "proposedBy": "claude" }The endpoint should insert a row in
status = 'proposed'and return the created row (including itsactionId). The approve/deny UI and the atomic approved-only execution gate live on the CRM side. (Ask Claude for theAI_Action_Queuemigration + routes — designed but not yet added.)
Until #2 exists, the READ tools work fully and the PROPOSE tools will return a clear error from the CRM (404) — which is the expected state before the queue is built.
Adding a tool
Add a
server.registerTool(name, { title, description, inputSchema }, handler)intools/read.tsortools/propose.ts.The handler should be thin: call
crm.get/post/...and returnok(data)/fail(msg). No business logic here — extend the CRM instead.Keep the tool list small and parameterized; prefer one flexible tool over many near-duplicates.
Roadmap
EXECUTE phase: add
apply_approved_action(actionId)— looks up an APPROVEDAI_Action_Queuerow, routes to the matching WebPages tool viawebToolsClient(or enqueues the existing Redis worker job), writes back the result. Refuses anything not inapprovedstate.Surface C: wrap the AMS_Automation scripts (e.g.
pnl_sheet_filler.py) behind a job-runner or the existing Redis queue so they become EXECUTE targets.Per-manager identity: swap the shared bearer token for OAuth if you want per-account-manager attribution on proposals.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/cscokus/lm-mcp-server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server