Skip to content

Commit 3e86952

Browse files
Merge pull request #10 from openapi/dev
Improve CI workflows and enhance API error logging
2 parents 0c5fdad + 68e0f5a commit 3e86952

19 files changed

Lines changed: 658 additions & 279 deletions

.github/copilot-instructions.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ Common patterns and code examples
2525
Environment and runtime
2626
- Run locally: `python src/openapi_mcp_sdk/main.py` (README shows `python main.py` from project root; in this layout use the module path). The server binds to 0.0.0.0:PORT (reads `PORT` env var, default 80).
2727
- Virtualenv: repository uses `uv` in README but `pyproject.toml` shows `requires-python = ">=3.13"`. Note: README states Python 3.9+. If you modify runtime or CI, confirm the correct Python target.
28-
- Memcached configuration is read from `MEMCACHED_HOST`, `MEMCACHED_PORT`, and `X-DEV-VM`/`K_SERVICE` influence defaults. Tests or dev runs can run with a mocked `memory_store` (it falls back to in-process dict on Memcached errors).
2928

3029
Developer workflows (concrete)
3130
- Start server (local):
@@ -42,7 +41,6 @@ Conventions and gotchas for agents
4241
- When adding a new API/tool module:
4342
- Follow the `@mcp.tool` decorator pattern and ensure the module is imported from `main.py` or otherwise registered during startup.
4443
- Prefer to call `make_api_call` for HTTP interactions so the common auth/header extraction is reused.
45-
- Beware `BASE_URL` / `callbackUrl` derivation in `memory_store.py` — it depends on `K_SERVICE` and `X-DEV-VM`. Tests running on CI may need to set these env vars.
4644
- The code expects `ctx` to contain `request_context.request.headers` in some places; use defensive checks when reading headers if you add new code paths.
4745

4846
What I couldn't verify automatically

.github/workflows/build.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
strategy:
1515
fail-fast: false
1616
matrix:
17-
python-version: ["3.9", "3.10", "3.11"]
17+
python-version: ["3.10", "3.11"]
1818

1919
steps:
2020
- uses: actions/checkout@v4
@@ -30,9 +30,9 @@ jobs:
3030
- name: Lint with flake8
3131
run: |
3232
# stop the build if there are Python syntax errors or undefined names
33-
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
33+
flake8 src --count --select=E9,F63,F7,F82 --show-source --statistics
3434
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
35-
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
35+
flake8 src --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
3636
- name: Test with pytest
3737
run: |
3838
pytest

.github/workflows/pylint.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Pylint
1+
name: pylint
22

33
on: [push]
44

@@ -20,4 +20,4 @@ jobs:
2020
pip install pylint
2121
- name: Analysing the code with pylint
2222
run: |
23-
pylint $(git ls-files '*.py')
23+
pylint $(git ls-files 'src/*.py')

Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
UV := uv
3939
HOST := 0.0.0.0
4040
MCP_PORT := 8080
41+
MCP_ENV ?= dev
42+
DEV_MODE ?= 1
4143
NGROK_DOMAIN ?=
4244

4345
## =======
@@ -60,7 +62,7 @@ check-env:
6062
## Setup the local environment and start the server (exposes a public ngrok URL if ngrok is installed)
6163
## Set NGROK_DOMAIN=your-name.ngrok-free.app for a stable URL across restarts.
6264
start: check-env .install.stamp
63-
@MCP_PORT=$(MCP_PORT) HOST=$(HOST) NGROK_DOMAIN=$(NGROK_DOMAIN) bash scripts/start.sh
65+
@MCP_PORT=$(MCP_PORT) HOST=$(HOST) MCP_ENV=$(MCP_ENV) DEV_MODE=$(DEV_MODE) NGROK_DOMAIN=$(NGROK_DOMAIN) bash scripts/start.sh
6466

6567
## Start local MCP server and open Claude interactively - production (requires: export OPENAPI_TOKEN=your_token)
6668
try-claude: check-env .install.stamp

scripts/start.sh

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Start the MCP server and expose it publicly via ngrok (if installed).
33
# Usage: bash scripts/start.sh
44
# Env: MCP_PORT (default 8080), HOST (default 0.0.0.0)
5+
# MCP_ENV (default dev), DEV_MODE (default 1)
56
# NGROK_DOMAIN — set to your reserved ngrok static domain to get a
67
# stable URL that never changes across restarts.
78
# Claim your free static domain at:
@@ -13,6 +14,8 @@ set -uo pipefail
1314

1415
MCP_PORT="${MCP_PORT:-8080}"
1516
HOST="${HOST:-0.0.0.0}"
17+
MCP_ENV="${MCP_ENV:-dev}"
18+
DEV_MODE="${DEV_MODE:-1}"
1619
NGROK_DOMAIN="${NGROK_DOMAIN:-}"
1720

