Production MCP Server
by nhannt95
README.md
# Production MCP Server
> π»π³ **HΖ°α»ng dαΊ«n tiαΊΏng Viα»t:** [HUONG_DAN.md](HUONG_DAN.md) Β· ThΓͺm tool: [THEM_TOOL.md](THEM_TOOL.md) Β· Prompts: [THEM_PROMPT.md](THEM_PROMPT.md) Β· Nhiα»u client (HTTP/SSE): [CHAY_NHIEU_CLIENT.md](CHAY_NHIEU_CLIENT.md) Β· Agent LangChain/CrewAI: [KET_NOI_AGENT.md](KET_NOI_AGENT.md) Β· CΖ‘ sα» dα»― liα»u: [KET_NOI_DATABASE.md](KET_NOI_DATABASE.md)
A production-ready, modular **Model Context Protocol (MCP)** server built with
**FastMCP**. It ships **40+ tools** across 8 categories and is designed to scale
to **300+ tools without modifying any existing code** β you just drop a new file
into `tools/<category>/` and it registers itself on startup.
## Highlights
- **Clean architecture & SOLID** β tools stay thin; logic lives in `services/`; cross-cutting concerns live in `core/`.
- **Dynamic auto-loading** β the loader scans `tools/**` (and `prompts/**`) and calls each module's `register(mcp)`. No manual imports, ever.
- **Prompts too** β 12 reusable MCP prompts (SQL Q&A, code review, summarize, system triageβ¦) auto-loaded the same way. See [THEM_PROMPT.md](THEM_PROMPT.md).
- **Standard tool template** β every tool has a description, input validation, error handling, logging, type hints and a docstring.
- **Uniform responses** β every tool returns `{ success, data, error }`.
- **Security by default** β filesystem sandbox, SSRF guard on HTTP, secret redaction, write-guarded SQL.
- **Async everywhere** β `httpx`, `aiofiles`, and threads for blocking libs (`sqlite3`, `openpyxl`).
- **Typed config** via `.env` (pydantic-settings) and structured logging.
- **Local AI** β AI tools call your **local model over an OpenAI-compatible API (vLLM)**, with an offline heuristic fallback so they work with zero config.
---
## Project structure
```
mcp-server/
βββ main.py # Entrypoint: configures logging, runs the transport
βββ server.py # Builds the FastMCP instance + runs the loader
βββ config.py # Typed settings from .env (pydantic-settings)
βββ requirements.txt
βββ pytest.ini
βββ .env.example
β
βββ core/ # Reusable infrastructure
β βββ logger.py # Logging (stderr-safe for stdio), JSON option
β βββ exceptions.py # Domain exception hierarchy
β βββ decorators.py # @tool_handler: logging + timing + error mapping
β βββ response.py # make_response() / make_error() envelopes
β βββ utils.py # validate_path/validate_url/safe_read/safe_write
β βββ loader.py # Dynamic tool discovery & registration
β
βββ services/ # Stateful / IO logic (reused by tools)
β βββ http_service.py # Shared httpx client + SSRF policy
β βββ database_service.py# Pluggable DB drivers (SQLite now; PG/MySQL later)
β βββ ai_service.py # OpenAI-compatible (vLLM) chat client
β βββ text_heuristics.py # Offline fallback for AI tools
β
βββ models/ # Pydantic models (typed tool outputs)
β βββ common.py
β
βββ tools/ # One folder per category, one file per tool
β βββ system/ # get_system_info, get_os, get_hostname, get_cpu,
β β # get_memory, get_disk, get_env, current_time
β βββ filesystem/ # read/write/append/delete/move/copy/rename files,
β β # create/delete/list directory, search, file_info
β βββ api/ # http_get, http_post, http_put, http_delete
β βββ network/ # dns_lookup (extend freely)
β βββ database/ # execute_query, list_tables, describe_table
β βββ utilities/ # base64, uuid, md5, sha256, json_format, yaml<->json
β βββ ai/ # count_tokens, summarize_text, extract_keywords
β βββ office/ # csv_reader/writer, excel_reader/writer
β
βββ tests/ # pytest suite (loader, core, ~2 tools/category)
βββ examples/ # Example client + Claude/VSCode/Cursor/Windsurf configs
```
---
## Quick start
```bash
# 1. Create and activate a virtual environment (Python 3.12+; 3.11 also works)
python -m venv .venv
.\.venv\Scripts\activate # Windows PowerShell
# source .venv/bin/activate # macOS / Linux
# 2. Install dependencies
pip install -r requirements.txt
# 3. Configure
copy .env.example .env # Windows (cp on macOS/Linux)
# edit .env β at minimum set MCP_ALLOWED_ROOT to your working folder
# 4. Run the server (stdio transport by default)
python main.py
# 5. (optional) Run the tests
pytest
```
To run over HTTP instead of stdio, set `MCP_TRANSPORT=http` (and `MCP_HOST` /
`MCP_PORT`) in `.env`, then `python main.py`.
---
## Trying it out with the example client
The in-memory client needs no running server β it imports the server object
directly:
```bash
python examples/mcp_client.py # in-memory (fastest)
python examples/mcp_client.py --stdio # spawns `python main.py` over stdio
```
---
## Connecting from an IDE / desktop client
Ready-to-edit configs live in `examples/`. Replace the absolute paths with your
own (point `command` at your venv's Python and `args` at `main.py`).
| Client | File to edit | Example |
|-----------------|------------------------------------------------------------------|--------------------------------------|
| Claude Desktop | `%APPDATA%\Claude\claude_desktop_config.json` (Win) / `~/Library/Application Support/Claude/β¦` (mac) | `examples/claude_desktop_config.json` |
| VSCode | `.vscode/mcp.json` | `examples/vscode_mcp.json` |
| Cursor | `.cursor/mcp.json` or `~/.cursor/mcp.json` | `examples/cursor_mcp.json` |
| Windsurf | `~/.codeium/windsurf/mcp_config.json` | `examples/windsurf_mcp.json` |
After editing, restart the client. The server's tools appear in the client's
tool list.
---
## Configuration reference (`.env`)
All variables are prefixed `MCP_`. See `.env.example` for the full list. Key ones:
| Variable | Default | Purpose |
|------------------------------|-----------------------------|----------------------------------------------------|
| `MCP_TRANSPORT` | `stdio` | `stdio` \| `http` \| `sse` |
| `MCP_LOG_LEVEL` | `INFO` | Logging verbosity |
| `MCP_ALLOWED_ROOT` | current working dir | **Filesystem sandbox root** β tools cannot escape it |
| `MCP_HTTP_ALLOW_PRIVATE_HOSTS` | `false` | Keep `false` to block SSRF to private/loopback IPs |
| `MCP_DATABASE_URL` | `sqlite:///./data/app.db` | DB connection (SQLite today) |
| `MCP_DB_ALLOW_WRITE` | `true` | Allow write/DDL SQL statements |
| `MCP_OPENAI_BASE_URL` | *(unset)* | vLLM OpenAI-compatible URL, e.g. `http://localhost:8001/v1` |
| `MCP_OPENAI_MODEL` | *(unset)* | Model name served by vLLM |
---
## Tool categories (40+ tools)
Every tool returns the same envelope:
```json
{ "success": true, "data": { ... }, "error": null }
```
...and on failure:
```json
{ "success": false, "data": null, "error": { "code": "validation_error", "message": "...", "details": {} } }
```
- **System** β `get_system_info`, `get_os`, `get_hostname`, `get_cpu`, `get_memory`, `get_disk`, `get_env`, `current_time`
- **File System** *(sandboxed)* β `read_file`, `write_file`, `append_file`, `delete_file`, `move_file`, `copy_file`, `rename_file`, `create_directory`, `delete_directory`, `list_directory`, `search_files`, `file_info`
- **API** *(SSRF-guarded, httpx)* β `http_get`, `http_post`, `http_put`, `http_delete`
- **Network** β `dns_lookup`
- **Database** *(SQLite; PG/MySQL-ready)* β `execute_query`, `list_tables`, `describe_table`
- **Utilities** β `base64_encode`, `base64_decode`, `uuid`, `md5`, `sha256`, `json_format`, `yaml_to_json`, `json_to_yaml`
- **AI** *(local vLLM or heuristic fallback)* β `count_tokens`, `summarize_text`, `extract_keywords`
- **Office** β `csv_reader`, `csv_writer`, `excel_reader`, `excel_writer`
---
## Using the local AI (vLLM) tools
The AI tools call a **local model through an OpenAI-compatible API**. Start vLLM:
```bash
python -m vllm.entrypoints.openai.api_server \
--model Qwen/Qwen2.5-7B-Instruct \
--port 8001
```
Then set in `.env`:
```env
MCP_OPENAI_BASE_URL=http://localhost:8001/v1
MCP_OPENAI_API_KEY=EMPTY
MCP_OPENAI_MODEL=Qwen/Qwen2.5-7B-Instruct
```
- With these set, `summarize_text` and `extract_keywords` use the LLM (`"backend": "llm"`).
- **Without** them, they fall back to a fast local heuristic (`"backend": "heuristic"`) β so the tools work out of the box with no model running.
- `count_tokens` always runs locally via `tiktoken`.
---
## How the dynamic loader works
On startup, [`core/loader.py`](core/loader.py) walks the `tools` package
recursively, imports every non-private module, and calls its `register(mcp)`
function. A broken tool is logged and skipped β it never takes down the server.
That single convention is what makes the project scale to 300+ tools with **zero
edits to existing files**.
---
## Add a new tool in under a minute
See [`ADD_A_TOOL.md`](ADD_A_TOOL.md) for the copy-paste template. In short:
1. Create `tools/<category>/my_tool.py`.
2. Paste the template, rename the function, write your logic.
3. Restart the server β done. It auto-registers.
```python
# tools/utilities/reverse_text.py
from __future__ import annotations
from typing import TYPE_CHECKING
from core.decorators import tool_handler
if TYPE_CHECKING:
from fastmcp import FastMCP
def register(mcp: "FastMCP") -> None:
@mcp.tool()
@tool_handler
async def reverse_text(text: str) -> dict:
"""Reverse a string."""
return {"reversed": text[::-1]}
```
No imports to update, no registry to edit. That's the whole workflow.
---
## Extending the database to PostgreSQL / MySQL
The database layer is built for this. In
[`services/database_service.py`](services/database_service.py):
1. Implement a class satisfying the `DatabaseDriver` protocol (`execute`,
`list_tables`, `describe_table`).
2. Register it in the `_DRIVERS` dict by URL scheme.
3. Point `MCP_DATABASE_URL` at the new backend.
The three database **tools do not change** β they depend on the abstract driver.
---
## Docker
The image defaults to the **HTTP transport** (stdio can't work in a container)
and exposes port `8000`.
```bash
# Compose (recommended) β build, run, persist data in a volume
docker compose up --build
# Or plain Docker
docker build -t production-mcp-server:latest .
docker run -d --name mcp-server -p 8000:8000 -v mcp-data:/data \
production-mcp-server:latest
```
The server is then reachable at `http://localhost:8000/mcp`. It runs as a
non-root user, and SQLite + the filesystem-tool workspace live under `/data`
(mounted as a volume). To wire in a local vLLM model, uncomment the `vllm`
service in `docker-compose.yml` and set the `MCP_OPENAI_*` vars.
---
## Testing
```bash
pytest # runs the full suite
pytest -v # verbose
```
The suite covers the dynamic loader, core helpers (response envelope, path/URL
validation, the `@tool_handler` decorator), and 1β2 representative tools per
category exercised end-to-end.
---
## Security notes
- **Filesystem** tools resolve every path and reject anything outside `MCP_ALLOWED_ROOT` (blocks `../` traversal).
- **HTTP** tools reject non-http(s) schemes and, by default, any host resolving to a private/loopback/link-local address (SSRF guard). Toggle with `MCP_HTTP_ALLOW_PRIVATE_HOSTS`.
- **`get_env`** redacts values whose names look secret (`*key*`, `*token*`, `*password*`, β¦).
- **SQL** write/DDL statements are blocked unless `MCP_DB_ALLOW_WRITE=true`; use parameterised queries (`?`).
- Logs go to **stderr** so they never corrupt the stdio JSON-RPC stream.
---
## License
MIT (or your organisation's preferred license).
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues