Metadata-Version: 2.4
Name: keila-mcp
Version: 0.1.2
Summary: MCP server for Keila API - comprehensive campaign and contact management
License-Expression: AGPL-3.0-or-later
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: mcp[cli]>=1.9.1
Requires-Dist: requests>=2.32.5
Provides-Extra: dev
Requires-Dist: pytest-mock>=3.12; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Description-Content-Type: text/markdown

# **Keila MCP**

[![PyPI](https://img.shields.io/pypi/v/keila-mcp)](https://pypi.org/project/keila-mcp/)
[![Python](https://img.shields.io/pypi/pyversions/keila-mcp)](https://pypi.org/project/keila-mcp/)

MCP server for [Keila](https://github.com/pentacent/keila) — gives any MCP-compatible AI assistant full control over your Keila email campaigns, contacts, segments and forms.

> [!NOTE]
> **Migrating from an earlier clone?** The source directory was renamed from `repo/` to `keila-mcp/`. Update any paths in your MCP client config accordingly.

  

---
 
  
 
## **Quickstart with uvx**

If you have [`uv`](https://docs.astral.sh/uv/) installed, no cloning or virtualenv needed:

```json
{
  "mcpServers": {
    "keila": {
      "command": "uvx",
      "args": ["keila-mcp"],
      "env": {
        "KEILA_URL": "https://your-keila-instance.com",
        "KEILA_API_KEY": "your-api-key"
      }
    }
  }
}
```

Add this to your MCP client config and it works on first run. Skip to [**Client Configuration**](#client-configuration) for client-specific formats.

---

## **Requirements**

- A running [Keila](https://www.keila.io/docs) instance — [self-host with Docker](https://www.keila.io/docs/self-hosting) or use [Keila Cloud](https://app.keila.io)
- A Keila API key — Settings → API Keys → Create
- Python 3.10+ on your machine

  

---
 
  
 
## **Installation**
 
Choose where to install:

- **Global** — use Keila MCP in any project: install in `~/.mcp/`
- **Project** — use it only in one project: install in your project folder, e.g. `~/Developer/my-project/.mcp/`
 
  
 
### **macOS / Linux**
 
  
 
```bash
# global
mkdir -p ~/.mcp && cd ~/.mcp

# or project-specific
mkdir -p ~/Developer/my-project/.mcp && cd ~/Developer/my-project/.mcp

git clone https://github.com/punkyard/keila-mcp.git keila-mcp
cd keila-mcp
python -m venv .venv
source .venv/bin/activate
pip install .
pwd
```

Copy the path printed by `pwd` — you will paste it into your client config below.
 
  
 
### **Windows**
 
  
 
```bash
# global
mkdir %USERPROFILE%\.mcp && cd %USERPROFILE%\.mcp

# or project-specific
mkdir C:\Users\your-username\Developer\my-project\.mcp && cd C:\Users\your-username\Developer\my-project\.mcp

git clone https://github.com/punkyard/keila-mcp.git keila-mcp
cd keila-mcp
python -m venv .venv
.venv\Scripts\activate
pip install .
cd
```

Copy the path printed by `cd` — you will paste it into your client config below.

  

---
 
  
 
## **Client Setup**
 
  
 
Every MCP client needs two things: the path to the Python interpreter and two environment variables.

| Variable        | Value                                                          |
|-----------------|----------------------------------------------------------------|
| `KEILA_URL`     | Your Keila instance URL, e.g. `https://keila.mydomain.com` |
| `KEILA_API_KEY` | Create a Keila API key per project                             |
| `KEILA_MCP_HTTP_PORT` | *(optional)* HTTP port for the MCP server. Default: `3001`  |

  

![Keila API Keys](https://raw.githubusercontent.com/punkyard/keila-mcp/main/docs/img/keila-create-api-key.png)

  

> [!IMPORTANT]
> In all examples below, replace `/path/to/keila-mcp/repo` with the path printed by `pwd` (or `cd` on Windows) at the end of the installation section.
 
  
 
### **Claude Desktop**
 
  
 
**File** (macOS): `~/Library/Application Support/Claude/claude_desktop_config.json`  
**File** (Windows): `%APPDATA%\Claude\claude_desktop_config.json`

```json
{
  "mcpServers": {
    "keila": {
      "command": "/path/to/keila-mcp/keila-mcp/.venv/bin/python",
      "args": ["/path/to/keila-mcp/keila-mcp/src/mcp_server.py"],
      "env": {
        "KEILA_URL": "https://your-keila-instance.com",
        "KEILA_API_KEY": "your-api-key"
      }
    }
  }
}
```

Fully quit and relaunch Claude Desktop after saving.
 
  
 
### **Claude Code**
 
  
 
```bash
claude mcp add-json keila '{
  "type": "stdio",
  "command": "/path/to/keila-mcp/keila-mcp/.venv/bin/python",
  "args": ["/path/to/keila-mcp/keila-mcp/src/mcp_server.py"],
  "env": {
    "KEILA_URL": "https://your-keila-instance.com",
    "KEILA_API_KEY": "your-api-key"
  }
}'
```

Add `--scope global` to make it available in all projects.
 
  
 
### **Cursor · Cline / Roo Code · Windsurf · OpenClaw**
 
  
 
- **Cursor**: `.cursor/mcp.json` in your project, or `~/.cursor/mcp.json` for global
- **Cline / Roo Code**: MCP Servers config via the sidebar
- **Windsurf**: `~/.codeium/windsurf/mcp_config.json`
- **OpenClaw**: `~/.openclaw/mcp.json`

```json
{
  "mcpServers": {
    "keila": {
      "command": "/path/to/keila-mcp/keila-mcp/.venv/bin/python",
      "args": ["/path/to/keila-mcp/keila-mcp/src/mcp_server.py"],
      "env": {
        "KEILA_URL": "https://your-keila-instance.com",
        "KEILA_API_KEY": "your-api-key"
      }
    }
  }
}
```
 
  
 
### **VS Code (GitHub Copilot Agent Mode)**
 
  
 
Create `.vscode/mcp.json` in your project:

```json
{
  "servers": {
    "keila": {
      "command": "/path/to/keila-mcp/keila-mcp/.venv/bin/python",
      "args": ["/path/to/keila-mcp/keila-mcp/src/mcp_server.py"],
      "env": {
        "KEILA_URL": "https://your-keila-instance.com",
        "KEILA_API_KEY": "${input:keilaApiKey}"
      }
    }
  },
  "inputs": [
    {
      "id": "keilaApiKey",
      "type": "promptString",
      "description": "Keila API Key",
      "password": true
    }
  ]
}
```

Requires Copilot Chat in Agent mode. VS Code will prompt for the API key on first use.
 
  
 
### **Zed**
 
  
 
In `~/.config/zed/settings.json` (global) or `.zed/settings.json` (project):

```json
{
  "context_servers": {
    "keila": {
      "command": "/path/to/keila-mcp/keila-mcp/.venv/bin/python",
      "args": ["/path/to/keila-mcp/keila-mcp/src/mcp_server.py"],
      "env": {
        "KEILA_URL": "https://your-keila-instance.com",
        "KEILA_API_KEY": "your-api-key"
      }
    }
  }
}
```
 
  
 
### **OpenCode**
 
  
 
In `~/.config/opencode/opencode.json` (global) or `opencode.json` (project):

```json
{
  "mcp": {
    "keila": {
      "type": "local",
      "command": ["/path/to/keila-mcp/keila-mcp/.venv/bin/python", "/path/to/keila-mcp/keila-mcp/src/mcp_server.py"],
      "env": {
        "KEILA_URL": "https://your-keila-instance.com",
        "KEILA_API_KEY": "your-api-key"
      }
    }
  }
}
```
 
  
 
### **Pi Agent**
 
  
 
In `~/.pi/agent/mcp.json`:

```json
{
  "keila": {
    "command": "/path/to/keila-mcp/keila-mcp/.venv/bin/python",
    "args": ["/path/to/keila-mcp/keila-mcp/src/mcp_server.py"],
    "env": {
      "KEILA_URL": "https://your-keila-instance.com",
      "KEILA_API_KEY": "your-api-key"
    },
    "lifecycle": "on-demand"
  }
}
```
 
  
 
### **Hermes**
 
  
 
In `~/.hermes/config.yaml`:

```yaml
mcp_servers:
  keila:
    command: /path/to/keila-mcp/keila-mcp/.venv/bin/python
    args:
      - /path/to/keila-mcp/keila-mcp/src/mcp_server.py
    env:
      KEILA_URL: https://your-keila-instance.com
      KEILA_API_KEY: your-api-key
```
 
  
 
### **OpenAI Codex CLI**
 
  
 
**File**: `~/.codex/config.toml` (global) or `.codex/config.toml` (project):

```toml
[mcp_servers.keila]
command = "/path/to/keila-mcp/keila-mcp/.venv/bin/python"
args = ["/path/to/keila-mcp/keila-mcp/src/mcp_server.py"]

[mcp_servers.keila.env]
KEILA_URL = "https://your-keila-instance.com"
KEILA_API_KEY = "your-api-key"
```

  

---
 
  
 
## **Running manually (for testing)**

```bash
# stdio mode (default)
python src/mcp_server.py

# HTTP mode
python src/mcp_server.py --http
# optional: export KEILA_MCP_HTTP_PORT=8325
```

  

---
 
  
 
## **Tools**
 
  
 
| Tool | Description |
|------|-------------|
| `list_campaigns`       | List all campaigns with optional status/search filter |
| `create_campaign`      | Create a new campaign |
| `get_campaign`         | Get a campaign by ID |
| `update_campaign`      | Update an existing campaign |
| `delete_campaign`      | Delete a campaign |
| `send_campaign`        | Send a campaign immediately |
| `schedule_campaign`    | Schedule a campaign for later delivery |
| `create_contact`       | Create a new contact |
| `get_contact`          | Get a contact by ID |
| `update_contact`       | Update a contact |
| `delete_contact`       | Delete a contact |
| `list_contacts`        | List contacts with optional filtering |
| `update_contact_data`  | Merge custom data fields on a contact |
| `replace_contact_data` | Replace all custom data fields on a contact |
| `list_senders`         | List all senders |
| `create_segment`       | Create a new segment |
| `list_segments`        | List all segments |
| `get_segment`          | Get a segment by ID |
| `update_segment`       | Update a segment |
| `delete_segment`       | Delete a segment |
| `list_forms`           | List all forms |
| `get_form`             | Get a form by ID |
| `create_form`          | Create a new signup form |
| `update_form`          | Update a form |
| `delete_form`          | Delete a form |
| `submit_form`          | Submit a signup form on behalf of a contact |
 
  
 
### **Campaigns**
 
  
 
#### `list_campaigns`

List all email campaigns with optional filtering.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `status` | string | No | Filter by: draft/scheduled/sent/archived/paused |
| `q` | string | No | Search by subject (case-insensitive substring) |
 
  
 
#### `create_campaign`

Create a new email campaign.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `subject` | string | Yes | Campaign subject line |
| `body_type` | string | Yes | Body type: markdown/text/block/mjml |
| `text_body` | string | No | Plain text body |
| `preview_text` | string | No | Preview text for inbox |
| `sender_id` | string | No | Sender identity ID |
| `segment_id` | string | No | Target segment ID |
| `data` | object | No | Liquid template variables |
| `do_not_track` | boolean | No | Disable open/click tracking |
 
  
 
#### `get_campaign`

Get a single campaign by ID.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Campaign ID |
 
  
 
#### `update_campaign`

Update an existing campaign.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Campaign ID |
| `subject` | string | No | New subject line |
| `preview_text` | string | No | New preview text |
 
  
 
#### `delete_campaign`

Delete a campaign.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Campaign ID |
 
  
 
#### `send_campaign`

Send a campaign immediately.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Campaign ID |
| `sender_id` | string | No | Override sender identity |
 
  
 
#### `schedule_campaign`

Schedule a campaign for later delivery.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Campaign ID |
| `scheduled_for` | string | Yes | ISO 8601 datetime (e.g. 2026-06-01T09:00:00Z) |
 
  
 
### **Contacts**
 
  
 
#### `create_contact`

Create a new contact.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `email` | string | Yes | Email address |
| `first_name` | string | No | First name |
| `last_name` | string | No | Last name |
| `external_id` | string | No | External system ID |
| `status` | string | No | Status: active/inactive/bouncing/blocked/spam |
| `data` | object | No | Custom fields |
 
  
 
#### `get_contact`

Get a contact by ID, email, or external ID.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Contact identifier |
| `id_type` | string | No | Lookup type: id (default)/email/external_id |
 
  
 
#### `update_contact`

Update an existing contact.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Contact identifier |
| `email` | string | No | New email |
| `first_name` | string | No | New first name |
| `last_name` | string | No | New last name |
| `external_id` | string | No | New external ID |
| `data` | object | No | New custom fields |
| `id_type` | string | No | Lookup type: id (default)/email/external_id |
 
  
 
#### `delete_contact`

Delete a contact.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Contact identifier |
| `id_type` | string | No | Lookup type: id (default)/email/external_id |
 
  
 
#### `list_contacts`

List contacts with pagination and optional search.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `page` | integer | No | Page number (default: 0) |
| `page_size` | integer | No | Results per page (default: 50) |
| `q` | string | No | Search query |
 
  
 
### **Senders**
 
  
 
#### `list_senders`

List all sender identities.

No parameters.
 
  
 
### **Contact Data**
 
Custom data is a free-form JSON object attached to each contact. Use it to store any extra fields (e.g. `plan`, `score`, `tags`). Fields can be used as merge tags in campaigns via `{{ contact.data.my_field }}` and as segment filters. See [Keila — Segments and Custom Data](https://www.keila.io/segments-and-data/) and [Contacts API docs](https://www.keila.io/docs/contacts/).
 
  
 
#### `update_contact_data`
 
  
 
Merge new key/value pairs into a contact's custom data field. Keys not present in `data` are preserved.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Contact ID (or email/external_id when `id_type` set) |
| `data` | object | Yes | Key/value pairs to merge |
| `id_type` | string | No | `id` (default), `email`, or `external_id` |
 
  
 
#### `replace_contact_data`

Replace a contact's entire custom data field with the provided dict.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Contact ID (or email/external_id when `id_type` set) |
| `data` | object | Yes | New data dict (replaces existing) |
| `id_type` | string | No | `id` (default), `email`, or `external_id` |
 
  
 
### **Segments**
 
  
 
#### `create_segment`

Create a contact segment with a filter.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Segment name |
| `filter` | object | Yes | Keila filter expression |
 
  
 
#### `list_segments`

List all segments.

No parameters.
 
  
 
#### `get_segment`

Get a segment by ID.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Segment ID |
 
  
 
#### `delete_segment`

Delete a segment.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Segment ID |
 
  
 
#### `update_segment`

Update a segment's name and/or filter. At least one of `name` or `filter` must be provided.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Segment ID |
| `name` | string | No | New segment name |
| `filter` | object | No | New filter expression |
 
  
 
### **Forms**
 
  
 
#### `list_forms`

List all subscription forms.

No parameters.
 
  
 
#### `get_form`

Get a form by ID.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Form ID |
 
  
 
#### `create_form`

Create a new subscription form.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Form name |
| `sender_id` | string | No | Sender identity ID |
| `fields` | array | No | Form field definitions |
| `settings` | object | No | Form settings (double opt-in, redirect URLs, etc.) |
 
  
 
#### `delete_form`

Delete a form.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Form ID |
 
  
 
#### `update_form`

Update an existing signup form.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Form ID |
| `name` | string | No | New form name |
| `sender_id` | string | No | New sender identity ID |
| `fields` | array | No | Replacement field definitions |
| `settings` | object | No | Updated form settings |
 
  
 
#### `submit_form`

Submit a signup form on behalf of a contact. Returns the created/updated contact on success, or `{"data": {"double_opt_in_required": true}}` if the form has double opt-in enabled.

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `form_id` | string | Yes | Form ID |
| `email` | string | Yes | Contact email address |
| `first_name` | string | No | Contact first name |
| `last_name` | string | No | Contact last name |
| `external_id` | string | No | External identifier |
| `status` | string | No | Contact status (e.g. `active`) |
| `data` | object | No | Custom data key/value pairs |

  

---

  

## **Development**

```bash
pip install -e ".[dev]"
pytest tests/ -v
```

  

<div align="center">

© 2026 — [LICENSE AGPL-3.0](https://github.com/punkyard/keila-mcp/blob/main/LICENSE)

made with ⏳ by [punkyard](https://github.com/punkyard)

</div>
