MCP server for Listmonk newsletter management (3rd-party fork). https://rhnvrm.github.io/listmonk-mcp/
  • Python 97.9%
  • Dockerfile 1.1%
  • Nix 0.5%
  • Shell 0.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-30 14:59:50 +00:00
.forgejo/workflows fix: resolve scan config from the attested OCI index 2026-10-29 16:36:35 +08:00
.github/workflows docs: update documentation and workflows 2025-08-06 19:59:11 +05:30
docs docs: update documentation and workflows 2025-08-06 19:59:11 +05:30
scripts feat: approved campaign sends for a separate send identity (v0.4.0) 2026-10-30 22:57:49 +08:00
src/listmonk_mcp feat: approved campaign sends for a separate send identity (v0.4.0) 2026-10-30 22:57:49 +08:00
tests feat: approved campaign sends for a separate send identity (v0.4.0) 2026-10-30 22:57:49 +08:00
.dockerignore feat(deploy): streamable-HTTP transport, image and CI 2026-08-25 12:24:44 +08:00
.env.example feat(deploy): streamable-HTTP transport, image and CI 2026-08-25 12:24:44 +08:00
.envrc Initial implementation: Listmonk MCP server foundation 2025-07-13 01:35:33 +05:30
.gitignore chore: include uv.lock file for reproducible builds 2025-08-06 14:16:34 +05:30
.python-version feat(deploy): streamable-HTTP transport, image and CI 2026-08-25 12:24:44 +08:00
AGENTS.md feat: approved campaign sends for a separate send identity (v0.4.0) 2026-10-30 22:57:49 +08:00
CLAUDE.md feat(deploy): streamable-HTTP transport, image and CI 2026-08-25 12:24:44 +08:00
Dockerfile feat: approved campaign sends for a separate send identity (v0.4.0) 2026-10-30 22:57:49 +08:00
flake.lock Initial implementation: Listmonk MCP server foundation 2025-07-13 01:35:33 +05:30
flake.nix Initial implementation: Listmonk MCP server foundation 2025-07-13 01:35:33 +05:30
HARDGATE_DESIGN.md feat: approved campaign sends for a separate send identity (v0.4.0) 2026-10-30 22:57:49 +08:00
LICENSE chore: Create LICENSE 2025-07-13 03:20:03 +05:30
mkdocs.yml feat: add GitHub Actions workflow for MkDocs Material documentation 2025-08-06 13:41:05 +05:30
pyproject.toml feat: approved campaign sends for a separate send identity (v0.4.0) 2026-10-30 22:57:49 +08:00
README.md feat: approved campaign sends for a separate send identity (v0.4.0) 2026-10-30 22:57:49 +08:00
uv.lock feat: approved campaign sends for a separate send identity (v0.4.0) 2026-10-30 22:57:49 +08:00

Listmonk MCP Server

An MCP (Model Context Protocol) server implementation for Listmonk, providing programmatic access to newsletter and mailing list management functionality.

Listmonk MCP Logo

Project Status

✅ Implementation Complete - The core MCP server is fully implemented and functional.

Goal

Create an MCP server that enables LLMs and AI assistants to interact with Listmonk instances through the Model Context Protocol. This will allow for:

  • Subscriber management (add, remove, update subscribers)
  • Mailing list operations (create, manage lists)
  • Campaign management (create, edit, archive; sending stays in the Listmonk UI)
  • Analytics and reporting access
  • Template and content management

Architecture

This server will bridge the MCP protocol with Listmonk's REST API, providing a standardized interface for AI models to interact with Listmonk installations.

Features

  • Complete Listmonk API Coverage: All major Listmonk operations supported
  • 67 MCP Tools: Subscriber, list, campaign, template, media, bounce, import, settings, and maintenance management — campaign drafting, pause/cancel/delete/archive/analytics, bounce handling, bulk operations by ID, and admin endpoints. No tool takes a SQL expression.
  • No email is sent through this server. Every tool path that would make Listmonk email someone (send, schedule, start, test send, transactional, opt-in, SMTP test, import notification) is refused before any request. Send from the Listmonk admin UI. See Sending.
  • MCP Resources: Easy access to subscriber, list, campaign, and template data
  • Async Operations: Built with modern async/await patterns
  • Type Safety: Full Pydantic model validation
  • Environment Configuration: Easy setup with environment variables

Installation

Install and run directly from PyPI without managing dependencies:

# Run directly (installs if needed)
uvx listmonk-mcp --help

