There is no SDK to install and no client library to keep in sync. Point any MCP client at the endpoint below: it authorises over OAuth 2.1, then discovers the whole CRM by itself — the six tables, every field, and every operation.
Most MCP clients — the Claude connector UI among them — handle authorisation themselves. Give them the endpoint and they will discover this server's OAuth metadata, register, open a browser for you to approve, and keep their own access and refresh tokens.
You will be asked to sign in and approve the connection. Approved agents are listed on your account page, where you can disconnect any of them.
For scripts and clients that take a static header, issue a token yourself. One per agent, so you can revoke a single one without disturbing the others. It is shown once and only its hash is stored.
Your agent then sends it on every request:
Authorization: Bearer crmk_…
Every MCP client takes the same two things: the endpoint and the auth header.
claude mcp add --transport http crm https://headlesscrm-production.up.railway.app/mcp \ --header "Authorization: Bearer crmk_…"
{
"mcpServers": {
"crm": {
"type": "http",
"url": "https://headlesscrm-production.up.railway.app/mcp",
"headers": {
"Authorization": "Bearer crmk_…"
}
}
}
}
// Streamable HTTP transport, protocol 2025-06-18. // The server issues an mcp-session-id on initialize; // send it back on every subsequent request. POST https://headlesscrm-production.up.railway.app/mcp Content-Type: application/json Accept: application/json, text/event-stream Authorization: Bearer crmk_…
# no server needed — runs on SQLite with zero config
git clone https://github.com/bdeeps/headlesscrm
npm install && npm run seed
claude mcp add crm -- node /path/to/headlesscrm/src/stdio.js
The health route needs no token and returns no CRM data — it is the quickest way to confirm the server and its database are up.
curl https://headlesscrm-production.up.railway.app/health
{
"status": "ok",
"database": { "engine": "postgres", "reachable": true },
"tools": ["discover", "execute"],
"operations": 66,
"auth": { "accounts": 1, "setup_required": false }
}
auth.setup_required in that response. If it is true, nobody has created an account yet and the CRM is refusing to serve data — open /setup first.It calls discover to learn the schema and the signatures, then execute to do the work. Both tools describe themselves, so a client that has never seen this CRM gets it right first time.
// what is here? discover({ scope: "overview" }) // what can I do with a contact? discover({ scope: "operations", category: "contact" }) // exactly how do I call this one? discover({ scope: "operation", operation: "contact.log" })
// everything about one customer execute({ operation: "contact.profile", params: { email: "alicia@northwind.com" } }) // record what just happened execute({ operation: "contact.log", params: { contact_id: "con_8k2", type: "call", subject: "Discovery call" } })
Every operation, with its full signature and a runnable example, is in the API reference.
mcp-session-id on initialize; send it back on every later request.dry_run executes in a transaction that is rolled back — nothing persists.idempotency_key makes a retried call return its first result instead of acting twice.actor records who did it, and shows up in the contact's history.