Enterprise Infrastructure & Metrics MCP Server
# Enterprise Infrastructure & Metrics MCP Server
A production-grade **Model Context Protocol (MCP)** server, written in
native async Python, that gives an LLM agent (Claude Desktop, Claude Code,
or any MCP-compatible host) safe, read-only introspection into:
- **Host system resources** — live CPU (per-core), memory, and disk metrics via `psutil`
- **Docker containers** — list, inspect health/state, and tail logs via the Docker SDK
- **Application/system logs** — sandboxed tail-and-filter of log files, with strict path-traversal protection
Built as a portfolio project demonstrating enterprise MCP server engineering: strict Pydantic v2 contracts, structured (never-raise) error handling, async-safe wrapping of blocking I/O, and an explicit, auditable security boundary.
---
## Architecture
```
┌──────────────────────────┐ stdio (JSON-RPC 2.0) ┌───────────────────────────────────────────┐
│ MCP Host │ <---------------------------------> │ enterprise-mcp-server (this project) │
│ (Claude Desktop / │ subprocess, stdin/stdout │ │
│ Claude Code / other) │ │ ┌─────────────────────────────────────┐ │
└──────────────────────────┘ │ │ server.py (FastMCP app) │ │
│ │ - registers 3 tools │ │
│ │ - stdio transport │ │
│ │ - logging -> stderr ONLY │ │
│ └───────────────┬─────────────────────┘ │
│ │ validated Pydantic │
│ ▼ input models │
│ ┌─────────────────────────────────────┐ │
│ │ tools/ │ │
│ │ ├─ system_metrics.py (psutil) │ │
│ │ ├─ docker_manager.py (docker SDK) │ │
│ │ └─ log_analyzer.py (sandboxed FS)│ │
│ └───────────────┬─────────────────────┘ │
│ │ asyncio.to_thread │
│ │ (never blocks loop) │
│ ┌────────────────▼─────────────────────┐│
│ │ utils/ ││
│ │ ├─ security.py (path sanitization) ││
│ │ ├─ errors.py (structured JSON) ││
│ │ └─ retry.py (jittered backoff) ││
│ └────────────────────────────────────────│
└───────────────┬───────────────┬───────────┘
│ │
┌───────────────▼───┐ ┌────────▼──────────┐
│ Host OS │ │ Docker daemon │
│ /proc, psutil │ │ /var/run/ │
│ sandboxed log dir │ │ docker.sock │
└────────────────────┘ └────────────────────┘
```
**Request lifecycle**: MCP host → JSON-RPC `tools/call` over stdin → FastMCP
parses & validates arguments against the tool's Pydantic input model →
tool function executes, wrapping every blocking call (`psutil`, Docker SDK,
file I/O) in `asyncio.to_thread` → result serialized to JSON (success
payload **or** structured `ToolError` — the function never raises past this
boundary) → written to stdout as the JSON-RPC response.
---
## The three tools
| Tool | Purpose | Mutates host state? |
|---|---|---|
| `get_system_metrics` | CPU (aggregate + per-core), memory (RAM + swap), disk (usage + I/O counters) | No |
| `manage_docker_containers` | List containers, inspect health/config, tail container logs | No — read-only by design |
| `analyze_local_logs` | Tail + filter a log file inside a sandboxed root directory | No — read-only, sandboxed |
---
## Security boundaries
This project treats the LLM as an **untrusted caller** operating a
**read-only monitoring surface**, not an operator with host control. Three
concrete boundaries enforce that:
1. **`manage_docker_containers` exposes no lifecycle verbs.** The Docker
SDK and daemon support starting, stopping, restarting, executing
commands in, and removing containers. None of that is wired up. Only
`list_containers`, `inspect_container`, and `get_container_logs` exist
as actions — an LLM cannot use this server to take down a container or
run arbitrary commands inside one, even if prompted to.
2. **`analyze_local_logs` is sandboxed to a single, explicit root
directory** (`MCP_LOG_ROOT_DIR`, default `/var/log`), enforced in
[`utils/security.py`](src/enterprise_mcp_server/utils/security.py).
Every requested path is:
- stripped of leading `/` and `..` segments (blocks absolute-path override),
- joined onto the resolved root,
- resolved again with `Path.resolve()` (collapses remaining `..`
segments **and** follows symlinks, closing the symlink-escape vector),
- and finally checked with `Path.is_relative_to()` against the resolved
root before any file is opened.
A request for `../../etc/shadow`, `/etc/shadow`, or a symlink inside the
sandbox that points outside it is rejected with a structured
`PATH_TRAVERSAL_BLOCKED` error — never a Python traceback, and never a
silent read.
3. **Secrets are never echoed back.** `inspect_container` returns
`env_var_count` (an integer), not the environment variables themselves,
since container env vars routinely contain credentials and API keys.
4. **Every response size is bounded.** `MCP_MAX_LOG_LINES` hard-caps log
tails server-side regardless of what a caller requests, and Docker log
tails are capped at 500 lines — both protect the LLM's context window
and prevent a single tool call from returning gigabytes of data.
---
## Reliability & engineering standards
- **Never-raise tool boundary**: every tool function wraps its entire body
in `try/except` and returns a structured JSON `ToolError`
(`utils/errors.py`) on failure — malformed input, a missing container, a
down Docker daemon, or a permissions error all produce a well-formed,
LLM-parseable payload instead of crashing the server process.
- **Async-safe by construction**: `psutil`, the `docker` SDK, and file I/O
are all synchronous/blocking under the hood. Every call site wraps them
in `asyncio.to_thread` so a slow disk read or a stalled Docker socket
cannot stall the event loop and starve other concurrent tool calls.
- **Jittered exponential backoff** (`utils/retry.py`) around Docker daemon
calls, since a momentarily busy socket is a transient condition worth
retrying — capped at `MCP_TOOL_RETRY_ATTEMPTS` attempts.
- **Strict Pydantic v2 contracts** (`schemas.py`) for every tool's input
*and* output. FastMCP derives the JSON Schema exposed to the LLM host
directly from the input models, so the tool's documented contract and
its runtime validation can never drift apart.
- **stdout is sacred**: the stdio transport uses stdout exclusively for
JSON-RPC frames. All logging is configured to write to **stderr**
(`server.py`) — a stray `print()` or misconfigured logger on stdout would
silently corrupt the protocol stream for every connected host.
---
## Project layout
```
enterprise-mcp-server/
├── pyproject.toml
├── README.md
├── .env.example
├── src/
│ └── enterprise_mcp_server/
│ ├── __init__.py
│ ├── server.py # FastMCP app, tool registration, stdio entrypoint
│ ├── config.py # Env-driven settings + security boundary (log root)
│ ├── schemas.py # Pydantic v2 input/output contracts for all tools
│ ├── tools/
│ │ ├── system_metrics.py
│ │ ├── docker_manager.py
│ │ └── log_analyzer.py
│ └── utils/
│ ├── security.py # Path-traversal sanitization
│ ├── errors.py # Structured ToolError contract
│ └── retry.py # Jittered exponential backoff
└── tests/
├── test_security.py
└── test_system_metrics.py
```
---
## Setup
### Prerequisites
- Python 3.11+
- Docker (optional — only required for `manage_docker_containers`; the
other two tools work without it)
### Install
```bash
git clone https://github.com/<your-username>/enterprise-mcp-server.git
cd enterprise-mcp-server
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
```
### Configure (optional)
Copy `.env.example` to `.env` and adjust as needed, or export directly:
```bash
export MCP_LOG_ROOT_DIR=/var/log # sandbox root for analyze_local_logs
export MCP_MAX_LOG_LINES=1000 # hard ceiling on lines returned
export MCP_DOCKER_TIMEOUT_SECONDS=10 # Docker daemon socket timeout
export MCP_TOOL_RETRY_ATTEMPTS=3 # retry attempts for transient failures
```
### Run standalone (for smoke-testing)
```bash
enterprise-mcp-server
# or
python -m enterprise_mcp_server.server
```
The process will sit waiting for JSON-RPC frames on stdin — this is
expected; it's designed to be launched by an MCP host, not run
interactively. Use the MCP Inspector (below) for interactive testing.
### Test with MCP Inspector
```bash
npx @modelcontextprotocol/inspector enterprise-mcp-server
```
This opens a browser UI where you can call each tool directly and inspect
the JSON Schema FastMCP generated from `schemas.py`.
### Run the test suite
```bash
pytest -v
```
---
## Claude Desktop configuration
Add the following to your Claude Desktop MCP config file
(`~/Library/Application Support/Claude/claude_desktop_config.json` on
macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows):
```json
{
"mcpServers": {
"enterprise-infra-metrics": {
"command": "/absolute/path/to/enterprise-mcp-server/.venv/bin/enterprise-mcp-server",
"args": [],
"env": {
"MCP_LOG_ROOT_DIR": "/var/log",
"MCP_MAX_LOG_LINES": "1000",
"MCP_DOCKER_TIMEOUT_SECONDS": "10"
}
}
}
}
```
> **Note**: Claude Desktop launches this as a subprocess with a minimal
> environment, so `command` must be the **absolute path** to the
> virtualenv's console script (not a bare `enterprise-mcp-server`, which
> relies on `PATH` being inherited — it usually isn't).
Restart Claude Desktop, and the hammer icon in the composer should show
`get_system_metrics`, `manage_docker_containers`, and `analyze_local_logs`
as available tools.
---
## Example interactions
> "Is my machine under memory pressure right now?"
→ calls `get_system_metrics(scope="memory")`
> "Are any of my Docker containers unhealthy?"
→ calls `manage_docker_containers(action="list_containers")`, then
`inspect_container` on anything with a non-`healthy` status
> "Check `nginx/access.log` for the last hour's errors"
→ calls `analyze_local_logs(relative_log_path="nginx/access.log", severity_filter="error_and_above")`
---
## License
MIT
TDQS
Scored across 3 tools
Each tool targets a distinct subsystem: system metrics, Docker containers, and log files. There is no overlap in purpose or functionality; an agent can easily select the correct tool based on the resource it needs to access.
All tool names follow a consistent snake_case verb_noun pattern: get_system_metrics, manage_docker_containers, analyze_local_logs. The verbs clearly indicate the action (get, manage, analyze) and the nouns specify the target, making the pattern predictable and readable.
With only 3 tools, the server is tightly scoped to its stated purpose of infrastructure and metrics. Each tool covers a major area (host metrics, container introspection, log analysis) and earns its place without redundancy or bloat.
The tool set covers core infrastructure monitoring needs: system resource metrics, Docker container state and logs, and local log tailing/filtering. Minor gaps exist, such as network metrics or process-level details, but these are not critical for the primary workflows and can be worked around.