# Or install globally
uv tool install listmonk-mcp
listmonk-mcp --help

Using pip

pip install listmonk-mcp

Development Installation

git clone https://github.com/rhnvrm/listmonk-mcp.git
cd listmonk-mcp
uv sync --locked --extra dev

Transports

The server speaks two transports. stdio is the default and is how a local MCP client runs it.

# stdio (default)
listmonk-mcp

# streamable-HTTP
LISTMONK_MCP_TRANSPORT=streamable-http listmonk-mcp
# or
listmonk-mcp --transport streamable-http

Under streamable-http the server opens two listeners:

default carries env override
public 0.0.0.0:3000 /mcp/, /health LISTMONK_MCP_BIND_ADDR
internal 127.0.0.1:9090 /metrics LISTMONK_MCP_METRICS_BIND_ADDR

The metrics listener resolves its address as: explicit env, then {POD_IP}:9090, then 127.0.0.1:9090. It never defaults to 0.0.0.0, because /metrics must not be reachable through the same Service that carries /mcp.

Authentication (streamable-HTTP)

Every request to /mcp/ must carry Authorization: Bearer <token>, including each request in an existing session: the session id is not a credential. Anything else gets 401 and no session. /health is open.

env meaning
LISTMONK_MCP_AUTH_TOKEN_SHA256 Required. Lowercase hex SHA-256 of each accepted token, comma separated for rotation. The server never holds a plaintext token. Without it the HTTP transport refuses to start.
LISTMONK_MCP_ALLOWED_HOSTS Host names the public listener answers to, comma separated. Any other Host header gets 421 (DNS-rebinding protection). Loopback is always allowed.
token="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')"
export LISTMONK_MCP_AUTH_TOKEN_SHA256="$(printf %s "$token" | sha256sum | cut -d' ' -f1)"

There is no unauthenticated mode; a development server takes the digest of a development token. stdio is unaffected: it is a local process talking to its parent.

Sending

The read identity never sends email. A campaign send needs the separate send identity and an approval per campaign (below). Without it every email-producing path is refused before Listmonk is asked:

  • send_campaign, schedule_campaign, and change_campaign_status to running or scheduled (pausing, cancelling and draft still work)
  • test_campaign, except to the deployment's own addresses (below); send_transactional_email, subscriber_send_optin
  • test_smtp_settings, import_subscribers and stop_import (Listmonk emails the admins when an import ends)
  • subscriber writes that would make Listmonk send opt-in mail: adding or re-adding to a double opt-in list without preconfirm

Send from the Listmonk admin UI, or through the send identity below.

Approved campaign sends (send identity). Set LISTMONK_MCP_SEND_TOKEN_SHA256 to the SHA-256 of a second, separate token and LISTMONK_MCP_SEND_LISTS to the list ids it may send to (the Heartbeat is 3). Give that token only to a client mount that asks a person before every call (in Hermes: an MCP server with trust: untrusted). For that identity alone, send_campaign, schedule_campaign and change_campaign_status to running/scheduled act when the request names the campaign by id and its exact subject, every list of the campaign is a send list, and the campaign is draft, paused or scheduled. Anything else is refused with nothing sent. The identity comes from the request's bearer token, never from an argument; the read token can never send, the two tokens must differ, and transactional, opt-in, import and SMTP-test mail stay refused for both.

One opening without approval: test sends to the owner's own addresses. Set LISTMONK_MCP_TEST_RECIPIENTS to a comma-separated list of addresses (Julian's own). test_campaign then sends a campaign test when every recipient is on that list, compared case-insensitively, and refuses the whole test if any recipient is not. Unset or empty keeps every test refused. Nothing else opens: sends, schedules, sending status changes, transactional and opt-in mail stay refused whatever the list holds.

Container image

docker build \
  --build-arg BUILD_VERSION=0.4.0 \
  --build-arg BUILD_REVISION="$(git rev-parse HEAD)" \
  --build-arg BUILD_CREATED="$(git show -s --format=%cI HEAD)" \
  -t listmonk-mcp:dev .

docker run --rm -p 3000:3000 \
  -e LISTMONK_MCP_URL=http://listmonk:9000 \
  -e LISTMONK_MCP_USERNAME=api-user \
  -e LISTMONK_MCP_PASSWORD=... \
  -e LISTMONK_MCP_AUTH_TOKEN_SHA256=<sha256 of the client token> \
  -e LISTMONK_MCP_ALLOWED_HOSTS=listmonk-mcp.example.com \
  listmonk-mcp:dev

