Skip to content

Commit 7f98be9

Browse files
feat(dev): improve api error logs and autoreload start
1 parent 6f94f0a commit 7f98be9

3 files changed

Lines changed: 118 additions & 7 deletions

File tree

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

src/openapi_mcp_sdk/mcp_core.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,49 @@
1313

1414
logger = logging.getLogger(__name__)
1515

16+
17+
def _log_api_event(
18+
tag: str,
19+
client_ip: str,
20+
action: str,
21+
path: str,
22+
reason: Optional[str] = None,
23+
) -> None:
24+
if reason:
25+
logger.info('%s %s "%s %s (%s)"', tag, client_ip, action, path, reason)
26+
return
27+
logger.info('%s %s "%s %s"', tag, client_ip, action, path)
28+
29+
30+
def _describe_http_error(error: requests.exceptions.HTTPError) -> str:
31+
response = error.response
32+
if response is None:
33+
return "api error"
34+
35+
reason_parts = [f"api {response.status_code}"]
36+
if response.reason:
37+
reason_parts.append(str(response.reason).lower())
38+
39+
try:
40+
payload = response.json()
41+
except ValueError:
42+
payload = None
43+
44+
if isinstance(payload, dict):
45+
detail = payload.get("message") or payload.get("error")
46+
if detail:
47+
reason_parts.append(str(detail).strip())
48+
49+
return " - ".join(reason_parts)
50+
51+
52+
def _describe_request_exception(error: requests.exceptions.RequestException) -> str:
53+
if isinstance(error, requests.exceptions.Timeout):
54+
return "timeout"
55+
if isinstance(error, requests.exceptions.ConnectionError):
56+
return "connection failed"
57+
return str(error).strip() or "request failed"
58+
1659
# Create the MCP server instance
1760
mcp = FastMCP(
1861
name="Openapi.com MCP Gateway",
@@ -74,7 +117,6 @@ def make_api_call(ctx: Context, method: str, url: str, json_payload: Optional[di
74117
except Exception:
75118
client_ip = "?"
76119
tag = f"[{service}]"
77-
logger.info('%s %s "%s %s"', tag, client_ip, method, parsed.path)
78120
# Attempt to retrieve the Authorization header from multiple sources
79121
try:
80122
auth_header = None
@@ -104,8 +146,10 @@ def make_api_call(ctx: Context, method: str, url: str, json_payload: Optional[di
104146
raise ValueError("Missing or malformed Header 'Authorization: Bearer <token>'.")
105147

106148
except Exception as e:
149+
_log_api_event(tag, client_ip, "ERROR", parsed.path, "token mancante")
107150
return ApiError(error="Auth Error", message=f"Missing Token from client: {e}").model_dump()
108151

152+
_log_api_event(tag, client_ip, method, parsed.path)
109153
headers = {"Authorization": auth_header, **kwargs.pop("headers", {})}
110154
try:
111155
request_args = dict(method=method, url=url, headers=headers, **kwargs)
@@ -126,8 +170,22 @@ def make_api_call(ctx: Context, method: str, url: str, json_payload: Optional[di
126170
return return_data
127171
return response_data
128172
except requests.exceptions.HTTPError as e:
173+
_log_api_event(
174+
tag,
175+
client_ip,
176+
"ERROR",
177+
parsed.path,
178+
_describe_http_error(e),
179+
)
129180
error_details = e.response.text
130181
return e.response.json()
131182
return ApiError(error="API HTTP Error", message=f"{e.response.status_code}: {error_details}").model_dump()
132183
except requests.exceptions.RequestException as e:
184+
_log_api_event(
185+
tag,
186+
client_ip,
187+
"ERROR",
188+
parsed.path,
189+
_describe_request_exception(e),
190+
)
133191
return ApiError(error="API Request Error", message=str(e)).model_dump()

0 commit comments

Comments
 (0)