
Less Annoying CRM (remote, OAuth)
com.northkeellabsPublished by northkeellabs.comOfficial publisherv0.1.0Updated Nov 11, 2026
Unofficial remote MCP server for Less Annoying CRM: OAuth 2.1, read-only grants, 10 tools.
Overview
Unofficial remote MCP server for Less Annoying CRM that lets an assistant search contacts, read notes and tasks, and create records via OAuth sign-in.
- What you can do
- Search contacts and companies, and fetch a full contact record
- Read notes attached to a contact, and list tasks in a date range
- List pipelines and the items in a pipeline, with optional status filter
- Create contacts or companies, add plain-text notes, and create tasks
- Move a pipeline item to another status (automations are not run)
- When to use it
- Useful when you want an assistant to look up CRM contacts, notes, tasks or pipeline status, or to log notes and tasks, without installing a local server. Read-only grants suit summarising and lookup; write grants are for adding records.
- Requirements
- A remote Streamable HTTP endpoint with OAuth 2.1 sign-in; no local runtime. You need a Less Annoying CRM account and its API key, which you paste on the consent page and choose an access level (read-only or read and write). Rate limit is 60 tool calls per minute per connection.
Installation
In SourceWeft
- Open Less Annoying CRM (remote, OAuth) in the dashboard and add it to a workspace.
- Connect the required credentials. They are stored privately and never shown on public pages.
- Enable the server for the chats that should use its tools.
Web executable via Streamable HTTP. Remote servers run from the web runtime once configured in a workspace.
Use with an agent
Paste this prompt into Claude Code, Codex, Cursor or another agent — or into SourceWeft chat. The agent reads this server's install guide, shows you its publisher, endpoint and sign-in, and adds it to your client once you agree. Keys never go into a command.
Read https://sourceweft.com/mcp/com.northkeellabs%2Flacrm-mcp-remote/install.md and connect the MCP server it describes to my client. Before changing any config, show me its publisher, endpoint and sign-in method and wait for my OK. Never put an API key in a command or in a file I would commit; ask me where it should come from.Other MCP clients
Add this to your client's mcpServers config. The client completes OAuth sign-in on first connect.
{
"mcpServers": {
"less-annoying-crm-remote-oauth": {
"type": "http",
"url": "https://lacrm-mcp.northkeellabs.com/mcp"
}
}
}README
lacrm-mcp-remote
A remote MCP server for Less Annoying CRM (LACRM), hosted on Cloudflare Workers, with OAuth 2.1 sign-in. It exposes the same 10 tools as the local lacrm-mcp server (stdio, Python, also by Northkeel Labs), but over Streamable HTTP, so it works as a claude.ai custom connector and in any MCP client that supports remote servers with OAuth.
Independent and unofficial. Not affiliated with, endorsed by or supported by Less Annoying CRM.
Hosted endpoint
Northkeel Labs runs this code at that URL. Paste it into any MCP client that supports remote servers with OAuth (see Connect a client), sign in, and paste your LACRM API key on the consent page. The Worker stores only the encrypted OAuth grant; it keeps no other copy of your key or CRM data. Free, best effort, no SLA, 60 tool calls per minute per connection. To run it on your own Cloudflare account instead, see Deploy your own.
Why remote + OAuth
A local stdio server needs every user to install Python, store an API key in a config file and keep the process running on their machine. A remote server is one URL. The user pastes it into claude.ai, Claude Desktop or Cursor, signs in once in the browser, and the client gets a short-lived, revocable token. The API key stays out of the client, out of config files and out of the model's context.
LACRM itself has no OAuth, only API keys. This server bridges the two with a "bring your key, get OAuth" pattern: the Worker is a standards-compliant OAuth 2.1 authorization server (discovery metadata, dynamic client registration, Client ID Metadata Documents, PKCE S256, refresh-token rotation), and its consent page is where the user pastes their LACRM key once. The key is checked with a single read-only LACRM call and then lives only inside the encrypted OAuth grant.
Architecture
Code map: src/index.ts (OAuthProvider wiring), src/authorize.ts (consent page), src/mcp.ts (protected endpoint), src/tools.ts (the 10 tools), src/lacrm.ts (API client, retries, error mapping), src/security.ts (truncation, untrusted-data wrapper, redaction), src/ratelimit.ts (Durable Object).
Built on @cloudflare/workers-oauth-provider 1.2.4, agents 0.33.0 (createMcpHandler) and the MCP TypeScript SDK v2 (@modelcontextprotocol/server 2.4.0). The handler is stateless: every request gets a fresh MCP server built for that grant. It serves both 2025-era Streamable HTTP clients and the stateless 2026 protocol from the same /mcp endpoint. There is no /sse endpoint. McpAgent is deprecated as of agents 0.33.0, so this uses createMcpHandler instead; the only Durable Object is the per-grant rate limiter.
The auth flow, step by step
- The client calls
POST /mcpwithout a token and gets401withWWW-Authenticate: Bearer resource_metadata=".../.well-known/oauth-protected-resource/mcp", scope="lacrm.read". - It reads the protected resource metadata (RFC 9728), then the authorization server metadata at
/.well-known/oauth-authorization-server(RFC 8414). - It identifies itself, either with a Client ID Metadata Document URL or by registering at
/oauth/register(RFC 7591 dynamic client registration). - It opens
/authorizein the browser with PKCE (S256 only),stateandresource. The server rejects unknown clients and any redirect URI that was not registered, and renders those errors locally instead of redirecting. - The consent page shows which client is asking (name, verified domain for CIMD clients, redirect host, a warning for local apps), asks for the LACRM API key and an access level: read-only (
lacrm.read) or read and write (lacrm.read lacrm.write). - On submit (POST only, CSRF handle bound to a
__Host-cookie, single use, 10 minutes), the server calls LACRMGetUserwith the key. A rejected key shows an error and nothing is stored. - The server completes the authorization. The key, LACRM user id, granted scopes and a grant id are stored as grant props, encrypted with key material wrapped by the token. The browser is redirected to the client with a code.
- The client exchanges the code plus PKCE verifier at
/oauth/tokenfor an access token (1 hour) and a rotating refresh token. - Every
/mcpcall is rate limited per grant (60 per minute by default) and scope-checked: a read-only grant sees 6 tools intools/listand any call to a write tool is refused, whether or not it was listed.
Tools
There are no delete tools. Limits match the Python version: at most 50 results per page with has_more_results, text fields truncated at 2,000 characters, lists capped at 50 items, and every response that contains CRM records carries a data_notice marking the content as untrusted data, not instructions. Inputs are validated with zod (ids are digit strings, enums, ISO dates, email format, length limits) before any request reaches LACRM.
Deploy your own
You need a Cloudflare account (the free plan is enough: one Worker, one KV namespace, SQLite-backed Durable Objects) and Node 22+.
Paste the printed namespace id into kv_namespaces[0].id in wrangler.jsonc (the id in the repo belongs to the hosted instance and will not work on your account). The repo config serves the Worker on our custom domain, so change the address too:
- Your own domain (the zone must be on the same Cloudflare account): set
routes[0].patternto your hostname, for examplemcp.example.com, andvars.MCP_RESOURCEtohttps://mcp.example.com/mcp. - Or workers.dev: delete the
routesentry, set"workers_dev": true, and setvars.MCP_RESOURCEtohttps://lacrm-mcp-remote.<your-workers-subdomain>.workers.dev/mcp(your subdomain is shown in the Cloudflare dashboard under Workers and Pages).
Tokens are bound to this exact URL. Then:
No server-side secret is needed: each user's LACRM key arrives through the consent page and is stored only in their encrypted grant. Optional settings are plain vars in wrangler.jsonc (RATE_LIMIT_PER_MINUTE, default 60). If you later add a secret of your own, use npx wrangler secret put NAME in production and .dev.vars locally (see .dev.vars.example; .dev.vars is gitignored).
Local development: copy .dev.vars.example to .dev.vars (it sets MCP_RESOURCE to http://localhost:8787/mcp, overriding the production value), then npx wrangler dev serves everything at http://localhost:8787.
Connect a client
Use the hosted endpoint https://lacrm-mcp.northkeellabs.com/mcp or your own deployment's URL.
-
claude.ai: Settings > Connectors > Add custom connector. Paste the URL, then Connect. A browser tab opens the consent page; paste your LACRM key (LACRM: Settings > Programmer API) and choose the access level.
-
Claude Desktop: Settings > Connectors > Add custom connector, same URL and flow as claude.ai.
-
Cursor: add to
~/.cursor/mcp.jsonand enable the server; Cursor opens the browser for sign-in. -
MCP Inspector:
npx @modelcontextprotocol/inspector, choose transport "Streamable HTTP", enter the URL, and complete its OAuth sign-in flow. Against local dev usehttp://localhost:8787/mcp.
To revoke access, delete or rotate the API key in LACRM. Every grant made with that key then stops working, whatever client holds the token.
Security
Summary (details and threat model in SECURITY.md):
- The LACRM key is never accepted as a tool argument, never returned to the client, never logged. Error text from LACRM is redacted before it reaches a tool result.
- Grant props (which hold the key) are encrypted by the OAuth provider; access tokens, refresh tokens and codes are stored only as hashes.
- Consent page: POST only, single-use CSRF handle bound to the browser, all client-supplied text HTML-escaped,
Content-Security-Policy: default-src 'none'; script-src 'none'; frame-ancestors 'none'with one nonce'd inline style, no JavaScript and no third-party resources,X-Frame-Options: DENY,no-store. - Exact redirect URI matching; unknown clients and unregistered redirect URIs are rendered as local errors, never redirected.
- Scope enforcement on every call; per-grant rate limit; writes are never auto-retried on 5xx or timeouts.
- CRM content is bounded and labelled as untrusted to reduce prompt-injection risk. It cannot be made harmless; keep a human in the loop for writes and prefer read-only grants for summarising.
Tests
Test results, 2026-11-10: npm test 50 tests passing (Workers runtime via @cloudflare/vitest-plugin, LACRM mocked at fetch); tsc --noEmit clean; wrangler deploy --dry-run succeeds; live end-to-end (scripts/e2e-live.mjs against wrangler dev and a real LACRM account holding demo data) 20/20 steps passing, and the same 20/20 against the production deployment at https://lacrm-mcp.northkeellabs.com. Redacted output: local, production. Also tested 2026-11-10 as a claude.ai custom connector (read-only grant).
The tests cover each tool's request mapping and output shape, truncation, pagination, the untrusted-data wrapper, error mapping and redaction, retry policy, scope gating (a read grant can neither see nor call write tools, including in a batch), the consent page (bad key, missing or forged CSRF token, unknown redirect URI, unknown client, HTML escaping, deny, single-use handle), rate limiting, and that the key never appears in any response or log line.
To run the live test yourself: start npx wrangler dev, then LACRM_API_KEY=... node scripts/e2e-live.mjs. It makes one write: a note on the contact "Maya Demo".
Disclaimer
This is an independent, unofficial project. It is not affiliated with, endorsed by or supported by Less Annoying CRM. "Less Annoying CRM" is a trademark of its owner. Use at your own risk; review the code before connecting a production CRM account. MIT licensed, see LICENSE.
Source: README.md at commit 62c7082
Tools
0Version history
1- v0.1.0LatestNov 11, 2026
Score
85% of the applicable checks met
How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.
- Reached in the last 30 daysRequired25 pts
- Auth declaredRequired10 pts
- Tools listed15 pts
Answer tools/list without a credential, or ship a package that starts in a sandbox without one.
- DescriptionRequired15 pts
- Logo10 pts
- Documentation15 pts
- License10 pts