Skip to main content
Glama
shgsousa
by shgsousa
README.md
# Blob Storage Service

A lightweight **MCP data broker** that stores large binary payloads under a UUID, enabling MCP clients and agents to exchange massive results without flooding the LLM context window.

## Architecture

```
MCP Client / LLM Agent
        │
        │  tool call (large result)
        ▼
   MCP Server  ──store_blob──▶  Blob Storage Service  ──▶  SQLite + local files
        │                              │
        │  returns UUID                │  retrieve_blob(uuid)
        ◀──────────────────────────────┘
```

The agent only ever sees a **UUID token** — not the raw bytes.

## Project Structure

```
src/blob_storage/
├── __init__.py       # package metadata
├── config.py         # pydantic-settings (env / .env)
├── models.py         # SQLAlchemy ORM + Pydantic schemas
├── database.py       # async engine, session factory, init_db()
├── storage.py        # StorageBackend ABC + LocalFileBackend
├── service.py        # BlobService (orchestration)
├── api.py            # FastAPI REST API
└── mcp_server.py     # MCP server (HTTP Streamable, /mcp)

tests/
├── conftest.py       # shared fixtures
├── test_api.py       # API integration tests
└── test_service.py   # service unit tests
```

## Quickstart

### Install dependencies

```bash
uv sync
```

### Start the REST API

```bash
uv run blob-api                            # defaults from config / .env
uv run blob-api --host 0.0.0.0 --port 9000 # override at runtime
```

Interactive docs available at `http://<host>:<port>/docs`.

### Start the MCP server

```bash
uv run blob-mcp                            # defaults: 0.0.0.0:8001
uv run blob-mcp --host 0.0.0.0 --port 9001
```

The MCP endpoint is served at `http://<host>:<port>/mcp`.

## Configuration

Settings are read from environment variables or a `.env` file in the project root.

| Variable | Default | Description |
|---|---|---|
| `STORAGE_DIR` | `./data/blobs` | Directory for blob files |
| `DATABASE_URL` | `sqlite+aiosqlite:///./data/metadata.db` | SQLAlchemy async DB URL |
| `DEFAULT_TTL_SECONDS` | `7776000` | Default TTL — 90 days. Use `0` for no expiry. |
| `MAX_BLOB_SIZE_BYTES` | `524288000` | Max upload size (500 MB) |
| `API_HOST` | `0.0.0.0` | REST API bind address |
| `API_PORT` | `8000` | REST API port |
| `MCP_HOST` | `0.0.0.0` | MCP server bind address |
| `MCP_PORT` | `8001` | MCP server port |
| `MCP_API_BASE_URL` | `http://127.0.0.1:8000` | REST API URL used internally by the MCP server |
| `SWEEP_INTERVAL_SECONDS` | `3600` | How often the TTL cleanup job runs |

> CLI flags (`--host`, `--port`) take precedence over config/env for the respective server.

## REST API Reference

| Method | Path | Description |
|---|---|---|
| `GET` | `/health` | Liveness check |
| `POST` | `/blobs` | Store a blob |
| `GET` | `/blobs/{uuid}` | Download a blob |
| `GET` | `/blobs/{uuid}/meta` | Metadata only (no payload) |
| `DELETE` | `/blobs/{uuid}` | Delete a blob |

### Store a blob

Pass metadata as custom request headers:

| Header | Default | Description |
|---|---|---|
| `X-Mime-Type` | `application/octet-stream` | MIME type of the payload |
| `X-Ttl-Seconds` | config default | Override TTL; `0` = never expires |
| `X-Origin` | — | Free-form source identifier |
| `X-Tags` | — | JSON object for arbitrary labels |

```bash
curl -X POST http://localhost:8000/blobs \
  -H "Content-Type: application/octet-stream" \
  -H "X-Mime-Type: application/json" \
  -H "X-Origin: my-mcp-server" \
  --data-binary '{"key": "value"}'
```

Response:
```json
{
  "uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "expires_at": "2026-05-30T16:42:07Z",
  "size_bytes": 16
}
```

## MCP Integration

The MCP server uses the **Streamable HTTP** transport. Configure your MCP host to connect over HTTP rather than spawning a subprocess:

```json
{
  "mcpServers": {
    "blob-storage": {
      "type": "http",
      "url": "http://127.0.0.1:8001/mcp"
    }
  }
}
```

### Available tools

| Tool | Input | Output |
|---|---|---|
| `store_blob` | `data_b64`, `mime_type`, `ttl_seconds?`, `origin?`, `tags?` | `{ uuid, expires_at, size_bytes }` |
| `retrieve_blob` | `uuid` | `{ uuid, mime_type, data_b64 }` |
| `get_blob_meta` | `uuid` | metadata JSON |
| `delete_blob` | `uuid` | `{ deleted: uuid }` |

All binary data is **base64-encoded** in MCP tool calls (MCP messages are JSON/text).

## Running Tests

```bash
uv run pytest tests/ -v
```

## Extending the Storage Backend

To use S3 or MinIO in production, implement the `StorageBackend` abstract class in `storage.py`:

```python
from blob_storage.storage import StorageBackend

class S3Backend(StorageBackend):
    async def store(self, blob_uuid: str, data: bytes) -> None: ...
    async def retrieve(self, blob_uuid: str) -> bytes: ...
    async def delete(self, blob_uuid: str) -> None: ...
```

Then inject it into `BlobService` and the `_backend` singleton in `api.py`.