1821
SERVER_PID=""
@@ -25,12 +28,34 @@ cleanup() {
2528
}
2629
trap cleanup EXIT INT TERM
2730

28-
# ── clear Python bytecode cache so edited sources are always reloaded ──────
29-
find src/ -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
31+
clear_python_cache() {
32+
find src/ -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
33+
}
34+
35+
start_server() {
36+
clear_python_cache
37+
PYTHONPATH=src MCP_ENV="$MCP_ENV" \
38+
uv run uvicorn openapi_mcp_sdk.main:app \
39+
--host "$HOST" \
40+
--port "$MCP_PORT" \
41+
--log-config scripts/log_config.json &
42+
SERVER_PID=$!
43+
}
44+
45+
stop_server() {
46+
[ -n "$SERVER_PID" ] && kill "$SERVER_PID" 2>/dev/null || true
47+
wait "$SERVER_PID" 2>/dev/null || true
48+
SERVER_PID=""
49+
}
50+
51+
snapshot_sources() {
52+
find src tests scripts -type f \( -name '*.py' -o -name '*.php' \) -print0 2>/dev/null \
53+
| sort -z \
54+
| xargs -0 stat -c '%n:%Y' 2>/dev/null
55+
}
3056

3157
# ── start uvicorn ──────────────────────────────────────────────────────────
32-
PYTHONPATH=src uv run uvicorn openapi_mcp_sdk.main:app --host "$HOST" --port "$MCP_PORT" --log-config scripts/log_config.json &
33-
SERVER_PID=$!
58+
start_server
3459

3560
# ── wait for server to accept connections ──────────────────────────────────
3661
printf "Waiting for server\n"
@@ -97,4 +122,30 @@ else
97122
echo "Check the dashboard at http://localhost:4040"
98123
fi
99124

100-
wait "$SERVER_PID" 2>/dev/null || true
125+
if [ "$DEV_MODE" != "1" ]; then
126+
wait "$SERVER_PID" 2>/dev/null || true
127+
exit 0
128+
fi
129+
130+
echo "dev mode enabled (MCP_ENV=${MCP_ENV})"
131+
echo "watching *.py and *.php for changes"
132+
133+
LAST_SNAPSHOT="$(snapshot_sources)"
134+
while true; do
135+
sleep 1
136+
137+
if [ -n "$SERVER_PID" ] && ! kill -0 "$SERVER_PID" 2>/dev/null; then
138+
echo "server exited, restarting..."
139+
start_server
140+
LAST_SNAPSHOT="$(snapshot_sources)"
141+
continue
142+
fi
143+
144+
CURRENT_SNAPSHOT="$(snapshot_sources)"
145+
if [ "$CURRENT_SNAPSHOT" != "$LAST_SNAPSHOT" ]; then
146+
echo "source change detected, restarting server..."
147+
stop_server
148+
start_server
149+
LAST_SNAPSHOT="$CURRENT_SNAPSHOT"
150+
fi
151+
done
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
1+
"""Async status tools."""
2+
13
import logging
2-
logging.getLogger(__name__).debug("module loaded")
3-
from fastmcp import Context
44
from typing import Any
5+
6+
from fastmcp import Context # pylint: disable=import-error
7+
58
from ..mcp_core import mcp
69
from ..memory_store import get_callback_result
710

811
logger = logging.getLogger(__name__)
12+
logger.debug("module loaded")
13+
914

1015
@mcp.tool
1116
async def check_async_status(request_id: str, ctx: Context) -> Any:
1217
"""Show the last status of an async request
1318
Args:
1419
request_id: required, returned by an async downgraded request to the mcp server
1520
"""
21+
del ctx
1622
return get_callback_result(request_id)
Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,37 @@
1+
"""Automotive tools."""
2+
3+
# MCP tool names and parameter names intentionally mirror the public API.
4+
# pylint: disable=invalid-name,redefined-builtin
5+
16
import logging
2-
logging.getLogger(__name__).debug("module loaded")
3-
from fastmcp import Context
47
from typing import Any
8+
9+
from fastmcp import Context # pylint: disable=import-error
10+
511
from ..mcp_core import make_api_call, mcp
612
from ..memory_store import OPENAPI_HOST_PREFIX
713

814
logger = logging.getLogger(__name__)
15+
logger.debug("module loaded")
16+
917

