Content
# DevOps MCP Server
A production-ready **Model Context Protocol (MCP)** server that serves DevOps knowledge base — skills, instructions,
prompts, and policies for Python, Ansible, Terraform, Docker, Kubernetes,
CI/CD, and general engineering — to any MCP-compatible AI client (Claude
Desktop, VS Code, Cursor, ...).
---
## 1. Overview
The server reads versioned Markdown files (with YAML frontmatter) from
`content/` and exposes them to AI assistants via 4 MCP tools over a
**Streamable HTTP** transport, deployed as a Docker container.
Why MCP? It gives AI assistants a controlled, auditable, read-only window
into your DevOps standards so generated code, pipelines, and infra always
align with internal policy — without copy-pasting documentation into prompts.
---
## 2. Architecture
```
┌─────────────────────┐ HTTPS / X-API-Key ┌─────────────────────────────┐
│ MCP Client │ ───────────────────────────▶ │ Starlette ASGI app │
│ (Claude / VS Code) │ │ ├─ /health │
└─────────────────────┘ │ └─ /mcp (FastMCP mount) │
│ APIKeyMiddleware │
└──────────────┬───────────────┘
│
▼
┌─────────────────────────────┐
│ mcp_server.tools (FastMCP) │
│ @tool fetch_devops_content│
│ @tool list_devops_* │
│ @tool get_devops_*_by_id │
└──────────────┬───────────────┘
│
▼
┌─────────────────────────────┐
│ src/services │
│ └─ ContentService │
│ ├─ ContentLoader (fs) │
│ └─ ContentFilter │
└──────────────┬───────────────┘
│
▼
┌────────────────────────┐
│ content/*.md │
│ (frontmatter + body) │
└────────────────────────┘
```
---
## 3. Prerequisites
- Python **3.11+**
- Poetry **1.7+** (or pip)
- Docker Engine **24+** and Docker Compose v2 (for containerized deployment)
- An API key value of your choosing (any non-empty string) for `MCP_API_KEY`
---
## 4. Local Setup
```bash
# 1. Clone
git clone <repo-url> hbiz-devops-mcp
cd hbiz-devops-mcp
# 2. Environment
cp .env.example .env
# Edit .env — at minimum set MCP_API_KEY to a non-empty secret
# 3. Install dependencies
poetry install
# OR
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
# 4. Run
poetry run python main.py
# OR
python main.py
```
The server listens on `http://localhost:8000` by default.
- Health: `GET http://localhost:8000/health`
- MCP: `POST http://localhost:8000/mcp` (with `X-API-Key: <your-key>`)
---
## 5. Docker
```bash
cp .env.example .env
# Edit .env — set MCP_API_KEY and adjust MCP_PORT as needed
# Build the image
docker build -t hbiz-devops-mcp:latest .
# Start the container
docker compose up -d
# Verify
curl http://localhost:8001/health
# {"status":"healthy","server":"hBiz-DevOps MCP Server","version":"1.0.0"}
# View live logs
docker logs hbiz-devops-mcp -f
```
The server is exposed on the port defined by `MCP_PORT` (default `8001`); the MCP endpoint is
`http://localhost:8001/mcp`.
Stop:
```bash
docker compose down
```
---
## 6. VS Code MCP Configuration
Create `.vscode/mcp.json` in any consumer workspace (this file is committed
on purpose — it contains no secrets, only the server URL):
```json
{
"servers": {
"hbiz-devops": {
"type": "http",
"url": "http://localhost:8000/mcp",
"headers": {
"X-API-Key": "${env:HBIZ_MCP_API_KEY}"
}
}
}
}
```
Then export the key in your shell (or VS Code user env):
```bash
export HBIZ_MCP_API_KEY="<your-key>"
```
For a production deployment, replace `http://localhost:8000/mcp` with
`https://<your-host>/mcp`.
---
## 7. Environment Variables
### Server
| Variable | Default | Description |
|--------------------|--------------------------|--------------------------------------------------|
| `DOCKER_IMAGE` | `hbiz-devops-mcp:latest` | Docker image name used in `docker-compose.yml` |
| `ENVIRONMENT` | `local` | `local` / `staging` / `production` |
| `MCP_API_KEY` | *(required)* | API key clients must send as `X-API-Key` |
| `MCP_SERVER_NAME` | `hBiz-DevOps MCP Server` | Display name surfaced to MCP clients |
| `MCP_HOST` | `0.0.0.0` | Bind address |
| `MCP_PORT` | `8000` | Bind port (exposed on host) |
| `CONTENT_DIR` | `content` | Path (relative or absolute) to content root |
### Logging
| Variable | Default | Description |
|-------------------------|------------------------------------------------------|------------------------------------------------------|
| `LOG_LEVEL` | `INFO` | `DEBUG` / `INFO` / `WARNING` / `ERROR` |
| `LOG_FORMAT` | `%(asctime)s - %(name)s - %(levelname)s - %(message)s` | Python `logging` format string |
| `LOG_FILE_ENABLED` | `false` | Set `true` to also write logs to a file |
| `LOG_FILE_PATH` | `logs/mcp_logs.log` | Path to the log file (created automatically) |
| `LOG_FILE_MAX_BYTES` | `10485760` | Max file size before rotation (default 10 MB) |
| `LOG_FILE_BACKUP_COUNT` | `5` | Number of rotated backup files to keep |
All variables can be overridden via `.env` or process environment.
---
## 8. MCP Tools
| Tool | Args | Returns |
|---------------------------------|-------------------------------------------------------|----------------------------------------|
| `fetch_devops_content` | `query: str, content_type?: str, tags?: list[str]` | Matching items rendered as markdown |
| `list_devops_categories` | *(none)* | Sorted list of categories |
| `list_devops_content` | `category?: str, content_type?: str` | Markdown table of metadata |
| `get_devops_content_by_id` | `content_id: str` | Single item rendered as markdown |
All tools are read-only (`readOnlyHint: true`, `destructiveHint: false`).
---
## 9. Adding New Content
1. Create a Markdown file under `content/<category>/<type>/<id>.md` where:
- `<category>` ∈ `python | ansible | terraform | docker | kubernetes | cicd | general`
- `<type>` ∈ `skills | instructions | prompts | policies | rules`
2. Add the required frontmatter:
```markdown
---
id: kebab-case-unique-id
title: Human-Readable Title
type: skill # skill | instruction | prompt | policy | rule
category: python # must match the folder name
tags:
- python # first tag should be the category
- clean-arch
version: "1.0" # quoted string
---
# Title
Body markdown here...
```
3. Restart the server (content is loaded once at startup).
`id` values must be **globally unique** across the whole tree — duplicates
will fail loading with `ContentLoadError`.
---
## 10. Project Structure
```
hbiz-devops-mcp/
├── main.py # Uvicorn entry point
├── pyproject.toml # Poetry deps
├── requirements.txt # pip-installable mirror
├── Dockerfile
├── .dockerignore
├── docker-compose.yml
├── .env.example # Template — copy to .env
├── config/
│ └── settings.py # Pydantic settings
├── src/
│ ├── core/
│ │ ├── content_loader.py # Filesystem → ContentItem[]
│ │ ├── content_filter.py # Pure filtering logic
│ │ └── exceptions.py
│ ├── models/
│ │ └── content.py # ContentMeta, ContentItem, ContentType
│ ├── services/
│ │ └── content_service.py # Caches + exposes content
│ └── utils/
│ ├── auth.py # APIKeyMiddleware
│ └── logger.py # stdout + optional rotating file handler
├── mcp_server/
│ ├── app.py # Starlette + FastMCP wiring
│ └── tools.py # @mcp.tool definitions
├── content/ # The DevOps knowledge base
│ ├── python/ ansible/ terraform/ docker/ kubernetes/ cicd/ general/
├── logs/ # Created at runtime (gitignored)
│ └── mcp_logs.log # Only present when LOG_FILE_ENABLED=true
└── tests/
├── conftest.py
├── core/
└── services/
```
---
## 11. Dependency Management
- **Source of truth:** `pyproject.toml` (Poetry).
- `requirements.txt` is regenerated for environments without Poetry:
```bash
poetry export -f requirements.txt -o requirements.txt --without-hashes
```
- Pin major + minor (`^1.0`). Update via `poetry update` and run the full
test suite before committing the new lockfile.
- Security: `poetry run pip-audit` (or `safety check`) on every PR.
---
## 12. Logging
By default logs are written to **stdout only** and are visible via:
```bash
# Local run
python main.py
# Docker
docker logs hbiz-devops-mcp
docker logs hbiz-devops-mcp -f # follow/live
```
### Enable file logging
Set the following in `.env` and restart the server:
```env
LOG_FILE_ENABLED=true
LOG_FILE_PATH=logs/mcp_logs.log
LOG_FILE_MAX_BYTES=10485760 # 10 MB per file
LOG_FILE_BACKUP_COUNT=5 # keep last 5 rotated files
```
The `logs/` directory is created automatically. Files rotate once they reach
`LOG_FILE_MAX_BYTES` and up to `LOG_FILE_BACKUP_COUNT` backups are retained
(`mcp_logs.log`, `mcp_logs.log.1`, … `mcp_logs.log.5`). Both stdout and file
logging are active simultaneously when file logging is enabled.
> `logs/` is gitignored — log files are never committed to source control.
---
## 13. Testing with Postman
The MCP server speaks **JSON-RPC 2.0 over HTTP POST**. Every request goes to
`http://localhost:8001/mcp` with the `X-API-Key` header set.
### Required headers (all requests)
| Header | Value |
|--------|-------|
| `Content-Type` | `application/json` |
| `Accept` | `application/json, text/event-stream` |
| `X-API-Key` | your `MCP_API_KEY` value |
---
### Step 1 — Verify the server is up
**GET** `http://localhost:8001/health`
Expected response:
```json
{"status": "healthy", "server": "hBiz-DevOps MCP Server", "version": "1.0.0"}
```
---
### Step 2 — Initialize the MCP session
**POST** `http://localhost:8001/mcp`
Body:
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "postman", "version": "1.0" }
}
}
```
> Copy the `mcp-session-id` value from the **response headers** — you must
> include it as `mcp-session-id: <value>` in all subsequent requests.
---
### Step 3 — List available tools
**POST** `http://localhost:8001/mcp`
Add header: `mcp-session-id: <value from step 2>`
Body:
```json
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}
```
---
### Step 4 — Call a tool
#### List all categories
```json
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "list_devops_categories",
"arguments": {}
}
}
```
#### Search content
```json
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "fetch_devops_content",
"arguments": {
"query": "docker",
"content_type": "skill"
}
}
}
```
#### List content by category
```json
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "list_devops_content",
"arguments": {
"category": "python"
}
}
}
```
#### Fetch a specific item by ID
```json
{
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {
"name": "get_devops_content_by_id",
"arguments": {
"content_id": "python-clean-architecture"
}
}
}
```
---
### Step 5 — Verify auth rejection
Send any request **without** the `X-API-Key` header (or with a wrong value).
Expected response: **HTTP 401**
```json
{"detail": "Unauthorized"}
```
---
## 14. Unit Tests
```bash
# Run all tests
poetry run pytest -v
# With coverage
poetry run pytest --cov=src --cov-report=term-missing
# A single file
poetry run pytest tests/core/test_content_loader.py -v
```
Tests use sample markdown fixtures created in `tmp_path` — they do not
read the production `content/` tree.
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available