Skip to main content
Glama
bobbylite

service-template

by bobbylite
README.md
# service-template

A minimal, reusable Python backend template: one FastAPI app exposing the same
business logic through **two front doors** — a REST endpoint and an MCP tool.

Clone it, rename it, and build on the seams it leaves you.

## The pattern

Service-layer architecture with a light ports-and-adapters seam. The HTTP route
and the MCP tool are thin adapters; neither owns logic, and neither knows about
the other.

```
HTTP request  ──▶ api/health.py  ──┐
                                   ├──▶ services/health_service.py  (single source of truth)
MCP tool call ──▶ mcp/tools.py  ───┘
```

Adding auth, identity, or persistence later touches the service and identity
layers — not the adapters.

## Layout

```
src/app/
├── main.py                # app factory, lifespan, mounts the MCP sub-app
├── api/health.py          # adapter #1 — HTTP
├── mcp/tools.py           # adapter #2 — MCP
├── services/
│   └── health_service.py  # shared business logic
├── core/
│   ├── config.py          # pydantic-settings
│   └── logging.py         # structured JSON logging
└── identity/provider.py   # extension seam — SPIFFE/SPIRE goes here later
```

## Quickstart

```bash
uv sync                                  # create .venv and install everything
uv run uvicorn app.main:app --reload     # serve on http://127.0.0.1:8000
```

| Surface | Address |
| --- | --- |
| Health endpoint | `GET http://127.0.0.1:8000/health` |
| OpenAPI docs | `http://127.0.0.1:8000/docs` |
| MCP endpoint | `http://127.0.0.1:8000/mcp/` (streamable HTTP) |

```bash
curl http://127.0.0.1:8000/health
# {"status":"ok","uptime_seconds":3.14,"version":"0.1.0"}
```

## Development

```bash
uv run pytest            # tests
uv run ruff check .      # lint
uv run ruff format .     # format
uv run mypy              # type check (strict)
```

## Configuration

Settings live in `src/app/core/config.py` and are read from the environment or a
`.env` file using the `APP_` prefix. Copy `.env.example` to `.env` to start.

| Setting | Env var | Default |
| --- | --- | --- |
| `app_name` | `APP_APP_NAME` | `service-template` |
| `version` | `APP_VERSION` | `0.1.0` |
| `log_level` | `APP_LOG_LEVEL` | `INFO` |

## Extending it

**A new MCP tool** — add a function to `src/app/mcp/tools.py` (or a new module
under `src/app/mcp/` registered on the same `mcp` instance). Put the logic in
`services/`, and call it from the tool.

**A new HTTP route** — add a router under `src/app/api/` and include it in
`create_app()`. Same rule: the logic belongs in `services/`.

**Workload identity (SPIFFE/SPIRE)** — `src/app/identity/provider.py` is a no-op
stub today. Replace `NullIdentityProvider` with an implementation backed by the
`spiffe` package and have callers depend on `get_identity_provider()`. Nothing in
`api/` or `services/` should need to change.

**Renaming for a new project** — rename the `src/app/` package, update `name` in
`pyproject.toml`, the `packages` entry under `[tool.hatch.build.targets.wheel]`,
and the `app_name` default in `core/config.py`. Nothing in the logic references
this template by name.