The image defaults LISTMONK_MCP_TRANSPORT to streamable-http and runs as UID 65532. Port 9090 is deliberately not EXPOSEd.

Development

Code Quality Checks

Run the same checks that are executed in the CI/CD pipeline:

# Install development dependencies
uv sync --locked --extra dev

# Run linting (same as CI)
uv run ruff check src/ tests/ scripts/

# Auto-fix linting issues
uv run ruff check src/ tests/ scripts/ --fix

# Run type checking (same as CI)
uv run mypy src/ scripts/

# Run the test suite (same as CI)
uv run pytest

# Audit the locked dependency set (same as CI)
uv export --locked --all-extras --no-hashes --no-emit-project > /tmp/audit-requirements.txt
uvx --from pip-audit==2.10.1 pip-audit -r /tmp/audit-requirements.txt --no-deps --disable-pip

# Run code checks together (run the dependency audit above too)
uv run ruff check src/ tests/ scripts/ \
  && uv run mypy src/ scripts/ \
  && uv run pytest \
  && uv run python scripts/check_buildcache_guard.py

Building and Testing

# Build a local package
uv build

# Test CLI locally (using entry point)
uv run listmonk-mcp --help
uv run listmonk-mcp --version

# Or install locally and test
uv pip install -e .
listmonk-mcp --help

Version Management

This fork releases OCI images through Forgejo; the retained upstream GitHub workflows do not publish this fork to PyPI. Version 0.4.0 must agree in pyproject.toml, uv.lock, the server fallback and the Docker build default. Merge only an independently reviewed exact head with green CI, then tag that reviewed source. A tag must match the package version (or its prerelease suffix) and descend from master.

CI builds one named OCI archive with SPDX/SLSA attestations, scans that exact image config and positive OS/application inventory, and refuses every CRITICAL finding, fixable HIGH finding or secret. Unfixed OS findings remain in the full report and count summary; this is not a vulnerability-free claim. Configuration checks also must pass. The publisher copies the complete existing index by digest, refuses a conflicting version tag and reads it back. Stable tags update latest; prerelease tags do not. Keep one release writer: the version-tag precheck is not registry compare-and-swap, and attestations are not signatures. The source, final binary/native client and authenticated production path must all be qualified before deployment.

python3 scripts/test_publish_oci.py --layout /path/to/oci --digest sha256:... uses a disposable localhost registry to verify every initially absent object, identical replay, conflicting tag and missing-descendant refusal, and that a prerelease cannot move latest. Use CI's checksum-pinned regctl.

Quick Start

1. Set up Listmonk (Local Development)

For testing, you can run a local Listmonk instance using Docker:

# Option 1: Use the provided compose file
docker compose -f docs/listmonk-docker-compose.yml up -d

# Option 2: Download the latest compose file
curl -LO https://github.com/knadh/listmonk/raw/master/docker-compose.yml
docker compose up -d

# Access Listmonk at http://localhost:9000
# Default credentials: admin / listmonk

2. Create API User and Token

  1. Access the Listmonk admin interface at http://localhost:9000/admin
  2. Login with the default credentials: admin / listmonk
  3. Navigate to Admin → Users (http://localhost:9000/admin/users)
  4. Create a new API user:
    • Click "Add new"
    • Enter a username (e.g., api-user)
    • Assign appropriate role/permissions
    • Save the user
  5. Generate an API token:
    • Click on the created user
    • Click "Generate API token"
    • Copy the generated token

3. Configure Environment Variables

The MCP server requires the following environment variables:

export LISTMONK_MCP_URL=http://localhost:9000
export LISTMONK_MCP_USERNAME=your-api-username
export LISTMONK_MCP_PASSWORD=your-generated-api-token

Important: The password field should contain the API token (not the user's login password). The server uses Listmonk's token authentication format: Authorization: token username:api_token.

Troubleshooting Configuration:

  • Verify variables: echo $LISTMONK_MCP_URL should show your Listmonk URL
  • Test API access: curl -H "Authorization: token username:api_token" http://localhost:9000/api/health
  • Common errors: "invalid session" or 403 errors indicate incorrect credentials

4. Run the MCP Server

# Using uv (recommended)
uv run python -m listmonk_mcp.server

# Or using the entry point
listmonk-mcp

Common Issues:

  • Connection refused: Listmonk server not running or wrong URL
  • Module not found: Install dependencies with uv sync --locked --extra dev