Skip to content

Commit 630fd3c

Browse files
feat: Add doctest infrastructure and fix existing doctests (#395)
* feat: Add doctest infrastructure and fix existing doctests - Fix Alembic env.py context execution to prevent import errors - Fix wrapper.py environment validation to prevent module-level exit - Fix JSON-RPC validation doctests with proper exception paths - Add proper header to create_slug.py Part of #249 - Phase 1: Infrastructure setup * Updated everything to use python3 binary instead of python for consistency Signed-off-by: Mihai Criveti <[email protected]> --------- Signed-off-by: Mihai Criveti <[email protected]> Co-authored-by: Mihai Criveti <[email protected]>
1 parent 1a39987 commit 630fd3c

36 files changed

+296
-76
lines changed

.github/tools/normalize_special_characters.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,16 @@
2727
Usage examples::
2828
2929
# See which files would change and view a coloured unified diff
30-
python normalize_characters.py "**/*.py" --dry-run --diff
30+
python3 normalize_characters.py "**/*.py" --dry-run --diff
3131
3232
# Clean the entire project tree, keeping *.bak* backups of changed files
33-
python normalize_characters.py . --backup-ext .bak
33+
python3 normalize_characters.py . --backup-ext .bak
3434
3535
# Normalise Markdown docs verbosely; ignore the vendor directory
36-
python normalize_characters.py "docs/**/*.md" -v -i "vendor/**/*"
36+
python3 normalize_characters.py "docs/**/*.md" -v -i "vendor/**/*"
3737
3838
# Process only Python files in src/ directory
39-
python normalize_characters.py "src/**/*.py" --verbose
39+
python3 normalize_characters.py "src/**/*.py" --verbose
4040
4141
Exit codes:
4242

.github/workflows/lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ jobs:
116116
# -----------------------------------------------------------
117117
- name: 📦 Install project (editable mode)
118118
run: |
119-
python -m pip install --upgrade pip
119+
python3 -m pip install --upgrade pip
120120
pip install -e .[dev]
121121
122122
# -----------------------------------------------------------

.github/workflows/pytest.yml

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ jobs:
6262
# -----------------------------------------------------------
6363
- name: 📦 Install dependencies (editable + dev extra)
6464
run: |
65-
python -m pip install --upgrade pip
65+
python3 -m pip install --upgrade pip
6666
# install the project itself in *editable* mode so tests import the same codebase
6767
# and pull in every dev / test extra declared in pyproject.toml
6868
pip install -e .[dev]
@@ -82,6 +82,30 @@ jobs:
8282
--cov-branch \
8383
--cov-fail-under=40
8484
85+
# -----------------------------------------------------------
86+
# 4️⃣ Run doctests
87+
# -----------------------------------------------------------
88+
- name: 🧪 Run doctests
89+
run: |
90+
pytest --doctest-modules mcpgateway/ --tb=short
91+
92+
# -----------------------------------------------------------
93+
# 5️⃣ Doctest coverage check
94+
# -----------------------------------------------------------
95+
- name: 📊 Doctest coverage validation
96+
run: |
97+
python3 -c "
98+
import subprocess, sys
99+
result = subprocess.run(['python', '-m', 'pytest', '--doctest-modules', 'mcpgateway/', '--tb=no', '-q'], capture_output=True)
100+
if result.returncode == 0:
101+
print('✅ All doctests passing')
102+
else:
103+
print('❌ Doctest failures detected')
104+
print(result.stdout.decode())
105+
print(result.stderr.decode())
106+
sys.exit(1)
107+
"
108+
85109
# -----------------------------------------------------------
86110
# 4️⃣ Upload coverage artifacts (XML + HTML)
87111
# --- keep disabled unless you need them ---
@@ -130,7 +154,7 @@ jobs:
130154
# echo "| File | Stmts | Miss | Branch | BrMiss | Cover |" >> "$GITHUB_STEP_SUMMARY"
131155
# echo "|------|------:|-----:|-------:|-------:|------:|" >> "$GITHUB_STEP_SUMMARY"
132156
# coverage json -q -o cov.json
133-
# python - <<'PY'
157+
# python3 - <<'PY'
134158
# import json, pathlib, sys, os
135159
# data = json.load(open("cov.json"))
136160
# root = pathlib.Path().resolve()

.pre-commit-config.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,3 +510,17 @@ repos:
510510
# - mdformat-gfm
511511
# - mdformat-tables
512512
# - mdformat-black
513+
514+
# -----------------------------------------------------------------------------
515+
# 🧪 DOCTEST VALIDATION
516+
# -----------------------------------------------------------------------------
517+
- repo: local
518+
hooks:
519+
- id: doctest
520+
name: 🧪 Doctest - Validate Documentation Examples
521+
description: Runs doctest on all Python modules to ensure documentation examples work.
522+
entry: python3 -m pytest --doctest-modules mcpgateway/ --tb=short
523+
language: system
524+
pass_filenames: false
525+
always_run: true
526+
types: [python]

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6767
* Expose local stdio MCP servers over SSE endpoints with session management
6868
* Bridge remote SSE endpoints to local stdio for seamless integration
6969
* Built-in keepalive mechanisms and unique session identifiers
70-
* Full CLI support: `python -m mcpgateway.translate --stdio "uvx mcp-server-git" --port 9000`
70+
* Full CLI support: `python3 -m mcpgateway.translate --stdio "uvx mcp-server-git" --port 9000`
7171

7272
* **Tool Annotations & Metadata** - comprehensive tool annotation system:
7373
* New `annotations` JSON column in tools table for storing rich metadata

DEVELOPING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ export MCP_AUTH_TOKEN="<your_bearer_token>"
1212
| Mode | Command | Notes |
1313
| ----------------------------------------------------------- | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
1414
| **SSE (direct)** | `npx @modelcontextprotocol/inspector` | Connects straight to the Gateway's SSE endpoint. |
15-
| **Stdio wrapper** <br/>*(for clients that can't speak SSE)* | `npx @modelcontextprotocol/inspector python -m mcpgateway.wrapper` | Spins up the wrapper **in-process** and points Inspector to its stdio stream. |
16-
| **Stdio wrapper via uv / uvx** | `npx @modelcontextprotocol/inspector uvx python -m mcpgateway.wrapper` | Uses the lightning-fast `uv` virtual-env if installed. |
15+
| **Stdio wrapper** <br/>*(for clients that can't speak SSE)* | `npx @modelcontextprotocol/inspector python3 -m mcpgateway.wrapper` | Spins up the wrapper **in-process** and points Inspector to its stdio stream. |
16+
| **Stdio wrapper via uv / uvx** | `npx @modelcontextprotocol/inspector uvx python3 -m mcpgateway.wrapper` | Uses the lightning-fast `uv` virtual-env if installed. |
1717

1818
🔍 MCP Inspector boots at **[http://localhost:5173](http://localhost:5173)** - open it in a browser and add:
1919

Makefile

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# (An enterprise-ready Model Context Protocol Gateway)
44
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
55
#
6-
# Author: Mihai Criveti
6+
# Authors: Mihai Criveti, Manav Gupta
77
# Description: Build & automation helpers for the MCP Gateway project
88
# Usage: run `make` or `make help` to view available targets
99
#
@@ -182,8 +182,12 @@ clean:
182182
# help: htmlcov - (re)build just the HTML coverage report into docs
183183
# help: test-curl - Smoke-test API endpoints with curl script
184184
# help: pytest-examples - Run README / examples through pytest-examples
185+
# help: doctest - Run doctest on all modules with summary report
186+
# help: doctest-verbose - Run doctest with detailed output (-v flag)
187+
# help: doctest-coverage - Generate coverage report for doctest examples
188+
# help: doctest-check - Check doctest coverage percentage (fail if < 100%)
185189

186-
.PHONY: smoketest test coverage pytest-examples test-curl htmlcov
190+
.PHONY: smoketest test coverage pytest-examples test-curl htmlcov doctest doctest-verbose doctest-coverage doctest-check
187191

188192
## --- Automated checks --------------------------------------------------------
189193
smoketest:
@@ -239,6 +243,43 @@ pytest-examples:
239243
test-curl:
240244
./test_endpoints.sh
241245

246+
## --- Doctest targets ---------------------------------------------------------
247+
doctest:
248+
@echo "🧪 Running doctest on all modules..."
249+
@test -d "$(VENV_DIR)" || $(MAKE) venv
250+
@/bin/bash -c "source $(VENV_DIR)/bin/activate && \
251+
python3 -m pytest --doctest-modules mcpgateway/ --tb=short"
252+
253+
doctest-verbose:
254+
@echo "🧪 Running doctest with verbose output..."
255+
@test -d "$(VENV_DIR)" || $(MAKE) venv
256+
@/bin/bash -c "source $(VENV_DIR)/bin/activate && \
257+
python3 -m pytest --doctest-modules mcpgateway/ -v --tb=short"
258+
259+
doctest-coverage:
260+
@echo "📊 Generating doctest coverage report..."
261+
@test -d "$(VENV_DIR)" || $(MAKE) venv
262+
@mkdir -p $(TEST_DOCS_DIR)
263+
@/bin/bash -c "source $(VENV_DIR)/bin/activate && \
264+
python3 -m pytest --doctest-modules mcpgateway/ \
265+
--cov=mcpgateway --cov-report=term --cov-report=html:htmlcov-doctest \
266+
--cov-report=xml:coverage-doctest.xml"
267+
@echo "✅ Doctest coverage report generated in htmlcov-doctest/"
268+
269+
doctest-check:
270+
@echo "🔍 Checking doctest coverage..."
271+
@test -d "$(VENV_DIR)" || $(MAKE) venv
272+
@/bin/bash -c "source $(VENV_DIR)/bin/activate && \
273+
python3 -c \"import subprocess, sys; \
274+
result = subprocess.run(['python', '-m', 'pytest', '--doctest-modules', 'mcpgateway/', '--tb=no', '-q'], capture_output=True); \
275+
if result.returncode == 0: \
276+
print('✅ All doctests passing'); \
277+
else: \
278+
print('❌ Doctest failures detected'); \
279+
print(result.stdout.decode()); \
280+
print(result.stderr.decode()); \
281+
sys.exit(1)\""
282+
242283
# =============================================================================
243284
# 📊 METRICS
244285
# =============================================================================
@@ -304,8 +345,8 @@ images:
304345
@mkdir -p $(DOCS_DIR)/docs/design/images
305346
@code2flow mcpgateway/ --output $(DOCS_DIR)/docs/design/images/code2flow.dot || true
306347
@dot -Tsvg -Gbgcolor=transparent -Gfontname="Arial" -Nfontname="Arial" -Nfontsize=14 -Nfontcolor=black -Nfillcolor=white -Nshape=box -Nstyle="filled,rounded" -Ecolor=gray -Efontname="Arial" -Efontsize=14 -Efontcolor=black $(DOCS_DIR)/docs/design/images/code2flow.dot -o $(DOCS_DIR)/docs/design/images/code2flow.svg || true
307-
@/bin/bash -c "source $(VENV_DIR)/bin/activate && python -m pip install snakefood3"
308-
@/bin/bash -c "source $(VENV_DIR)/bin/activate && python -m snakefood3 . mcpgateway > snakefood.dot"
348+
@/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 -m pip install snakefood3"
349+
@/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 -m snakefood3 . mcpgateway > snakefood.dot"
309350
@dot -Tpng -Gbgcolor=transparent -Gfontname="Arial" -Nfontname="Arial" -Nfontsize=12 -Nfontcolor=black -Nfillcolor=white -Nshape=box -Nstyle="filled,rounded" -Ecolor=gray -Efontname="Arial" -Efontsize=10 -Efontcolor=black snakefood.dot -o $(DOCS_DIR)/docs/design/images/snakefood.png || true
310351
@pyreverse --colorized mcpgateway || true
311352
@dot -Tsvg -Gbgcolor=transparent -Gfontname="Arial" -Nfontname="Arial" -Nfontsize=14 -Nfontcolor=black -Nfillcolor=white -Nshape=box -Nstyle="filled,rounded" -Ecolor=gray -Efontname="Arial" -Efontsize=14 -Efontcolor=black packages.dot -o $(DOCS_DIR)/docs/design/images/packages.svg || true
@@ -403,7 +444,13 @@ pycodestyle: ## 📝 Simple PEP-8 checker
403444
@$(VENV_DIR)/bin/pycodestyle mcpgateway --max-line-length=200
404445

405446
pre-commit: ## 🪄 Run pre-commit hooks
406-
@$(VENV_DIR)/bin/pre-commit run --all-files --show-diff-on-failure
447+
@echo "🪄 Running pre-commit hooks..."
448+
@test -d "$(VENV_DIR)" || $(MAKE) venv install install-dev
449+
@if [ ! -f "$(VENV_DIR)/bin/pre-commit" ]; then \
450+
echo "📦 Installing pre-commit..."; \
451+
/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 -m pip install --quiet pre-commit"; \
452+
fi
453+
@/bin/bash -c "source $(VENV_DIR)/bin/activate && pre-commit run --all-files --show-diff-on-failure"
407454

408455
ruff: ## ⚡ Ruff lint + format
409456
@$(VENV_DIR)/bin/ruff check mcpgateway && $(VENV_DIR)/bin/ruff format mcpgateway tests
@@ -460,7 +507,7 @@ spellcheck-sort: .spellcheck-en.txt ## 🔤 Sort spell-list
460507

461508
tox: ## 🧪 Multi-Python tox matrix (uv)
462509
@echo "🧪 Running tox with uv ..."
463-
python -m tox -p auto $(TOXARGS)
510+
python3 -m tox -p auto $(TOXARGS)
464511

465512
sbom: ## 🛡️ Generate SBOM & security report
466513
@echo "🛡️ Generating SBOM & security report..."
@@ -470,7 +517,7 @@ sbom: ## 🛡️ Generate SBOM & security report
470517
@/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 -m uv pip install cyclonedx-bom sbom2doc"
471518
@echo "🔍 Generating SBOM from environment..."
472519
@/bin/bash -c "source $(VENV_DIR)/bin/activate && \
473-
python -m cyclonedx_py environment \
520+
python3 -m cyclonedx_py environment \
474521
--output-format XML \
475522
--output-file $(PROJECT_NAME).sbom.xml \
476523
--no-validate \
@@ -843,7 +890,7 @@ pip-audit:
843890
deps-update:
844891
@echo "⬆️ Updating project dependencies via update-deps.py..."
845892
@test -f update-deps.py || { echo "❌ update-deps.py not found in root directory."; exit 1; }
846-
@/bin/bash -c "source $(VENV_DIR)/bin/activate && python update-deps.py"
893+
@/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 update-deps.py"
847894
@echo "✅ Dependencies updated in pyproject.toml and docs/requirements.txt"
848895

849896
containerfile-update:
@@ -2090,7 +2137,7 @@ devpi-unconfigure-pip:
20902137
# 📦 Version helper (defaults to the version in pyproject.toml)
20912138
# override on the CLI: make VER=0.2.1 devpi-delete
20922139
# ─────────────────────────────────────────────────────────────────────────────
2093-
VER ?= $(shell python -c "import tomllib, pathlib; \
2140+
VER ?= $(shell python3 -c "import tomllib, pathlib; \
20942141
print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])" \
20952142
2>/dev/null || echo 0.0.0)
20962143

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ curl -s -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \
238238
```powershell
239239
# 1️⃣ Isolated env + install from PyPI
240240
mkdir mcpgateway ; cd mcpgateway
241-
python -m venv .venv ; .\.venv\Scripts\Activate.ps1
241+
python3 -m venv .venv ; .\.venv\Scripts\Activate.ps1
242242
pip install --upgrade pip
243243
pip install mcp-contextforge-gateway
244244
@@ -255,7 +255,7 @@ mcpgateway.exe --host 0.0.0.0 --port 4444
255255
# Start-Process -FilePath "mcpgateway.exe" -ArgumentList "--host 0.0.0.0 --port 4444"
256256
257257
# 4️⃣ Bearer token and smoke-test
258-
$Env:MCPGATEWAY_BEARER_TOKEN = python -m mcpgateway.utils.create_jwt_token `
258+
$Env:MCPGATEWAY_BEARER_TOKEN = python3 -m mcpgateway.utils.create_jwt_token `
259259
--username admin --exp 10080 --secret my-test-key
260260
261261
curl -s -H "Authorization: Bearer $Env:MCPGATEWAY_BEARER_TOKEN" `
@@ -389,7 +389,7 @@ docker logs -f mcpgateway
389389

390390
# Generating an API key
391391
docker run --rm -it ghcr.io/ibm/mcp-context-forge:0.3.1 \
392-
python -m mcpgateway.utils.create_jwt_token --username admin --exp 0 --secret my-test-key
392+
python3 -m mcpgateway.utils.create_jwt_token --username admin --exp 0 --secret my-test-key
393393
```
394394

395395
Browse to **[http://localhost:4444/admin](http://localhost:4444/admin)** (user `admin` / pass `changeme`).

charts/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ migration:
325325
tag: latest # Should match mcpContextForge.image.tag
326326

327327
command:
328-
waitForDb: "python /app/mcpgateway/utils/db_isready.py --max-tries 30 --interval 2 --timeout 5"
328+
waitForDb: "python3 /app/mcpgateway/utils/db_isready.py --max-tries 30 --interval 2 --timeout 5"
329329
migrate: "alembic upgrade head || echo '⚠️ Migration check failed'"
330330
---
331331

charts/mcp-stack/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ Kubernetes: `>=1.21.0`
177177
| migration.activeDeadlineSeconds | int | `600` | |
178178
| migration.backoffLimit | int | `3` | |
179179
| migration.command.migrate | string | `"alembic upgrade head || echo '⚠️ Migration check failed'"` | |
180-
| migration.command.waitForDb | string | `"python /app/mcpgateway/utils/db_isready.py --max-tries 30 --interval 2 --timeout 5"` | |
180+
| migration.command.waitForDb | string | `"python3 /app/mcpgateway/utils/db_isready.py --max-tries 30 --interval 2 --timeout 5"` | |
181181
| migration.enabled | bool | `true` | |
182182
| migration.image.pullPolicy | string | `"Always"` | |
183183
| migration.image.repository | string | `"ghcr.io/ibm/mcp-context-forge"` | |

0 commit comments

Comments
 (0)