Metadata-Version: 2.4
Name: mcp-celery
Version: 0.2.0
Summary: Expose Celery tasks as async MCP tools with automatic lifecycle management
Project-URL: Homepage, https://github.com/saleemasekrea000/mcp-celery
Project-URL: Repository, https://github.com/saleemasekrea000/mcp-celery
Project-URL: Issues, https://github.com/saleemasekrea000/mcp-celery/issues
Project-URL: Changelog, https://github.com/saleemasekrea000/mcp-celery/blob/main/CHANGELOG.md
Author-email: Saleem <saleem@ancileo.com>
License: MIT License
        
        Copyright (c) 2026 Saleem
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: async,celery,llm,long-running,mcp,model-context-protocol,tools
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Celery
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: celery>=5.3.0
Requires-Dist: mcp>=1.0.0
Provides-Extra: dev
Requires-Dist: build>=1.2.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: redis>=5.0; extra == 'dev'
Requires-Dist: ruff>=0.4.0; extra == 'dev'
Requires-Dist: twine>=5.0.0; extra == 'dev'
Requires-Dist: ty>=0.0.29; extra == 'dev'
Description-Content-Type: text/markdown

# mcp-celery

Expose [Celery](https://docs.celeryq.dev/) tasks as asynchronous [MCP](https://modelcontextprotocol.io/) tools — with automatic lifecycle management and no boilerplate.

Surfacing a long-running Celery task to an MCP client normally means hand-writing start / status / result / cancel tools for every task. `mcp-celery` generates that surface for you: register a task once and the right MCP tools appear, from a single synchronous call to full polling, depending on the strategy you pick.

> **Status:** Alpha. The four *stable* strategies use only ratified MCP primitives and work on any MCP client today.

## Installation

```bash
pip install mcp-celery
```

Requires Python ≥ 3.10, `mcp >= 1.0`, and Celery with a broker and result backend (e.g. Redis).

## Quick start

```python
from celery import Celery
from mcp_celery import AsyncToolServer

celery_app = Celery(
    "myapp",
    broker="redis://localhost:6379/0",
    backend="redis://localhost:6379/1",
)

@celery_app.task
def generate_report(user_id: str) -> dict:
    ...  # slow work
    return {"user_id": user_id, "revenue": 42000}

server = AsyncToolServer("my-server", celery_app=celery_app)
server.register_async_tool(generate_report, description="Generate a report")

if __name__ == "__main__":
    server.run()
```

That single registration generates the MCP tools. The default `AdaptiveExposureStrategy` returns the result inline when the task is fast, and hands back a token plus `generate_report_status` / `_result` / `_cancel` tools when it runs long.

To define and register a task in one step, use the decorator:

```python
@server.async_tool(cancel=True, result_ttl=300)
def train_model(dataset: str) -> dict:
    ...
```

## Choosing a strategy

The **exposure strategy** decides how a task is surfaced over MCP. Pick by task duration and client constraints; see [docs/strategy-stability.md](docs/strategy-stability.md) for full guidance.

| Strategy | Tools/task | Best for | Stability |
|---|---|---|---|
| `AdaptiveExposureStrategy` **(default)** | 1 + polling fallback | Mixed or unknown durations — inline if fast, polling if slow; streams progress while blocking | stable |
| `BlockingExposureStrategy` | 1 | Short tasks — one synchronous call, result inline | stable |
| `SingleToolExposureStrategy` | 1 (action-driven) | Full lifecycle in one tool — minimal tool budget | stable |
| `PollingExposureStrategy` | 4 (`_start` / `_status` / `_result` / `_cancel`) | Long tasks, or explicit lifecycle control | stable |
| `OperationResourceStrategy` | 1 + MCP task/resource API | MCP-native async tasks (SEP-1391) | **experimental** |

```python
from mcp_celery.exposure import BlockingExposureStrategy

server = AsyncToolServer(
    "my-server",
    celery_app=celery_app,
    strategy=BlockingExposureStrategy(max_wait=15.0),
)
```

Every stable strategy relies only on MCP tools, so it works on any client. `OperationResourceStrategy` depends on the un-ratified SEP-1391 Task primitive and emits an `ExperimentalFeatureWarning` on use.

## Architecture

`AsyncToolServer` sits on two swappable layers:

- **Backend** (`AbstractBackend`) — *how a task runs*. `CeleryBackend` is the only implementation today.
- **Exposure strategy** (`AbstractExposureStrategy`) — *how the task is surfaced over MCP* (the table above).

Task state is normalised to a single `TaskStatus` enum (`PENDING`, `RUNNING`, `COMPLETED`, `FAILED`, `CANCELLED`, `RETRYING`). The token a client carries across calls is the Celery task id. Transport (stdio, SSE, streamable-HTTP) is delegated to FastMCP via `server.run()`.

Attach to a FastMCP server you already configured with
`AsyncToolServer.from_fastmcp(existing_mcp, celery_app=...)`.

## Examples

Runnable end-to-end examples live in [`examples/`](examples/), each driven by a single script that handles Redis, the virtualenv, and the Celery worker:

```bash
bash examples/run.sh adaptive       # AdaptiveExposureStrategy (default)
bash examples/run.sh blocking       # BlockingExposureStrategy
bash examples/run.sh single_tool    # SingleToolExposureStrategy
bash examples/run.sh with_package   # PollingExposureStrategy
bash examples/run.sh approach3      # OperationResourceStrategy (experimental)
bash examples/run.sh baseline       # hand-written MCP tools, for comparison
```

Requires `python3`, `bash`, and Docker (only to auto-start Redis).

## Limitations

- **Token validity.** Celery cannot tell an unknown or expired task id from a pending one — both report `PENDING`. A polling strategy given a bad or TTL-expired token keeps reporting `PENDING` rather than erroring.
- **Cancellation is best-effort.** `cancel` issues `revoke(terminate=True)` and reports the *current* state, which is often still `RUNNING` immediately after, since revocation is asynchronous.
- **`OperationResourceStrategy` is experimental.** It depends on the un-ratified MCP SEP-1391 Task primitive; most clients do not understand the returned `task://` URI.

## Development

```bash
pip install -e ".[dev]"
pytest                 # unit tests use an in-memory backend — no Redis or worker needed
ruff check src tests
```

## License

MIT — see [LICENSE](LICENSE).
