feat: comprehensive Listmonk API coverage — 41 new tools #2

Merged
jlxq0 merged 3 commits from feat/comprehensive-api-coverage into master 2026-08-26 05:17:52 +00:00
Owner

Lands the three commits that have sat unmerged on this branch since 2026-05-07. master is byte-identical to rhnvrm/listmonk-mcp@master today, so until this merges every deployment built from the default branch ships upstream's server without any of this.

What it contains

master this branch
@mcp.tool registrations 29 70
ListmonkClient async methods added 42
diff 1192 insertions, 5 deletions across client.py, server.py

Three commits:

  • 4661545 feat: comprehensive API coverage
  • dc07b44 fix(test_campaign): send full campaign payload, not just subscribers
  • 4fd84c2 fix(update_setting): send raw JSON value, not wrapped in {"value": ...}

The 41 new tools group as:

  • Subscriberspatch_subscriber, subscriber_send_optin, blocklist_subscriber(s), manage_subscriber_lists, get_subscriber_activity, and the four by-query variants (delete_subscribers_by_query, blocklist_subscribers_by_query, manage_subscriber_lists_by_query)
  • Bounceslist_bounces, get_bounce, delete_bounce(s), get_subscriber_bounces, delete_subscriber_bounces, blocklist_bounced_subscribers
  • Importimport_subscribers, get_import_status, get_import_logs, stop_import
  • Campaignstest_campaign, change_campaign_status, archive_campaign, delete_campaign(s), get_running_campaign_stats, get_campaign_analytics
  • Templatesset_default_template, preview_template_body
  • Admin / settingsget_settings, update_settings, update_setting, test_smtp_settings, reload_app, get_logs, get_about_info
  • Dashboardget_dashboard_counts, get_dashboard_charts
  • Maintenancegc_subscribers, gc_campaign_analytics, gc_unconfirmed_subscriptions

Version bumps 0.1.00.2.0.

Fork or upstream

This is a fork patch, and it is the fork's whole divergence. master carries zero divergence from rhnvrm/listmonk-mcp today; merging this makes it 1192 lines. The tools are a broad API-surface expansion rather than a bug fix, so sending them upstream is a real conversation with the upstream maintainer and not a mechanical cherry-pick — worth having, but it should not block deployment work here.

By contrast the two follow-up commits (dc07b44, 4fd84c2) are plain bug fixes against upstream behaviour and would be accepted upstream on their own if the tools ever go up.

Scope of this PR

No new files, no CI, no transport change. Deployment work (streamable-HTTP, Dockerfile, Forgejo Actions) is jlxq0/listmonk-mcp#1 and stacks on top of this branch.

Verification

Not verified by execution in this PR: master has no tests/ directory and no Forgejo workflow, so there is no gate to run these commits through. Adding one is part of jlxq0/listmonk-mcp#1.

Refs #1

Lands the three commits that have sat unmerged on this branch since 2026-05-07. `master` is byte-identical to `rhnvrm/listmonk-mcp@master` today, so until this merges every deployment built from the default branch ships upstream's server without any of this. ## What it contains | | master | this branch | |---|---|---| | `@mcp.tool` registrations | 29 | 70 | | `ListmonkClient` async methods added | | 42 | | diff | | 1192 insertions, 5 deletions across `client.py`, `server.py` | Three commits: - `4661545` feat: comprehensive API coverage - `dc07b44` fix(test_campaign): send full campaign payload, not just subscribers - `4fd84c2` fix(update_setting): send raw JSON value, not wrapped in `{"value": ...}` The 41 new tools group as: - **Subscribers** — `patch_subscriber`, `subscriber_send_optin`, `blocklist_subscriber(s)`, `manage_subscriber_lists`, `get_subscriber_activity`, and the four by-query variants (`delete_subscribers_by_query`, `blocklist_subscribers_by_query`, `manage_subscriber_lists_by_query`) - **Bounces** — `list_bounces`, `get_bounce`, `delete_bounce(s)`, `get_subscriber_bounces`, `delete_subscriber_bounces`, `blocklist_bounced_subscribers` - **Import** — `import_subscribers`, `get_import_status`, `get_import_logs`, `stop_import` - **Campaigns** — `test_campaign`, `change_campaign_status`, `archive_campaign`, `delete_campaign(s)`, `get_running_campaign_stats`, `get_campaign_analytics` - **Templates** — `set_default_template`, `preview_template_body` - **Admin / settings** — `get_settings`, `update_settings`, `update_setting`, `test_smtp_settings`, `reload_app`, `get_logs`, `get_about_info` - **Dashboard** — `get_dashboard_counts`, `get_dashboard_charts` - **Maintenance** — `gc_subscribers`, `gc_campaign_analytics`, `gc_unconfirmed_subscriptions` Version bumps `0.1.0` → `0.2.0`. ## Fork or upstream **This is a fork patch, and it is the fork's whole divergence.** `master` carries zero divergence from `rhnvrm/listmonk-mcp` today; merging this makes it 1192 lines. The tools are a broad API-surface expansion rather than a bug fix, so sending them upstream is a real conversation with the upstream maintainer and not a mechanical cherry-pick — worth having, but it should not block deployment work here. By contrast the two follow-up commits (`dc07b44`, `4fd84c2`) are plain bug fixes against upstream behaviour and would be accepted upstream on their own if the tools ever go up. ## Scope of this PR No new files, no CI, no transport change. Deployment work (streamable-HTTP, Dockerfile, Forgejo Actions) is jlxq0/listmonk-mcp#1 and stacks on top of this branch. ## Verification Not verified by execution in this PR: `master` has no `tests/` directory and no Forgejo workflow, so there is no gate to run these commits through. Adding one is part of jlxq0/listmonk-mcp#1. Refs #1
Adds wrappers for every operational Listmonk REST endpoint that was missing
from the MCP, bringing total tool count from ~40 to 70. Skips admin user
management (users/profile/roles/2FA) since those make more sense in the web UI.

Campaign lifecycle (7 tools):
- test_campaign — POST /api/campaigns/{id}/test
- change_campaign_status — pause / cancel / draft / scheduled / running
- archive_campaign — toggle the public archive flag, set slug & template
- delete_campaign / delete_campaigns — single + bulk
- get_running_campaign_stats — live sent / rate / ETA per campaign
- get_campaign_analytics — views / clicks / links / bounces

Subscribers (11 tools):
- patch_subscriber, subscriber_send_optin
- blocklist_subscriber / blocklist_subscribers (single + bulk)
- manage_subscriber_lists — bulk add/remove/unsubscribe
- get_subscriber_activity — opt-ins, list changes, bounces
- delete_subscribers_by_query, blocklist_subscribers_by_query,
  manage_subscriber_lists_by_query — advanced SQL-query operations
- get_subscriber_bounces, delete_subscriber_bounces

Bounces (5 tools):
- list_bounces, get_bounce, delete_bounce, delete_bounces (with all-flag),
  blocklist_bounced_subscribers

Subscriber import (4 tools):
- import_subscribers — multipart CSV/zip upload
- get_import_status, get_import_logs, stop_import

Templates (2 tools):
- set_default_template, preview_template_body

Settings & system (9 tools):
- get_settings, update_settings, update_setting (single-key, avoids the
  password-masking footgun where GET masks secrets as bullets and PUT
  overwrites them with literal bullets)
- test_smtp_settings, reload_app
- get_logs, get_about_info, get_dashboard_counts, get_dashboard_charts

Maintenance (3 tools):
- gc_subscribers, gc_campaign_analytics, gc_unconfirmed_subscriptions

All new client methods follow the existing httpx-based pattern with typed
parameters and JSON-data payloads. All new tools use the existing
safe_execute_async wrapper for consistent error handling.

Smoke-tested against a live Listmonk instance — all reads return expected
shapes; analytics correctly rejects empty campaign-id list with the same
"Missing field(s): id" message the web UI sees, confirming param encoding.

Bumps version 0.1.0 → 0.2.0.
Listmonk's POST /api/campaigns/{id}/test endpoint validates the request as
a full campReq — name, subject, lists, body, messenger, content_type.
Sending only {"subscribers": [...]} fails with "Invalid length for name"
because the empty campaign name fails strHasLen validation.

Fix: fetch the saved campaign first and merge its fields into the test
request, mirroring the pattern in update_campaign. Test recipients are
appended via the "subscribers" key.

Also flag in the MCP docstring that recipients must already exist as
subscribers — listmonk's TestCampaign handler looks them up via
GetSubscribersByEmail and silently skips unknown addresses, so an
unknown email returns success but sends nothing.

Verified end-to-end against a live instance — test send delivered,
response {"data": true}.
fix(update_setting): send raw JSON value, not wrapped in {\"value\": ...}
Some checks failed
Deploy Docs / build (pull_request) Failing after 1m19s
Deploy Docs / deploy (pull_request) Has been skipped
4fd84c2012
Listmonk's UpdateSettingsByKey handler binds the request body as a
json.RawMessage and writes it directly into the settings document.
Sending {\"value\": value} caused the whole object to be stored under
the key, which broke the field (e.g. custom_js stopped executing,
custom_css stopped applying) until restored from the full-settings
PUT path.

Loosens _request's json_data type from dict to Any since httpx's
json= argument accepts any JSON-serialisable value.

Verified by hand: direct PUT of the raw string to
/api/settings/appearance.public.custom_js correctly updates the
public-page JS; PUTting {\"value\": \"...\"} stores garbage.
Author
Owner

Held, not stalled, and the reason is not the code.

Julian's decision, asked and answered: this waits until the scope of the Listmonk API credential is set.

What made it a question rather than a merge. The branch is 41 new tools, not 30 — 29 @mcp.tool registrations on master against 70 here, counted rather than recalled — and two of them are larger than the count suggests:

update_settings replaces the whole settings document. Its own docstring records why that is sharp:

GET /api/settings masks secrets as bullets. PUTting that body back overwrites real values with literal bullets.

So the obvious way to use it — read, change one field, write it back — destroys Listmonk's SMTP credentials. That is the tool working as written, not a misuse of it. reload_app sits beside it and makes the new settings live.

The credential Julian is scoping decides whether either can be called at all, and campaigns:send being distinct from campaigns:manage in Listmonk v6.1.0 does not speak to settings permissions. Merging first would have made the credential the only thing standing between an agent and a broken mail path, before anyone had decided what the credential is.

Nothing here needs changing to unblock it. When the scope is set, this merges as it stands.

The red check is not this branch

Deploy Docs / build fails because it is a GitHub Pages workflow — actions/configure-pages, deploy-pages — inherited from rhnvrm/listmonk-mcp and incapable of running on a Forgejo runner.

It runs here only because this branch predates .forgejo/workflows/. Forgejo reads .forgejo/workflows and ignores .github/workflows entirely when the former exists, which is worth stating because it is not obvious and it decides whether .github/ should be deleted. It should not.

Verified rather than assumed: caldav-mcp carries both directories, its .github/workflows/ci.yml declares jobs quality and container, and across the full 35-record task history of that repository the only job names that have ever run are cargo and docker. jmap-mcp is the same shape.

So once #3 is in the tree, .github/workflows stops running here, publish.yml included, and this check disappears on its own.

**Held, not stalled, and the reason is not the code.** Julian's decision, asked and answered: this waits until the scope of the Listmonk API credential is set. What made it a question rather than a merge. The branch is 41 new tools, not 30 — 29 `@mcp.tool` registrations on `master` against 70 here, counted rather than recalled — and two of them are larger than the count suggests: `update_settings` replaces the whole settings document. Its own docstring records why that is sharp: > `GET /api/settings` masks secrets as bullets. PUTting that body back overwrites real values with literal bullets. So the obvious way to use it — read, change one field, write it back — destroys Listmonk's SMTP credentials. That is the tool working as written, not a misuse of it. `reload_app` sits beside it and makes the new settings live. The credential Julian is scoping decides whether either can be called at all, and `campaigns:send` being distinct from `campaigns:manage` in Listmonk v6.1.0 does not speak to settings permissions. Merging first would have made the credential the only thing standing between an agent and a broken mail path, before anyone had decided what the credential is. Nothing here needs changing to unblock it. When the scope is set, this merges as it stands. ## The red check is not this branch `Deploy Docs / build` fails because it is a GitHub Pages workflow — `actions/configure-pages`, `deploy-pages` — inherited from `rhnvrm/listmonk-mcp` and incapable of running on a Forgejo runner. It runs here only because this branch predates `.forgejo/workflows/`. **Forgejo reads `.forgejo/workflows` and ignores `.github/workflows` entirely when the former exists**, which is worth stating because it is not obvious and it decides whether `.github/` should be deleted. It should not. Verified rather than assumed: `caldav-mcp` carries both directories, its `.github/workflows/ci.yml` declares jobs `quality` and `container`, and across the full 35-record task history of that repository the only job names that have ever run are `cargo` and `docker`. `jmap-mcp` is the same shape. So once #3 is in the tree, `.github/workflows` stops running here, `publish.yml` included, and this check disappears on its own.
jlxq0 merged commit 3b80c11d4f into master 2026-08-26 05:17:52 +00:00
Sign in to join this conversation.
No description provided.