Skip to main content
Glama
faizzansiddiq

AetherServe

README.md
# AetherServe

**An operations layer for Model Context Protocol (MCP) servers.**

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](pyproject.toml)
[![Tests](https://img.shields.io/badge/tests-passing-brightgreen.svg)](tests/)

AetherServe wraps an MCP server built with the official
[Model Context Protocol Python SDK](https://github.com/modelcontextprotocol/python-sdk)
and adds the operational concerns that matter once that server leaves a
notebook and goes into a real deployment: **metrics, health checks, rate
limiting, structured logging, and lifecycle hooks.**

It is a small, dependency-light middleware layer, not a fork of the
protocol or the SDK. You keep writing tools the normal way; AetherServe
observes and governs the calls around them.

---

## Why

The MCP SDK gives you the protocol. It does not give you an answer to
questions every production deployment eventually asks:

- *Which tools are slow, and how slow, at the p50/p95/p99?*
- *Is this server healthy enough to keep receiving traffic?*
- *What stops one client from calling an expensive tool 500 times a second?*
- *How do I correlate a log line with the tool call that produced it?*
- *How do I run code exactly once at startup and exactly once at shutdown?*

AetherServe answers all five with a small, composable API, without asking
you to adopt a heavier framework or change how you write MCP tools.

## Installation

```bash
pip install aetherserve
```

This pulls in the `mcp` package as its only required runtime dependency.

For the bundled examples (which use Starlette/Uvicorn to expose HTTP
health and metrics endpoints):

```bash
pip install "aetherserve[examples]"
```

## Quickstart

```python
from mcp.server.fastmcp import FastMCP
from aetherserve import AetherServer, AetherServeConfig

raw_server = FastMCP("weather-server")
server = AetherServer(raw_server, AetherServeConfig(service_name="weather-server"))

@server.tool()
def get_forecast(city: str) -> str:
    return f"Sunny in {city}"

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

Every call to `get_forecast` is now timed, counted, rate-limited against
the server's default policy, wrapped in a trace span, and logged with a
correlation id — without a single line of that logic living in
`get_forecast` itself.

## Core Concepts

| Concern | Module | What you get |
|---|---|---|
| Metrics | `aetherserve.observability.metrics` | Counters, gauges, histograms; Prometheus text exposition |
| Logging | `aetherserve.observability.logging_setup` | JSON or text logs, automatic request-id correlation |
| Tracing | `aetherserve.observability.tracing` | Lightweight nested spans, pluggable exporter |
| Health | `aetherserve.health` | Named checks, critical vs. degraded aggregation, HTTP-status mapping |
| Rate limiting | `aetherserve.ratelimit` | Token-bucket limiter, global or per-client scope, per-tool overrides |
| Lifecycle | `aetherserve.hooks.lifecycle` | `startup` / `shutdown` / `before_tool_call` / `after_tool_call` / `tool_call_error` events |

See [`ARCHITECTURE.md`](ARCHITECTURE.md) for how these pieces fit together
around the `AetherServer` wrapper, and [`docs/`](docs/) for a deeper dive
into each subsystem.

## Per-tool rate limits

```python
from aetherserve.ratelimit.policies import RateLimitScope

server.set_rate_limit(
    "generate_image",
    capacity=2,
    refill_rate=0.1,          # ~1 token every 10 seconds
    scope=RateLimitScope.PER_CLIENT,
)

@server.tool(client_key=lambda client_id, prompt: client_id)
def generate_image(client_id: str, prompt: str) -> str:
    ...
```

## Health checks

```python
def database_ping() -> None:
    connection.execute("SELECT 1")  # raises on failure

server.add_health_check("database", database_ping, critical=True)
```

`server.health.run_all()` aggregates every registered check into a single
`healthy` / `degraded` / `unhealthy` report; `aetherserve.health.endpoint.health_response`
maps that straight into an HTTP status code your framework of choice can
return.

## Examples

- [`examples/basic_server.py`](examples/basic_server.py) — the smallest
  possible AetherServe server.
- [`examples/rate_limited_server.py`](examples/rate_limited_server.py) —
  a stricter, per-client policy on one expensive tool.
- [`examples/full_observability_demo.py`](examples/full_observability_demo.py) —
  wiring `/healthz` and `/metrics` into a Starlette app alongside the MCP
  tool server.

## Development

```bash
git clone https://github.com/faizansiddiq/aetherserve.git
cd aetherserve
pip install -e ".[dev]"
pytest
mypy src/aetherserve
ruff check .
```

See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the full workflow and
[`docs/testing.md`](docs/testing.md) for how the test suite is structured
(including the in-repo fake MCP server used so unit tests don't require a
live protocol connection).

## Relationship to the MCP SDK

AetherServe is **not** a fork, rebrand, or replacement of the official
Model Context Protocol Python SDK. It is an independent project that
takes an `mcp.server.fastmcp.FastMCP` instance as a constructor argument
and wraps it, in the same sense that a WSGI middleware wraps a WSGI app.
Full attribution for the underlying SDK is in [`NOTICE.md`](NOTICE.md).

## License

MIT — see [`LICENSE`](LICENSE). Copyright (c) 2026 Faizan Siddiq.