- Rust 95.3%
- Shell 4.3%
- Makefile 0.4%
|
Some checks failed
Integration Tests / integration (push) Failing after 1s
Seeder now creates regular CRM users via admin auth and distributes all assignable records (leads, opportunities, tasks, meetings, calls, cases) randomly across them. Added base64 and dotenvy deps to seeder feature gate, auto-loads .env.test, and grants User read permission to the test role. Includes example session docs and MCP config. |
||
|---|---|---|
| .github/workflows | ||
| docs | ||
| scripts | ||
| src | ||
| tests | ||
| .gitignore | ||
| .mcp.json | ||
| Cargo.lock | ||
| Cargo.toml | ||
| docker-compose.yml | ||
| example_session.md | ||
| examples.md | ||
| Makefile | ||
| README.md | ||
espomcp
A Model Context Protocol (MCP) server for EspoCRM. Provides both read and write access to CRM data, plus agent workflow prompts. Enables AI assistants to discover entities, query records, create/update/delete data, manage relationships, and execute CRM workflows through a standardized protocol.
Built in Rust. Communicates over stdio using JSON-RPC.
Quick Start
# Build
cargo build --release
# Configure
export ESPOCRM_URL=https://your-instance.example.com
export ESPOCRM_API_KEY=your-api-key
# Run (stdio — intended to be launched by an MCP client)
./target/release/espomcp
EspoCRM Setup
Create an API User
The MCP server authenticates via API key. Create a dedicated API user in EspoCRM:
- Log into EspoCRM as admin
- Go to Administration > Users
- Click Create User, set Type to API
- Set Auth Method to API Key
- Save — EspoCRM auto-generates the API key
- Copy the API key for configuration
Assign Permissions
The API user has no access by default. Create a Role with appropriate permissions:
- Go to Administration > Roles
- Create a role with permissions on the entity types you need:
- Read = All for entities you want to query
- Create = Yes, Edit = All, Delete = All for entities the agent should modify
- Stream = All for entities where you want to add notes
- Important: Do NOT grant Email send permission — the server only creates drafts
- Assign the role to your API user
For automated setup (e.g., testing), see scripts/setup-test-user.sh.
Configuration
Configuration is loaded from three sources (highest priority first):
- Environment variables
- Config file (
~/.config/espocrm-mcp/config.toml) - Built-in defaults
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
ESPOCRM_URL |
Yes | — | EspoCRM instance URL |
ESPOCRM_API_KEY |
Yes | — | API authentication key |
ESPOCRM_TIMEOUT_SECS |
No | 30 |
HTTP request timeout |
ESPOCRM_TLS_VERIFY |
No | true |
Verify TLS certificates (set false for local dev) |
SERVER_MAX_LIMIT |
No | 200 |
Maximum records per query |
SERVER_METADATA_TTL_SECS |
No | 300 |
Schema/entity type cache TTL |
SERVER_RATE_LIMIT_REQUESTS_PER_SECOND |
No | 10 |
Rate limit |
SERVER_RATE_LIMIT_BURST |
No | 20 |
Burst allowance |
SERVER_RATE_LIMIT_MAX_CONCURRENT |
No | 5 |
Max concurrent requests |
LOGGING_LEVEL |
No | info |
Log level (error, warn, info, debug, trace) |
LOGGING_JSON |
No | false |
JSON-formatted log output |
Config File
~/.config/espocrm-mcp/config.toml (or $XDG_CONFIG_HOME/espocrm-mcp/config.toml):
[espocrm]
url = "https://your-instance.example.com"
api_key = "your-api-key"
timeout_secs = 30
tls_verify = true
[server]
max_limit = 200
metadata_ttl_secs = 300
[server.rate_limit]
requests_per_second = 10
burst = 20
max_concurrent = 5
[logging]
level = "info"
json = false
If the config file is world-readable, the server logs a warning. Use chmod 600 to restrict access.
MCP Client Setup
The server is a stdio binary — the MCP client spawns it as a subprocess. Below are configurations for all major clients.
Claude Desktop
Edit the config file (or via Settings > Developer > Edit Config):
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"espocrm": {
"command": "/path/to/espomcp",
"args": [],
"env": {
"ESPOCRM_URL": "https://your-instance.example.com",
"ESPOCRM_API_KEY": "your-api-key"
}
}
}
}
Restart Claude Desktop after editing.
Claude Code
Option A: CLI command
claude mcp add --transport stdio \
--env ESPOCRM_URL=https://your-instance.example.com \
--env ESPOCRM_API_KEY=your-api-key \
espocrm -- /path/to/espomcp
Add --scope user to make it available across all projects, or --scope project to create a shared .mcp.json.
Option B: Project .mcp.json (checked into repo, secrets via env var expansion)
{
"mcpServers": {
"espocrm": {
"command": "/path/to/espomcp",
"args": [],
"env": {
"ESPOCRM_URL": "${ESPOCRM_URL}",
"ESPOCRM_API_KEY": "${ESPOCRM_API_KEY}"
}
}
}
}
Useful commands:
claude mcp list # List configured servers
claude mcp get espocrm # Show server details
claude mcp remove espocrm # Remove server
Use /mcp inside Claude Code to check server status.
Cursor
Create ~/.cursor/mcp.json (global) or .cursor/mcp.json (per-project):
{
"mcpServers": {
"espocrm": {
"command": "/path/to/espomcp",
"args": [],
"env": {
"ESPOCRM_URL": "https://your-instance.example.com",
"ESPOCRM_API_KEY": "your-api-key"
}
}
}
}
VS Code (GitHub Copilot)
Create .vscode/mcp.json in your workspace:
{
"inputs": [
{
"type": "promptString",
"id": "espocrm-api-key",
"description": "EspoCRM API Key",
"password": true
}
],
"servers": {
"espocrm": {
"type": "stdio",
"command": "/path/to/espomcp",
"args": [],
"env": {
"ESPOCRM_URL": "https://your-instance.example.com",
"ESPOCRM_API_KEY": "${input:espocrm-api-key}"
}
}
}
}
Note: VS Code uses servers (not mcpServers) and supports ${input:id} for secret prompting.
Windsurf (Codeium)
Edit ~/.codeium/windsurf/mcp_config.json:
{
"mcpServers": {
"espocrm": {
"command": "/path/to/espomcp",
"args": [],
"env": {
"ESPOCRM_URL": "https://your-instance.example.com",
"ESPOCRM_API_KEY": "your-api-key"
}
}
}
}
Zed
Edit ~/.config/zed/settings.json (Linux) or ~/Library/Application Support/Zed/settings.json (macOS):
{
"context_servers": {
"espocrm": {
"source": "custom",
"command": "/path/to/espomcp",
"args": [],
"env": {
"ESPOCRM_URL": "https://your-instance.example.com",
"ESPOCRM_API_KEY": "your-api-key"
}
}
}
}
Note: Zed uses context_servers with "source": "custom".
Continue.dev
Edit ~/.continue/config.yaml:
mcpServers:
- name: espocrm
command: /path/to/espomcp
args: []
env:
ESPOCRM_URL: "https://your-instance.example.com"
ESPOCRM_API_KEY: "your-api-key"
Client Config Summary
| Client | Config File | Root Key |
|---|---|---|
| Claude Desktop | ~/Library/Application Support/Claude/claude_desktop_config.json |
mcpServers |
| Claude Code | .mcp.json or claude mcp add |
mcpServers |
| Cursor | ~/.cursor/mcp.json |
mcpServers |
| VS Code | .vscode/mcp.json |
servers |
| Windsurf | ~/.codeium/windsurf/mcp_config.json |
mcpServers |
| Zed | ~/.config/zed/settings.json |
context_servers |
| Continue.dev | ~/.continue/config.yaml |
mcpServers |
Tools
The server exposes 13 tools: 5 read-only and 8 write tools.
Read Tools
list_entity_types
Discover all available entity types.
{}
get_schema
Get field definitions and relationship links for an entity type. Call this before create/update to discover required fields and valid enum values.
{ "entityType": "Contact" }
search
Query records with filters, sorting, and pagination.
{
"entityType": "Opportunity",
"where": [
{ "field": "stage", "type": "in", "value": ["Proposal", "Negotiation"] },
{ "field": "amount", "type": "greaterThan", "value": 10000 }
],
"orderBy": "amount",
"order": "desc",
"limit": 20
}
Filter operators: equals, notEquals, like (% wildcards), greaterThan, lessThan, in (array), isNull, isNotNull, between (array of two values).
get
Retrieve a single record by ID.
{ "entityType": "Contact", "id": "abc123" }
get_related
Traverse relationships to get linked records.
{ "entityType": "Account", "id": "acct456", "link": "contacts", "limit": 20 }
Write Tools
create
Create a new record. Validates fields against schema. Cannot create Email records — use draft_email instead.
{
"entityType": "Account",
"data": { "name": "Acme Corp", "website": "https://acme.com" },
"skipDuplicateCheck": false
}
update
Update an existing record's fields. Only include fields you want to change.
{
"entityType": "Opportunity",
"id": "opp123",
"data": { "stage": "Closed Won" }
}
delete
Delete a record with mandatory two-step confirmation:
- Call with
confirm: false— returns a preview of the record - Call with
confirm: true— actually deletes the record
{ "entityType": "Account", "id": "acct123", "confirm": true }
link
Link records via a named relationship. Provide foreignId (single) or foreignIds (multiple).
{
"entityType": "Account",
"id": "acct123",
"link": "contacts",
"foreignId": "cont456"
}
unlink
Remove a relationship link between records.
{
"entityType": "Account",
"id": "acct123",
"link": "contacts",
"foreignId": "cont456"
}
add_note
Post a text note to a record's activity stream.
{
"entityType": "Account",
"id": "acct123",
"text": "Called and confirmed renewal for Q3."
}
update_kanban
Move a record to a different Kanban group (stage/status) and/or position.
{
"entityType": "Opportunity",
"id": "opp123",
"group": "Proposal",
"position": 0
}
draft_email
Create a draft email. Always creates as Draft — never sends. Optionally renders an email template.
{
"to": "client@example.com",
"subject": "Follow-up",
"body": "Hi, just following up...",
"isHtml": false,
"templateId": "tpl123",
"parentType": "Contact",
"parentId": "cont456"
}
MCP Prompts
The server provides 6 workflow prompts that prime AI agents with CRM-specific instructions:
| Prompt | Arguments | Description |
|---|---|---|
process-call-transcript |
transcript (req), account_hint (opt), contact_hint (opt) |
Parse a call transcript, identify contacts, create tasks, update pipeline, draft follow-up |
log-meeting |
notes (req), attendees (opt), date (opt), duration (opt) |
Create meeting record, link attendees, create action item tasks |
process-inbound-lead |
details (req), source (opt) |
Deduplicate, create lead, assign follow-up task |
draft-followup |
context (req), recipient (opt), template_name (opt) |
Draft a context-aware follow-up email for a CRM record |
pipeline-update |
changes (req) |
Process batch pipeline stage changes |
email-triage |
emails (req) |
Triage incoming email, identify sender, create tasks, draft response |
All prompts include safety guardrails: never send emails (only drafts), never delete without confirmation, always verify before updating.
Customizing Prompts
On first use, the server writes default prompt templates to .espomcp/prompts/ in the working directory. Each prompt is stored as a separate .md file:
.espomcp/prompts/
process-call-transcript.md
log-meeting.md
process-inbound-lead.md
draft-followup.md
pipeline-update.md
email-triage.md
Edit any file to customize the system prompt for that workflow. Changes take effect on the next get_prompt call — no restart required. Delete a file to reset it to the built-in default on the next access.
Error Handling
All errors include a machine-readable code, human-readable message, and optional suggestion:
{
"error": {
"code": "INVALID_ENTITY_TYPE",
"message": "Entity type 'BadType' not found",
"suggestion": "Use list_entity_types to see available entities."
}
}
| Code | Condition | Retryable |
|---|---|---|
INVALID_ENTITY_TYPE |
Unknown entity type | No |
INVALID_FIELD |
Unknown field on entity | No |
INVALID_OPERATOR |
Unsupported filter operator | No |
RECORD_NOT_FOUND |
ID does not exist | No |
INVALID_LINK |
Unknown relationship link | No |
AUTH_FAILED |
Invalid API key (401) | No |
PERMISSION_DENIED |
Insufficient permissions (403) | No |
RATE_LIMITED |
Too many requests (429) | Yes |
UPSTREAM_ERROR |
EspoCRM API error | Yes |
UPSTREAM_TIMEOUT |
Request timed out | Yes |
MISSING_REQUIRED_FIELD |
Required field not provided in create | No |
INVALID_FIELD_VALUE |
Wrong type or invalid enum value | No |
DELETE_NOT_CONFIRMED |
Delete called without confirm=true | Yes |
DUPLICATE_DETECTED |
Record already exists (HTTP 409) | Yes |
TEMPLATE_NOT_FOUND |
Email template ID not found | No |
SEND_BLOCKED |
Attempted to create Email via generic create tool | No |
Development
Prerequisites
- Rust 1.85+ (edition 2024)
- Podman or Docker (for integration tests)
Running Tests
# Unit tests only (no containers needed)
make test-unit
# Prompt tests (no containers needed)
make test-prompts
# Integration tests (requires running EspoCRM)
make test-setup # Start EspoCRM containers + seed data
make test-read # v1 read-only tests
make test-write # v2 write tests
make test-integration # All integration tests
make test-teardown # Stop containers
# Full cycle
make test-full # Setup -> all tests -> teardown
# Reset test data without restarting containers
make test-reset
Project Structure
src/
main.rs # Entry point — config, logging, stdio transport
lib.rs # Module declarations
config.rs # Figment-based config (env + TOML + defaults)
client.rs # EspoCRM HTTP client with caching and rate limiting
server.rs # MCP tool handlers (13 tools) + prompt support
types.rs # Domain types (EntityType, Schema, SearchParams, etc.)
error.rs # Error types with codes and suggestions (18 variants)
cache.rs # Metadata cache (entity types + schemas)
rate_limit.rs # Governor-based rate limiter
validation.rs # Input validation (entity type, ID, limit, offset)
write_validation.rs # Schema-aware write data validation
prompts.rs # 6 MCP prompt definitions and message builders
redaction.rs # PII redaction for TRACE-level logging
tests/
integration.rs # Read-only integration tests against live EspoCRM
write_integration.rs # Write integration tests (create/update/delete/link)
prompt_tests.rs # Prompt definition and guardrail tests
common/
mod.rs # Test helpers and client factory
write_helpers.rs # Admin client, RAII cleanup guards, write test context
scripts/ # Setup, teardown, and seeding scripts
docs/ai/ # PRD and Architecture Decision Records
License
See LICENSE for details.