1018
@mcp.tool
11-
async def check_license_plate(countryCode: str, type: str, licensePlate: str, ctx: Context) -> Any:
12-
"""Retrieve informations about type=car,bike,insurance,mot from a countryCode=IT,FR,UK,DE,PT,ES and a licensePlate.
13-
Available Combinations: IT-car, IT-bike, IT-insurance,FR-car,FR-bike,UK-car,UK-bike,UK-mot,PT-car,PT-insurance,ES-car,ES-bike
19+
async def check_license_plate(
20+
countryCode: str, type: str, licensePlate: str, ctx: Context
21+
) -> Any:
22+
"""Retrieve vehicle data for a supported country and plate.
23+
24+
Available combinations:
25+
IT-car, IT-bike, IT-insurance, FR-car, FR-bike, UK-car, UK-bike,
26+
UK-mot, PT-car, PT-insurance, ES-car, ES-bike.
27+
1428
Args:
1529
countryCode: required, 2 digit country code (IT|FR|UK|DE|PT|ES)
1630
type: required, type of information needed (car|bike|insurance|mot)
1731
licensePlate: required, the license plate to check
1832
"""
19-
url = f"https://{OPENAPI_HOST_PREFIX}automotive.openapi.com/{countryCode}-{type}/{licensePlate}"
33+
url = (
34+
f"https://{OPENAPI_HOST_PREFIX}automotive.openapi.com/"
35+
f"{countryCode}-{type}/{licensePlate}"
36+
)
2037
return make_api_call(ctx, "GET", url)

src/openapi_mcp_sdk/apis/cap.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1+
"""Italian postal code and municipality tools."""
2+
3+
# MCP tool names and parameter names intentionally mirror the public API.
4+
# pylint: disable=invalid-name
5+
16
import logging
2-
logging.getLogger(__name__).debug("module loaded")
3-
from fastmcp import Context
47
from typing import Any
8+
9+
from fastmcp import Context # pylint: disable=import-error
10+
511
from ..mcp_core import make_api_call, mcp
612
from ..memory_store import OPENAPI_HOST_PREFIX
713

814
logger = logging.getLogger(__name__)
15+
logger.debug("module loaded")
16+
917

1018
@mcp.tool
1119
async def get_IT_regions_list(ctx: Context) -> Any:
@@ -15,6 +23,7 @@ async def get_IT_regions_list(ctx: Context) -> Any:
1523
url = f"https://{OPENAPI_HOST_PREFIX}cap.openapi.it/regioni"
1624
return make_api_call(ctx, "GET", url)
1725

26+
1827
@mcp.tool
1928
async def get_IT_provinces_list(ctx: Context) -> Any:
2029
"""
@@ -23,6 +32,7 @@ async def get_IT_provinces_list(ctx: Context) -> Any:
2332
url = f"https://{OPENAPI_HOST_PREFIX}cap.openapi.it/province"
2433
return make_api_call(ctx, "GET", url)
2534

35+
2636
@mcp.tool
2737
async def get_IT_metropolitan_cities_list(ctx: Context) -> Any:
2838
"""
@@ -31,6 +41,7 @@ async def get_IT_metropolitan_cities_list(ctx: Context) -> Any:
3141
url = f"https://{OPENAPI_HOST_PREFIX}cap.openapi.it/citta_metropolitane"
3242
return make_api_call(ctx, "GET", url)
3343

44+
3445
@mcp.tool
3546
async def get_suppressed_italian_municipalities(ctx: Context) -> Any:
3647
"""
@@ -39,6 +50,7 @@ async def get_suppressed_italian_municipalities(ctx: Context) -> Any:
3950
url = f"https://{OPENAPI_HOST_PREFIX}cap.openapi.it/comuni_soppressi"
4051
return make_api_call(ctx, "GET", url)
4152

53+
4254
@mcp.tool
4355
async def find_IT_istat_by_comune_name(comune: str, ctx: Context) -> Any:
4456
"""
@@ -48,6 +60,7 @@ async def find_IT_istat_by_comune_name(comune: str, ctx: Context) -> Any:
4860
params = {"comune": comune}
4961
return make_api_call(ctx, "GET", url, params=params)
5062

63+
5164
@mcp.tool
5265
async def find_IT_municipality_by_istat(istatCode: str, ctx: Context) -> Any:
5366
"""
@@ -76,6 +89,7 @@ async def find_IT_municipality_by_istat(istatCode: str, ctx: Context) -> Any:
7689
url = f"https://{OPENAPI_HOST_PREFIX}cap.openapi.it/comuni_advance/{istatCode}"
7790
return make_api_call(ctx, "GET", url)
7891

92+
7993
@mcp.tool
8094
async def find_IT_municipalities_by_zip(zip_code: str, ctx: Context) -> Any:
8195
"""

0 commit comments

Comments
 (0)