Skip to content

highdeserthacker/espocrm-mcp-server

Repository files navigation

mcp-host

A modular MCP (Model Context Protocol) server stack exposing EspoCRM and other optional tools to MCP clients such as AI coding agents, MCP enabled AI tools, and agent such as OpenClaw.

Architecture

[MCP client]
     |
     | HTTP streamable-http / SSE   port 8000
     v
[ mcp-proxy ]           ghcr.io/tbxark/mcp-proxy
     |
     |-- stdio:  github, filesystem, fetch   (spawned inside proxy)
     |-- SSE:    http://mcp-espocrm:8001/sse

All inter-container traffic is on the internal Docker network mcp-net. Only mcp-proxy binds a host port (8000).

Features

Docker Ready

Spin up a container and immediately use as is. Built in mcp-proxy so you can add other mcp-servers if desired.

Supports all Entities through auto-discovery

Supports standard as well as your custom entities. All entities in the allow list get the five core tools registered automatically:

  • find
  • get
  • create
  • update
  • delete

How it works: At startup, server.py reads the MCP_ENTITY_ALLOW list and mounts any tools/*.py file whose name matches an allowed entity. Entities in the allow list with no custom file get the five core tools registered automatically. Adding a new entity requires adding it to MCP_ENTITY_ALLOW (and optionally dropping a custom file in tools/) then restarting.

EspoCRM field metadata auto-discovery

Fields for each entity are auto-discovered.

How it Works: Before writing a record, normalize_fields() fetches the entity's field definitions from the EspoCRM Metadata API (/api/v1/Metadata). This allows the server to:

  • Detect special write formats - emailAddress and phoneNumber fields require a structured *Data array payload rather than a plain string; the server handles this transparently.
  • Merge multiEnum fields - multiEnum fields (arrays of selected values) are read first so that a write can add or remove individual values without clobbering the rest. The caller passes a boolean dict ({"Blog": true, "Newsletter": false}) and the server computes the correct merged array.
  • Metadata caching - field definitions are fetched once per entity per process lifetime and cached in memory, so the extra API call is paid only on the first write to each entity type.

Access Control

Set the entities you want to expose in the MCP server in the environment MCP_ENTITY_ALLOW setting. Only the entities listed here will exposed as tools.

Use your defined Api User and associated permissions to limit access to the crm system. Set the use api key in the environment ESPOCRM_API_KEY setting.

Structured logging

Log output includes timestamp, level, and message. The tool call decorator logs every invocation (function name + parameters) at INFO level and errors at ERROR level. Noisy internal loggers (fastmcp, mcp, uvicorn request access) are suppressed to keep the log readable at INFO level.

Control the level of logging - Log level is controlled by the LOG_LEVEL environment variable (default: INFO). Set it in docker-compose.yml under mcp-espocrm > environment:

- LOG_LEVEL=info    # error | warning | info | debug

View mcp-espocrm logs with:

docker compose logs -f mcp-espocrm

Repository Layout

Stack root (~/dockerdata/mcp-host/)

├── docker-compose.yml          # main stack definition
├── docker-compose.override.yml # host-specific overrides (bind mounts, etc.)
├── config.json                 # proxy upstream config (copy from config.json.example)
├── config.json.example
├── .env                        # secrets - never committed
├── .env.example                # template with all supported variables
├── Dockerfile                  # builds the mcp-espocrm image
├── README.md
└── data/                       # bind-mounted into proxy at /data (filesystem server root)

EspoCRM server source (NAS repo: mcp-espocrm)

├── Dockerfile
├── requirements.txt
├── server.py                   # entrypoint: mounts tool modules filtered by allow list
├── .env.example
├── tests/
│   ├── conftest.py
│   └── test_accounts.py
└── tools/
    ├── config.py               # reads MCP_ENTITY_ALLOW; defaults to 5 standard entities
    ├── entity.py               # entity_find/get/create/update/delete helpers + register_entity()
    ├── espo_fields.py          # field normalization (email, phone, multiEnum)
    ├── log.py                  # log_tool_call decorator
    ├── accounts.py             # Account - custom: find_by_domain, find_by_name
    ├── contacts.py             # Contact - custom: find_by_email
    ├── leads.py                # Lead - custom: convert (stub)
    └── <your-entity>.py        # add custom entities here (see Adding Your Own Entity)

Quick Start

git clone <repo-url>
cd mcp-host
cp .env.example .env
cp config.json.example config.json
docker compose up -d

Configuration

.env

Copy .env.example to .env and fill in values. Never commit .env.

Variable Description
ESPOCRM_URL Full URL to your EspoCRM instance (e.g. https://crm.example.com)
ESPOCRM_API_KEY EspoCRM API User key - create under Admin > API Keys
MCP_ENTITY_ALLOW Comma-separated entity names to expose (default: Account,Contact,Lead,Opportunity,Task)

config.json

Copy config.json.example to config.json. Defines all upstream MCP servers exposed by the proxy. The example includes only the EspoCRM server. To add off-the-shelf stdio servers (e.g. GitHub, filesystem), add entries under mcpServers with command/args and restart the proxy - no new container needed.

docker-compose.override.yml

Optional. Create this file for host-specific overrides such as extra bind mounts or a custom build context. See the Merging into an existing stack section below.

Server Paths

The EspoCRM server is accessible at:

Server Path Description
EspoCRM /espocrm/mcp CRM contacts, accounts, leads, and custom entities

Additional servers can be added to config.json - each gets its own path under port 8000.

Merging into an existing stack

If you already have an mcp-proxy running, you can add just the EspoCRM container:

  1. Add the mcp-espocrm service from docker-compose.yml to your own compose file.
  2. Add the EspoCRM entry from config.json.example to your proxy's config.
  3. Build and start: docker compose up -d --build mcp-espocrm then restart your proxy.

The mcp-espocrm container has no dependency on the proxy - it just exposes SSE on port 8001 and can be pointed to from any MCP proxy that supports SSE upstreams.

EspoCRM MCP Server

Tool Naming Convention

Exposed tool names follow the pattern {namespace}_{operation} or {namespace}_{operation}_{description}.

  • namespace - the module filename stem (e.g. contacts, accounts). FastMCP prepends this automatically when the module is mounted.
  • operation - the Python function name inside the module. Use find for all search operations. Other verbs: get, create, update, delete.
  • description (optional) - appended when the operation alone is ambiguous, e.g. find_by_email, find_blog_new.

So a function find_by_email in tools/contacts.py becomes the tool contacts_find_by_email.

Each module exposes a FastMCP instance named mcp and is auto-discovered at startup provided the entity name appears in MCP_ENTITY_ALLOW.

Standard Entity Tools

Core tools (all entities)

Every entity in MCP_ENTITY_ALLOW automatically gets these five tools via register_entity() in tools/entity.py. The tool name prefix is the lowercase entity filename stem.

Tool Exposed as (example: accounts) Description
find(query="", filters=[], fields=[], ...) accounts_find Name-contains search + optional structured filters
get(record_id) accounts_get Fetch a single record by EspoCRM ID
create(fields) accounts_create Create a record; handles email/phone/multiEnum field formats
update(record_id, fields) accounts_update Update fields; pass only fields to change
delete(record_id) accounts_delete Delete a record by ID

find filters format: list of dicts with attribute, type, and optional value keys, e.g. {"attribute": "industry", "type": "equals", "value": "Technology"}.

Entity-specific custom tools

Entity Tool Description
accounts accounts_find_by_domain Find by website domain; strips protocol/www prefix
accounts accounts_find_by_name Ranked name match (exact > starts-with > contains)
contacts contacts_find_by_email Exact email address lookup
leads leads_convert Convert lead to contact/account (stub)

How to customize standard entities as well as add your own entity

Both standard EspoCRM entities (e.g. Opportunities, Cases) and fully custom entities follow the same pattern. Create one file per entity under tools/ and restart - no other registration is required. Expand the core tools for any entity by adding your own custom functions. See the examples included for accounts, contacts.

1. Create tools/<entity>.py

Use the filename stem as the namespace (it becomes the tool name prefix). Call register_entity() to get the pre-defined five core tools, then add any custom tools by following the instructions below.

import httpx
from fastmcp import FastMCP
from tools.log import log_tool_call
from tools.entity import register_entity, _base_url, _headers

mcp = FastMCP("<entity>")          # display name, does not affect tool names
register_entity(mcp, "<EntityApiName>")   # registers find, get, create, update, delete


# --- custom tools (optional) ------------------------------------------------

@mcp.tool()
@log_tool_call
async def my_custom_tool(some_param: str) -> dict:
    # Add entity-specific tools here.
    async with httpx.AsyncClient() as client:
        r = await client.get(f"{_base_url()}/api/v1/<EntityApiName>",
                             headers=_headers(), params={"someParam": some_param})
        r.raise_for_status()
        return r.json()

Replace <EntityApiName> with the EspoCRM REST entity name (singular, e.g. Opportunity, Case, Meeting). The filename stem (e.g. opportunities) becomes the tool name prefix (e.g. opportunities_find, opportunities_get).

2. Restart the container

cd ~/dockerdata/mcp-host
docker compose up -d --force-recreate mcp-espocrm
docker compose up -d --force-recreate mcp-proxy

The proxy must also be restarted to pick up the new tool names.

Instance-specific entities

If the entity is specific to your EspoCRM instance and you do not want it committed to a shared repo, add it to tools/.gitignore:

# instance-specific entity files
registrations.py
my_custom_entity.py

The auto-discovery mechanism picks them up regardless of whether they are tracked by git.

Health Check

# Verify a server is responding:
curl -s -X POST http://localhost:8000/espocrm/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' \
  | python3 -c "import sys,json; [print(t['name']) for t in json.load(sys.stdin)['result']['tools']]"

Operational Notes

  • Restart after code changes: docker compose up -d --force-recreate (docker compose restart does not re-read compose YAML or apply healthcheck ordering)
  • Rebuild after dependency changes: docker compose up -d --build mcp-espocrm
  • Logs: docker compose logs -f mcp-espocrm
  • mcp-proxy caches the tool list from upstream servers at connect time - restart the proxy after adding new tools to mcp-espocrm

About

MCP Server for the EspoCrm system. Full stack - includes mcp proxy.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors