mcp-server-celery 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ariel Even
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-server-celery
3
+ Version: 0.1.0
4
+ Summary: Read-only MCP server for debugging Celery: workers, queue depths, and stuck tasks.
5
+ Keywords: mcp,celery,model-context-protocol,debugging,rabbitmq,redis
6
+ Author: Ariel Even
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Framework :: Celery
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: System :: Monitoring
14
+ Requires-Dist: celery>=5.3
15
+ Requires-Dist: fastmcp>=4.1
16
+ Requires-Dist: mcp>=2.3
17
+ Requires-Dist: redis>=5.0
18
+ Requires-Python: >=3.10
19
+ Project-URL: Repository, https://github.com/taltal-beep/mcp-server-celery
20
+ Project-URL: Issues, https://github.com/taltal-beep/mcp-server-celery/issues
21
+ Description-Content-Type: text/markdown
22
+
23
+ # mcp-server-celery
24
+
25
+ <!-- mcp-name: io.github.taltal-beep/mcp-server-celery -->
26
+
27
+ [![CI](https://github.com/taltal-beep/mcp-server-celery/actions/workflows/ci.yml/badge.svg)](https://github.com/taltal-beep/mcp-server-celery/actions/workflows/ci.yml)
28
+
29
+ An MCP server that lets AI agents debug a running Celery deployment. It checks which workers are alive, what each one consumes, and where messages are piling up — the checks you'd otherwise run by hand with `celery inspect` and `rabbitmqctl` every time something gets stuck.
30
+
31
+ It is **read-only by default**. Broker passwords are masked in every response, including the broker info that Celery's own `inspect stats` sends back in plain text.
32
+
33
+ Works with RabbitMQ (`amqp://`) and Redis (`redis://`, `rediss://`).
34
+
35
+ ## Tools
36
+
37
+ | Tool | What it answers |
38
+ | --- | --- |
39
+ | `list_workers` | Which workers respond, their pool and concurrency, uptime, prefetch count, tasks processed, and the queues each one listens to. |
40
+ | `queue_depths` | How many messages are waiting in each queue and which workers consume it. With no arguments it checks your configured queues plus every queue a live worker consumes. |
41
+
42
+ Coming next: `active_tasks`, `reserved_tasks`, `scheduled_tasks`, `task_status`, and `diagnose_stuck`, which cross-references the above to flag queues no worker listens to, tasks routed to queues with no consumer, and workers that stopped responding. See the [roadmap](#roadmap).
43
+
44
+ ## Install
45
+
46
+ Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/).
47
+
48
+ ```bash
49
+ uvx mcp-server-celery
50
+ ```
51
+
52
+ Until the first PyPI release, run it from GitHub:
53
+
54
+ ```bash
55
+ uvx --from git+https://github.com/taltal-beep/mcp-server-celery mcp-server-celery
56
+ ```
57
+
58
+ ## Configuration
59
+
60
+ All configuration is through environment variables.
61
+
62
+ | Variable | Default | Purpose |
63
+ | --- | --- | --- |
64
+ | `CELERY_BROKER_URL` | `amqp://guest:guest@localhost:5672//` | Broker to inspect. |
65
+ | `CELERY_RESULT_BACKEND` | — | Result backend (used by `task_status`, coming in the next release). |
66
+ | `CELERY_APP` | — | Import path of your app, e.g. `proj.celery:app`. Lets the server see your `task_queues` and routes. `CELERY_BROKER_URL` still overrides the app's broker if both are set. |
67
+ | `CELERY_INSPECT_TIMEOUT` | `1.0` | Seconds to wait for worker replies. Raise it for large or slow clusters. |
68
+
69
+ `CELERY_APP` has to be importable, so run the server from your project's directory and environment:
70
+
71
+ ```bash
72
+ uv run --with mcp-server-celery mcp-server-celery
73
+ ```
74
+
75
+ ## Connect it
76
+
77
+ ### Claude Code
78
+
79
+ ```bash
80
+ claude mcp add celery -e CELERY_BROKER_URL=redis://localhost:6379/0 -- uvx mcp-server-celery
81
+ ```
82
+
83
+ ### Cursor
84
+
85
+ In `.cursor/mcp.json`:
86
+
87
+ ```json
88
+ {
89
+ "mcpServers": {
90
+ "celery": {
91
+ "command": "uvx",
92
+ "args": ["mcp-server-celery"],
93
+ "env": { "CELERY_BROKER_URL": "redis://localhost:6379/0" }
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
99
+ ### Claude Desktop
100
+
101
+ In `claude_desktop_config.json`, use the same `command`, `args` and `env` block as Cursor, under `mcpServers`.
102
+
103
+ ## Example
104
+
105
+ Ask the agent "why are my email tasks not running?" and it can call `queue_depths`:
106
+
107
+ ```json
108
+ {
109
+ "broker": "amqp://app:****@rabbit:5672//",
110
+ "queues": [
111
+ { "queue": "celery", "exists": true, "messages": 0, "consumers": 2, "workers_consuming": ["w1@web", "w2@web"] },
112
+ { "queue": "emails", "exists": true, "messages": 1843, "consumers": 0, "workers_consuming": [] }
113
+ ]
114
+ }
115
+ ```
116
+
117
+ `emails` has a backlog and no worker consumes it: the workers were started without `-Q emails`.
118
+
119
+ ## Notes on the data
120
+
121
+ - **Redis** has no notion of consumers, so `consumers` is `null`; use `workers_consuming`, which comes from the workers themselves. Redis also deletes a queue's key once it is empty, so a drained queue shows `"messages": 0, "exists": null`.
122
+ - **RabbitMQ** checks use a passive declare, so the server never creates a queue. A queue that doesn't exist shows `"exists": false`.
123
+ - Workers that are too busy to answer within `CELERY_INSPECT_TIMEOUT` don't appear in `list_workers`. If a worker you expect is missing, try a larger `timeout` argument before assuming it's dead.
124
+
125
+ ## Safety
126
+
127
+ - Every tool is annotated `readOnlyHint: true`. Write actions (revoke, retry, purge) will only be available behind an explicit opt-in flag.
128
+ - URL credentials and `password`/`secret`/`token` fields are masked in all output and error messages.
129
+
130
+ ## Roadmap
131
+
132
+ **v0.1** — `list_workers`, `queue_depths` ✅
133
+
134
+ **v1** — `active_tasks`, `reserved_tasks`, `scheduled_tasks`, `task_status(task_id)`, `diagnose_stuck`
135
+
136
+ **v2** — `summarize_failures`: groups recent failures by task name and exception type. Celery keeps no failure history by default, so this needs an event listener.
137
+
138
+ ## Development
139
+
140
+ ```bash
141
+ uv sync
142
+ uv run pytest tests/unit
143
+ uv run ruff check . && uv run ruff format --check .
144
+ ```
145
+
146
+ Integration tests start a real worker and need a broker:
147
+
148
+ ```bash
149
+ CELERY_BROKER_URL=redis://localhost:6379/0 uv run pytest tests/integration -m integration
150
+ ```
151
+
152
+ CI runs them against Redis and RabbitMQ on every push.
153
+
154
+ ## License
155
+
156
+ MIT
@@ -0,0 +1,134 @@
1
+ # mcp-server-celery
2
+
3
+ <!-- mcp-name: io.github.taltal-beep/mcp-server-celery -->
4
+
5
+ [![CI](https://github.com/taltal-beep/mcp-server-celery/actions/workflows/ci.yml/badge.svg)](https://github.com/taltal-beep/mcp-server-celery/actions/workflows/ci.yml)
6
+
7
+ An MCP server that lets AI agents debug a running Celery deployment. It checks which workers are alive, what each one consumes, and where messages are piling up — the checks you'd otherwise run by hand with `celery inspect` and `rabbitmqctl` every time something gets stuck.
8
+
9
+ It is **read-only by default**. Broker passwords are masked in every response, including the broker info that Celery's own `inspect stats` sends back in plain text.
10
+
11
+ Works with RabbitMQ (`amqp://`) and Redis (`redis://`, `rediss://`).
12
+
13
+ ## Tools
14
+
15
+ | Tool | What it answers |
16
+ | --- | --- |
17
+ | `list_workers` | Which workers respond, their pool and concurrency, uptime, prefetch count, tasks processed, and the queues each one listens to. |
18
+ | `queue_depths` | How many messages are waiting in each queue and which workers consume it. With no arguments it checks your configured queues plus every queue a live worker consumes. |
19
+
20
+ Coming next: `active_tasks`, `reserved_tasks`, `scheduled_tasks`, `task_status`, and `diagnose_stuck`, which cross-references the above to flag queues no worker listens to, tasks routed to queues with no consumer, and workers that stopped responding. See the [roadmap](#roadmap).
21
+
22
+ ## Install
23
+
24
+ Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/).
25
+
26
+ ```bash
27
+ uvx mcp-server-celery
28
+ ```
29
+
30
+ Until the first PyPI release, run it from GitHub:
31
+
32
+ ```bash
33
+ uvx --from git+https://github.com/taltal-beep/mcp-server-celery mcp-server-celery
34
+ ```
35
+
36
+ ## Configuration
37
+
38
+ All configuration is through environment variables.
39
+
40
+ | Variable | Default | Purpose |
41
+ | --- | --- | --- |
42
+ | `CELERY_BROKER_URL` | `amqp://guest:guest@localhost:5672//` | Broker to inspect. |
43
+ | `CELERY_RESULT_BACKEND` | — | Result backend (used by `task_status`, coming in the next release). |
44
+ | `CELERY_APP` | — | Import path of your app, e.g. `proj.celery:app`. Lets the server see your `task_queues` and routes. `CELERY_BROKER_URL` still overrides the app's broker if both are set. |
45
+ | `CELERY_INSPECT_TIMEOUT` | `1.0` | Seconds to wait for worker replies. Raise it for large or slow clusters. |
46
+
47
+ `CELERY_APP` has to be importable, so run the server from your project's directory and environment:
48
+
49
+ ```bash
50
+ uv run --with mcp-server-celery mcp-server-celery
51
+ ```
52
+
53
+ ## Connect it
54
+
55
+ ### Claude Code
56
+
57
+ ```bash
58
+ claude mcp add celery -e CELERY_BROKER_URL=redis://localhost:6379/0 -- uvx mcp-server-celery
59
+ ```
60
+
61
+ ### Cursor
62
+
63
+ In `.cursor/mcp.json`:
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "celery": {
69
+ "command": "uvx",
70
+ "args": ["mcp-server-celery"],
71
+ "env": { "CELERY_BROKER_URL": "redis://localhost:6379/0" }
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ ### Claude Desktop
78
+
79
+ In `claude_desktop_config.json`, use the same `command`, `args` and `env` block as Cursor, under `mcpServers`.
80
+
81
+ ## Example
82
+
83
+ Ask the agent "why are my email tasks not running?" and it can call `queue_depths`:
84
+
85
+ ```json
86
+ {
87
+ "broker": "amqp://app:****@rabbit:5672//",
88
+ "queues": [
89
+ { "queue": "celery", "exists": true, "messages": 0, "consumers": 2, "workers_consuming": ["w1@web", "w2@web"] },
90
+ { "queue": "emails", "exists": true, "messages": 1843, "consumers": 0, "workers_consuming": [] }
91
+ ]
92
+ }
93
+ ```
94
+
95
+ `emails` has a backlog and no worker consumes it: the workers were started without `-Q emails`.
96
+
97
+ ## Notes on the data
98
+
99
+ - **Redis** has no notion of consumers, so `consumers` is `null`; use `workers_consuming`, which comes from the workers themselves. Redis also deletes a queue's key once it is empty, so a drained queue shows `"messages": 0, "exists": null`.
100
+ - **RabbitMQ** checks use a passive declare, so the server never creates a queue. A queue that doesn't exist shows `"exists": false`.
101
+ - Workers that are too busy to answer within `CELERY_INSPECT_TIMEOUT` don't appear in `list_workers`. If a worker you expect is missing, try a larger `timeout` argument before assuming it's dead.
102
+
103
+ ## Safety
104
+
105
+ - Every tool is annotated `readOnlyHint: true`. Write actions (revoke, retry, purge) will only be available behind an explicit opt-in flag.
106
+ - URL credentials and `password`/`secret`/`token` fields are masked in all output and error messages.
107
+
108
+ ## Roadmap
109
+
110
+ **v0.1** — `list_workers`, `queue_depths` ✅
111
+
112
+ **v1** — `active_tasks`, `reserved_tasks`, `scheduled_tasks`, `task_status(task_id)`, `diagnose_stuck`
113
+
114
+ **v2** — `summarize_failures`: groups recent failures by task name and exception type. Celery keeps no failure history by default, so this needs an event listener.
115
+
116
+ ## Development
117
+
118
+ ```bash
119
+ uv sync
120
+ uv run pytest tests/unit
121
+ uv run ruff check . && uv run ruff format --check .
122
+ ```
123
+
124
+ Integration tests start a real worker and need a broker:
125
+
126
+ ```bash
127
+ CELERY_BROKER_URL=redis://localhost:6379/0 uv run pytest tests/integration -m integration
128
+ ```
129
+
130
+ CI runs them against Redis and RabbitMQ on every push.
131
+
132
+ ## License
133
+
134
+ MIT
@@ -0,0 +1,73 @@
1
+ [project]
2
+ name = "mcp-server-celery"
3
+ version = "0.1.0"
4
+ description = "Read-only MCP server for debugging Celery: workers, queue depths, and stuck tasks."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.10"
9
+ keywords = [
10
+ "mcp",
11
+ "celery",
12
+ "model-context-protocol",
13
+ "debugging",
14
+ "rabbitmq",
15
+ "redis",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Framework :: Celery",
20
+ "Intended Audience :: Developers",
21
+ "Programming Language :: Python :: 3",
22
+ "Topic :: System :: Monitoring",
23
+ ]
24
+ dependencies = [
25
+ "celery>=5.3",
26
+ "fastmcp>=4.1",
27
+ "mcp>=2.3",
28
+ "redis>=5.0",
29
+ ]
30
+
31
+ [[project.authors]]
32
+ name = "Ariel Even"
33
+
34
+ [project.urls]
35
+ Repository = "https://github.com/taltal-beep/mcp-server-celery"
36
+ Issues = "https://github.com/taltal-beep/mcp-server-celery/issues"
37
+
38
+ [project.scripts]
39
+ mcp-server-celery = "mcp_server_celery.server:main"
40
+
41
+ [build-system]
42
+ requires = ["uv_build>=0.8,<0.14"]
43
+ build-backend = "uv_build"
44
+
45
+ [dependency-groups]
46
+ dev = [
47
+ "pytest>=8",
48
+ "ruff>=0.15",
49
+ ]
50
+
51
+ [tool.pytest.ini_options]
52
+ markers = ["integration: needs a live broker (set CELERY_BROKER_URL)"]
53
+ filterwarnings = ["error"]
54
+
55
+ [tool.ruff]
56
+ line-length = 120
57
+
58
+ [tool.ruff.lint]
59
+ select = [
60
+ "E",
61
+ "F",
62
+ "I",
63
+ "B",
64
+ "UP",
65
+ "S",
66
+ "BLE",
67
+ ]
68
+
69
+ [tool.ruff.lint.per-file-ignores]
70
+ "tests/**" = [
71
+ "S101",
72
+ "S603",
73
+ ]
@@ -0,0 +1,53 @@
1
+ [project]
2
+ name = "mcp-server-celery"
3
+ version = "0.1.0"
4
+ description = "Read-only MCP server for debugging Celery: workers, queue depths, and stuck tasks."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "Ariel Even" }]
9
+ requires-python = ">=3.10"
10
+ keywords = ["mcp", "celery", "model-context-protocol", "debugging", "rabbitmq", "redis"]
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Framework :: Celery",
14
+ "Intended Audience :: Developers",
15
+ "Programming Language :: Python :: 3",
16
+ "Topic :: System :: Monitoring",
17
+ ]
18
+ dependencies = [
19
+ "celery>=5.3",
20
+ "fastmcp>=4.1",
21
+ "mcp>=2.3",
22
+ "redis>=5.0",
23
+ ]
24
+
25
+ [project.urls]
26
+ Repository = "https://github.com/taltal-beep/mcp-server-celery"
27
+ Issues = "https://github.com/taltal-beep/mcp-server-celery/issues"
28
+
29
+ [project.scripts]
30
+ mcp-server-celery = "mcp_server_celery.server:main"
31
+
32
+ [build-system]
33
+ requires = ["uv_build>=0.8,<0.14"]
34
+ build-backend = "uv_build"
35
+
36
+ [dependency-groups]
37
+ dev = [
38
+ "pytest>=8",
39
+ "ruff>=0.15",
40
+ ]
41
+
42
+ [tool.pytest.ini_options]
43
+ markers = ["integration: needs a live broker (set CELERY_BROKER_URL)"]
44
+ filterwarnings = ["error"]
45
+
46
+ [tool.ruff]
47
+ line-length = 120
48
+
49
+ [tool.ruff.lint]
50
+ select = ["E", "F", "I", "B", "UP", "S", "BLE"]
51
+
52
+ [tool.ruff.lint.per-file-ignores]
53
+ "tests/**" = ["S101", "S603"]
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("mcp-server-celery")
@@ -0,0 +1,3 @@
1
+ from mcp_server_celery.server import main
2
+
3
+ main()
@@ -0,0 +1,80 @@
1
+ import os
2
+ import sys
3
+ from contextlib import suppress
4
+ from typing import Any, Protocol
5
+
6
+ from celery import Celery
7
+ from celery.utils.imports import symbol_by_name
8
+ from kombu.exceptions import ChannelError
9
+
10
+ from mcp_server_celery.config import DEFAULT_BROKER_URL, Settings
11
+
12
+
13
+ class Inspector(Protocol):
14
+ def ping(self) -> dict[str, Any] | None: ...
15
+ def stats(self) -> dict[str, Any] | None: ...
16
+ def active_queues(self) -> dict[str, list[dict[str, Any]]] | None: ...
17
+
18
+
19
+ class CeleryClient:
20
+ def __init__(self, app: Celery, inspect_timeout: float = 1.0):
21
+ self.app = app
22
+ self.inspect_timeout = inspect_timeout
23
+
24
+ @classmethod
25
+ def from_settings(cls, settings: Settings) -> "CeleryClient":
26
+ if settings.app_path:
27
+ if os.getcwd() not in sys.path:
28
+ sys.path.insert(0, os.getcwd()) # match `celery -A`, which imports from cwd
29
+ app = symbol_by_name(settings.app_path)
30
+ if settings.broker_url:
31
+ app.conf.broker_url = settings.broker_url
32
+ if settings.result_backend:
33
+ app.conf.result_backend = settings.result_backend
34
+ else:
35
+ app = Celery(
36
+ "mcp-server-celery",
37
+ broker=settings.broker_url or DEFAULT_BROKER_URL,
38
+ backend=settings.result_backend,
39
+ )
40
+ return cls(app, settings.inspect_timeout)
41
+
42
+ def inspect(self, timeout: float | None = None, destination: list[str] | None = None) -> Inspector:
43
+ return self.app.control.inspect(timeout=timeout or self.inspect_timeout, destination=destination)
44
+
45
+ def configured_queues(self) -> list[str]:
46
+ queues = self.app.conf.task_queues or []
47
+ names = [q.name for q in queues]
48
+ default = self.app.conf.task_default_queue
49
+ return names if default in names else [default, *names]
50
+
51
+ def queue_depths(self, names: list[str]) -> list[dict[str, Any]]:
52
+ with self.app.connection_for_read() as conn:
53
+ conn.ensure_connection(max_retries=1)
54
+ is_amqp = conn.transport.driver_type == "amqp"
55
+ return [self._probe(conn, name, is_amqp) for name in names]
56
+
57
+ @staticmethod
58
+ def _probe(conn, name: str, is_amqp: bool) -> dict[str, Any]:
59
+ channel = conn.channel()
60
+ try:
61
+ _, messages, consumers = channel.queue_declare(queue=name, passive=True)
62
+ except ChannelError:
63
+ if is_amqp:
64
+ return {
65
+ "queue": name,
66
+ "exists": False,
67
+ "messages": None,
68
+ "consumers": None,
69
+ }
70
+ # Virtual transports drop the key once a queue is drained.
71
+ return {"queue": name, "exists": None, "messages": 0, "consumers": None}
72
+ finally:
73
+ with suppress(ChannelError, OSError):
74
+ channel.close()
75
+ return {
76
+ "queue": name,
77
+ "exists": True,
78
+ "messages": messages,
79
+ "consumers": consumers if is_amqp else None,
80
+ }
@@ -0,0 +1,22 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+
4
+ DEFAULT_BROKER_URL = "amqp://guest:guest@localhost:5672//"
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class Settings:
9
+ broker_url: str | None = None
10
+ result_backend: str | None = None
11
+ app_path: str | None = None
12
+ inspect_timeout: float = 1.0
13
+
14
+ @classmethod
15
+ def from_env(cls, env: dict[str, str] | None = None) -> "Settings":
16
+ env = os.environ if env is None else env
17
+ return cls(
18
+ broker_url=env.get("CELERY_BROKER_URL") or None,
19
+ result_backend=env.get("CELERY_RESULT_BACKEND") or None,
20
+ app_path=env.get("CELERY_APP") or None,
21
+ inspect_timeout=float(env.get("CELERY_INSPECT_TIMEOUT", "1.0")),
22
+ )
File without changes
@@ -0,0 +1,22 @@
1
+ import re
2
+ from typing import Any
3
+
4
+ SECRET_KEYS = frozenset({"password", "passwd", "secret", "token", "api_key", "apikey"})
5
+ MASK = "****"
6
+ _URL_CREDENTIALS = re.compile(r"(?P<scheme>[a-zA-Z][a-zA-Z0-9+.-]*://)(?P<user>[^:@/\s]*):(?P<password>[^@/\s]+)@")
7
+
8
+
9
+ def mask_url(url: str) -> str:
10
+ return _URL_CREDENTIALS.sub(lambda m: f"{m['scheme']}{m['user']}:{MASK}@", url)
11
+
12
+
13
+ def redact(value: Any) -> Any:
14
+ if isinstance(value, dict):
15
+ return {
16
+ k: (MASK if isinstance(k, str) and k.lower() in SECRET_KEYS and v else redact(v)) for k, v in value.items()
17
+ }
18
+ if isinstance(value, (list, tuple)):
19
+ return [redact(v) for v in value]
20
+ if isinstance(value, str):
21
+ return mask_url(value)
22
+ return value
@@ -0,0 +1,72 @@
1
+ from collections.abc import Callable
2
+ from functools import wraps
3
+ from typing import Annotated, Any
4
+
5
+ from fastmcp import FastMCP
6
+ from fastmcp.exceptions import ToolError
7
+ from kombu.exceptions import OperationalError
8
+ from mcp.types import ToolAnnotations
9
+ from pydantic import Field
10
+
11
+ from mcp_server_celery import tools
12
+ from mcp_server_celery.client import CeleryClient
13
+ from mcp_server_celery.config import Settings
14
+ from mcp_server_celery.redact import mask_url
15
+
16
+ READ_ONLY = ToolAnnotations(read_only_hint=True, destructive_hint=False, open_world_hint=True)
17
+
18
+ Timeout = Annotated[
19
+ float | None,
20
+ Field(description="Seconds to wait for worker replies. Defaults to CELERY_INSPECT_TIMEOUT."),
21
+ ]
22
+
23
+
24
+ def _broker_errors(client: CeleryClient, fn: Callable[..., Any]) -> Callable[..., Any]:
25
+ @wraps(fn)
26
+ def wrapper(*args, **kwargs):
27
+ try:
28
+ return fn(*args, **kwargs)
29
+ except (OperationalError, ConnectionError, OSError) as exc:
30
+ broker = mask_url(client.app.conf.broker_url or "")
31
+ raise ToolError(f"Cannot reach broker {broker}: {mask_url(str(exc))}") from exc
32
+
33
+ return wrapper
34
+
35
+
36
+ def build_server(client: CeleryClient) -> FastMCP:
37
+ mcp = FastMCP(
38
+ "celery",
39
+ instructions=(
40
+ "Read-only diagnostics for a Celery deployment: workers, queues and tasks. "
41
+ "Broker credentials are always masked."
42
+ ),
43
+ )
44
+
45
+ def _wrap(fn):
46
+ return _broker_errors(client, fn)
47
+
48
+ @mcp.tool(annotations=READ_ONLY)
49
+ @_wrap
50
+ def list_workers(timeout: Timeout = None) -> dict:
51
+ """List workers that reply to broadcast, with pool, concurrency, uptime and the queues each consumes."""
52
+ return tools.list_workers(client, timeout=timeout)
53
+
54
+ @mcp.tool(annotations=READ_ONLY)
55
+ @_wrap
56
+ def queue_depths(
57
+ queues: Annotated[
58
+ list[str] | None,
59
+ Field(
60
+ description="Queue names to check. Defaults to configured queues plus every queue a worker consumes."
61
+ ),
62
+ ] = None,
63
+ timeout: Timeout = None,
64
+ ) -> dict:
65
+ """Count waiting messages per queue and show which workers consume each one. Works on RabbitMQ and Redis."""
66
+ return tools.queue_depths(client, queues=queues, timeout=timeout)
67
+
68
+ return mcp
69
+
70
+
71
+ def main() -> None:
72
+ build_server(CeleryClient.from_settings(Settings.from_env())).run()
@@ -0,0 +1,44 @@
1
+ from typing import Any
2
+
3
+ from mcp_server_celery.client import CeleryClient
4
+ from mcp_server_celery.redact import redact
5
+
6
+
7
+ def list_workers(client: CeleryClient, timeout: float | None = None) -> dict[str, Any]:
8
+ inspector = client.inspect(timeout=timeout)
9
+ pings = inspector.ping() or {}
10
+ stats = inspector.stats() or {}
11
+ queues = inspector.active_queues() or {}
12
+
13
+ workers = []
14
+ for hostname in sorted(set(pings) | set(stats) | set(queues)):
15
+ worker_stats = stats.get(hostname, {})
16
+ pool = worker_stats.get("pool") or {}
17
+ workers.append(
18
+ {
19
+ "hostname": hostname,
20
+ "responded_to_ping": hostname in pings,
21
+ "pid": worker_stats.get("pid"),
22
+ "uptime_seconds": worker_stats.get("uptime"),
23
+ "pool": pool.get("implementation"),
24
+ "concurrency": pool.get("max-concurrency"),
25
+ "prefetch_count": worker_stats.get("prefetch_count"),
26
+ "tasks_processed": worker_stats.get("total", {}),
27
+ "queues": sorted(q["name"] for q in queues.get(hostname, [])),
28
+ }
29
+ )
30
+ return redact({"worker_count": len(workers), "workers": workers})
31
+
32
+
33
+ def queue_depths(client: CeleryClient, queues: list[str] | None = None, timeout: float | None = None) -> dict[str, Any]:
34
+ consumed_by: dict[str, list[str]] = {}
35
+ for hostname, worker_queues in (client.inspect(timeout=timeout).active_queues() or {}).items():
36
+ for q in worker_queues:
37
+ consumed_by.setdefault(q["name"], []).append(hostname)
38
+
39
+ names = queues or sorted(set(client.configured_queues()) | set(consumed_by))
40
+ results = []
41
+ for depth in client.queue_depths(names):
42
+ listeners = sorted(consumed_by.get(depth["queue"], []))
43
+ results.append({**depth, "workers_consuming": listeners})
44
+ return redact({"broker": client.app.conf.broker_url, "queues": results})