Skip to content

Commit b5626dd

Browse files
Taxusptvedantwalia
andauthored
chore: improve packaging, pytest path, and server startup tests (#242)
* updating the codebase to use the new MCP server startup mechanism. This includes changes to the unit and e2e tests to reflect the new server parameters and startup process. The tests have been modified to ensure they are compatible with the updated server initialization logic. * code updated as per feedback receievd * cleaned up code --------- Co-authored-by: Vedant Walia <vedantwalia@outlook.com>
1 parent 3619b9a commit b5626dd

6 files changed

Lines changed: 80 additions & 45 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ build/
1212
# Virtual environments
1313
.env
1414
.venv/
15+
.uv-cache/
1516
*.log
1617
.DS_Store
1718
.claude.md

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ dependencies = [
2222
garmin-mcp = "garmin_mcp:main"
2323
garmin-mcp-auth = "garmin_mcp.auth_cli:main"
2424

25+
[tool.hatch.build.targets.wheel]
26+
packages = ["src/garmin_mcp"]
27+
2528
[tool.uv]
2629
dev-dependencies = [
2730
"pytest>=9.0.2",

pytest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ python_functions = test_*
88

99
# Test paths
1010
testpaths = tests
11+
pythonpath = src
1112

1213
# Minimum version
1314
minversion = 7.0

tests/e2e/test_server_e2e.py

Lines changed: 27 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
Or skip with: pytest -m "not e2e"
1414
"""
1515

16+
import os
17+
import sys
1618
import pytest
1719
import asyncio
1820
from datetime import datetime
@@ -27,6 +29,22 @@
2729
load_dotenv()
2830

2931

32+
def _build_server_params():
33+
"""Construct MCP server parameters using the active Python interpreter."""
34+
env = os.environ.copy()
35+
repo_root = Path(__file__).resolve().parents[2]
36+
src_path = repo_root / "src"
37+
if str(src_path) not in env.get("PYTHONPATH", ""):
38+
env["PYTHONPATH"] = os.pathsep.join(
39+
filter(None, [str(src_path), env.get("PYTHONPATH", "")])
40+
)
41+
return StdioServerParameters(
42+
command=sys.executable,
43+
args=["-m", "garmin_mcp"],
44+
env=env,
45+
)
46+
47+
3048
@pytest.mark.e2e
3149
@pytest.mark.asyncio
3250
@pytest.mark.timeout(30) # Pytest timeout
@@ -39,11 +57,7 @@ async def test_mcp_server_connection():
3957
- May require MFA code input if tokens are expired
4058
"""
4159
# Use python module execution instead of direct script path
42-
server_params = StdioServerParameters(
43-
command="python",
44-
args=["-m", "garmin_mcp"],
45-
env=None, # Uses current environment which includes .env variables
46-
)
60+
server_params = _build_server_params()
4761

4862
# Connect to server with timeout
4963
try:
@@ -77,11 +91,7 @@ async def test_mcp_server_connection():
7791
@pytest.mark.timeout(30)
7892
async def test_list_activities_tool():
7993
"""Test the list_activities MCP tool with real API"""
80-
server_params = StdioServerParameters(
81-
command="python",
82-
args=["-m", "garmin_mcp"],
83-
env=None,
84-
)
94+
server_params = _build_server_params()
8595

8696
try:
8797
async with asyncio.timeout(20):
@@ -111,11 +121,7 @@ async def test_list_activities_tool():
111121
@pytest.mark.timeout(30)
112122
async def test_get_steps_data_tool():
113123
"""Test the get_steps_data MCP tool with real API"""
114-
server_params = StdioServerParameters(
115-
command="python",
116-
args=["-m", "garmin_mcp"],
117-
env=None,
118-
)
124+
server_params = _build_server_params()
119125

120126
try:
121127
async with asyncio.timeout(20):
@@ -147,11 +153,7 @@ async def test_get_steps_data_tool():
147153
@pytest.mark.timeout(45)
148154
async def test_multiple_tools():
149155
"""Test multiple MCP tools in a single session"""
150-
server_params = StdioServerParameters(
151-
command="python",
152-
args=["-m", "garmin_mcp"],
153-
env=None,
154-
)
156+
server_params = _build_server_params()
155157

156158
try:
157159
async with asyncio.timeout(40):
@@ -202,11 +204,7 @@ async def test_schedule_workouts_tool():
202204
import os
203205
import json
204206

205-
server_params = StdioServerParameters(
206-
command="python",
207-
args=["-m", "garmin_mcp"],
208-
env=None,
209-
)
207+
server_params = _build_server_params()
210208

211209
workout_ids_env = os.environ.get("GARMIN_TEST_WORKOUT_IDS", "")
212210
dates_env = os.environ.get("GARMIN_TEST_SCHEDULE_DATES", "")
@@ -265,11 +263,7 @@ async def test_upload_workouts_tool():
265263
"""
266264
import json
267265

268-
server_params = StdioServerParameters(
269-
command="python",
270-
args=["-m", "garmin_mcp"],
271-
env=None,
272-
)
266+
server_params = _build_server_params()
273267

274268
minimal_workout = {
275269
"workoutName": "e2e Test Workout - DELETE ME",
@@ -343,11 +337,7 @@ async def test_delete_workouts_tool():
343337
"""
344338
import json
345339

346-
server_params = StdioServerParameters(
347-
command="python",
348-
args=["-m", "garmin_mcp"],
349-
env=None,
350-
)
340+
server_params = _build_server_params()
351341

352342
try:
353343
async with asyncio.timeout(50):
@@ -393,11 +383,7 @@ async def test_schedule_workouts_inline_upload():
393383
"""
394384
import json
395385

396-
server_params = StdioServerParameters(
397-
command="python",
398-
args=["-m", "garmin_mcp"],
399-
env=None,
400-
)
386+
server_params = _build_server_params()
401387

402388
inline_workout = {
403389
"workoutName": "e2e Inline Test - DELETE ME",
@@ -471,11 +457,7 @@ async def test_schedule_workouts_missing_required_fields():
471457
"""
472458
import json
473459

474-
server_params = StdioServerParameters(
475-
command="python",
476-
args=["-m", "garmin_mcp"],
477-
env=None,
478-
)
460+
server_params = _build_server_params()
479461

480462
try:
481463
async with asyncio.timeout(50):
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""Project metadata regression tests."""
2+
3+
from pathlib import Path
4+
5+
6+
def test_project_caps_mcp_to_v1_series() -> None:
7+
"""The project should explicitly stay on the MCP v1 series for compatibility."""
8+
repo_root = Path(__file__).resolve().parents[2]
9+
pyproject_text = (repo_root / "pyproject.toml").read_text()
10+
11+
assert '"mcp>=1.28.1,<2"' in pyproject_text

tests/unit/test_server_startup.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Startup smoke tests for the packaged MCP server."""
2+
3+
import asyncio
4+
from unittest.mock import Mock
5+
6+
import garmin_mcp
7+
from mcp.server.fastmcp import FastMCP
8+
9+
10+
def test_main_registers_tools_and_starts_stdio(monkeypatch):
11+
"""Run main() without real Garmin auth and stop before entering the server loop."""
12+
run_calls = []
13+
14+
monkeypatch.delenv("GARMIN_MCP_TRANSPORT", raising=False)
15+
monkeypatch.delenv("GARMIN_MCP_HOST", raising=False)
16+
monkeypatch.delenv("GARMIN_MCP_PORT", raising=False)
17+
monkeypatch.setattr(garmin_mcp, "init_api", lambda _email, _password: Mock())
18+
19+
def capture_run(self, **kwargs):
20+
tools = asyncio.run(self.list_tools())
21+
run_calls.append(
22+
{
23+
"transport": kwargs.get("transport"),
24+
"tool_count": len(tools),
25+
"tool_names": [tool.name for tool in tools],
26+
}
27+
)
28+
29+
monkeypatch.setattr(FastMCP, "run", capture_run)
30+
31+
garmin_mcp.main()
32+
33+
assert run_calls
34+
assert run_calls[0]["transport"] == "stdio"
35+
assert run_calls[0]["tool_count"] >= 10
36+
assert "get_devices" in run_calls[0]["tool_names"]
37+
assert "get_workouts" in run_calls[0]["tool_names"]

0 commit comments

Comments
 (0)