Skip to content

Commit fcf11b8

Browse files
authored
harden python mcp server starter
1 parent 8e39dd6 commit fcf11b8

9 files changed

Lines changed: 198 additions & 38 deletions

File tree

.github/workflows/cd.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ jobs:
5151
5252
- name: Build
5353
run: |
54-
pip install build
54+
python -m pip install --upgrade pip
55+
python -m pip install build
5556
python -m build
5657
5758
- name: Generate SLSA build provenance attestation

.github/workflows/ci.yml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ jobs:
5252
cache-dependency-path: pyproject.toml
5353

5454
- name: Install dependencies
55-
run: pip install -e . pip-licenses
55+
run: |
56+
python -m pip install --upgrade pip
57+
python -m pip install -e ".[dev]"
5658
5759
- name: Check licenses
5860
run: pip-licenses --fail-on="GPL-2.0;GPL-3.0;AGPL-3.0"
@@ -72,12 +74,12 @@ jobs:
7274
cache-dependency-path: pyproject.toml
7375

7476
- name: Install dependencies
75-
run: pip install -e ".[dev]"
77+
run: |
78+
python -m pip install --upgrade pip
79+
python -m pip install -e ".[dev]"
7680
7781
- name: Dependency audit (pip-audit)
78-
run: |
79-
python -m pip install pip-audit
80-
pip-audit --strict || pip-audit
82+
run: pip-audit . --strict
8183

8284
- name: Lint
8385
run: ruff check .

README.ko.md

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ MCP 서버를 만들고, 원클릭 배포. 시크릿 불필요.
3939
npx @starter-series/create my-mcp-server --template mcp-server-python
4040
cd my-mcp-server
4141
python -m venv .venv && source .venv/bin/activate
42-
pip install -e '.[dev]'
42+
python -m pip install -e '.[dev]'
43+
python -m pytest
4344
```
4445

4546
**또는 직접 클론:**
@@ -48,7 +49,8 @@ pip install -e '.[dev]'
4849
git clone https://github.com/starter-series/python-mcp-server-starter my-mcp-server
4950
cd my-mcp-server
5051
python -m venv .venv && source .venv/bin/activate
51-
pip install -e '.[dev]'
52+
python -m pip install -e '.[dev]'
53+
python -m pytest
5254
```
5355

5456
## 도구 추가
@@ -137,13 +139,25 @@ def register(mcp: FastMCP) -> None:
137139

138140
```bash
139141
# 테스트 실행
140-
pytest -v
142+
python -m pytest -v
141143

142144
# 린트
143-
ruff check .
145+
python -m ruff check .
146+
147+
# 포맷 확인
148+
python -m ruff format --check .
149+
150+
# 타입 확인
151+
python -m mypy src/
152+
153+
# wheel + sdist 빌드
154+
python -m build
144155

145156
# 서버 실행 (stdio)
146157
python -m my_mcp_server
158+
159+
# 설치된 console script로 동일 서버 실행
160+
my-mcp-server
147161
```
148162

149163
## CI/CD
@@ -182,11 +196,14 @@ src/my_mcp_server/
182196
tests/
183197
├── test_tools.py # 툴 테스트
184198
├── test_server_info.py # Resource 테스트
185-
└── test_code_review.py # Prompt 테스트
199+
├── test_code_review.py # Prompt 테스트
200+
├── test_runtime_contract.py # 시작/패키지 메타데이터 테스트
201+
└── test_version_resolution.py # 버전 SSOT 테스트
186202
.github/
187203
├── workflows/
188204
│ ├── ci.yml # 린트, 테스트, 보안
189205
│ ├── cd.yml # PyPI OIDC 배포
206+
│ ├── codeql.yml # 정적 분석
190207
│ ├── stale.yml # Stale 이슈 관리
191208
│ └── maintenance.yml # 주간 헬스 체크
192209
└── dependabot.yml # 의존성 업데이트
@@ -195,11 +212,14 @@ tests/
195212
## 스크립트
196213

197214
```bash
198-
pip install -e ".[dev]" # dev 의존성 포함 설치
215+
python -m pip install -e ".[dev]" # dev 의존성 포함 설치
199216
python -m my_mcp_server # 서버 실행
200-
pytest -v # 테스트 실행
201-
ruff check . # 린트
202-
ruff format . # 포맷
217+
my-mcp-server # 설치된 console script로 서버 실행
218+
python -m pytest -v # 테스트 실행
219+
python -m ruff check . # 린트
220+
python -m ruff format . # 포맷
221+
python -m mypy src/ # 타입 확인
222+
python -m build # wheel + sdist 빌드
203223
```
204224

205225
## 라이선스

README.md

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ Build your MCP server. One-click publish. Zero secrets needed.
4242
npx @starter-series/create my-mcp-server --template mcp-server-python
4343
cd my-mcp-server
4444
python -m venv .venv && source .venv/bin/activate
45-
pip install -e '.[dev]'
45+
python -m pip install -e '.[dev]'
46+
python -m pytest
4647
```
4748

4849
**Or clone directly:**
@@ -51,7 +52,8 @@ pip install -e '.[dev]'
5152
git clone https://github.com/starter-series/python-mcp-server-starter my-mcp-server
5253
cd my-mcp-server
5354
python -m venv .venv && source .venv/bin/activate
54-
pip install -e '.[dev]'
55+
python -m pip install -e '.[dev]'
56+
python -m pytest
5557
```
5658

5759
## Adding Tools
@@ -157,13 +159,25 @@ Add your own in `server.py`.
157159

158160
```bash
159161
# Run tests
160-
pytest -v
162+
python -m pytest -v
161163

162164
# Lint
163-
ruff check .
165+
python -m ruff check .
166+
167+
# Format check
168+
python -m ruff format --check .
169+
170+
# Type check
171+
python -m mypy src/
172+
173+
# Build wheel + sdist
174+
python -m build
164175

165176
# Run the server (stdio)
166177
python -m my_mcp_server
178+
179+
# Same server via installed console script
180+
my-mcp-server
167181
```
168182

169183
## CI/CD
@@ -202,11 +216,14 @@ src/my_mcp_server/
202216
tests/
203217
├── test_tools.py # Tool tests
204218
├── test_server_info.py # Resource tests
205-
└── test_code_review.py # Prompt tests
219+
├── test_code_review.py # Prompt tests
220+
├── test_runtime_contract.py # Startup + package metadata tests
221+
└── test_version_resolution.py # Version SSOT tests
206222
.github/
207223
├── workflows/
208224
│ ├── ci.yml # Lint, test, security
209225
│ ├── cd.yml # PyPI OIDC publish
226+
│ ├── codeql.yml # Static analysis
210227
│ ├── stale.yml # Stale issue management
211228
│ └── maintenance.yml # Weekly health check
212229
└── dependabot.yml # Dependency updates
@@ -215,11 +232,14 @@ tests/
215232
## Scripts
216233

217234
```bash
218-
pip install -e ".[dev]" # Install with dev deps
235+
python -m pip install -e ".[dev]" # Install with dev deps
219236
python -m my_mcp_server # Run server
220-
pytest -v # Run tests
221-
ruff check . # Lint
222-
ruff format . # Format
237+
my-mcp-server # Run installed console script
238+
python -m pytest -v # Run tests
239+
python -m ruff check . # Lint
240+
python -m ruff format . # Format
241+
python -m mypy src/ # Type check
242+
python -m build # Build wheel + sdist
223243
```
224244

225245
## License

pyproject.toml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ readme = "README.md"
1010
license = "MIT"
1111
requires-python = ">=3.11"
1212
authors = [
13-
{ name = "Your Name" },
13+
{ name = "Heznpc" },
1414
]
1515
keywords = ["mcp", "mcp-server", "model-context-protocol"]
1616
classifiers = [
@@ -25,12 +25,20 @@ dependencies = [
2525
"mcp>=1.27.1,<2",
2626
]
2727

28+
[project.urls]
29+
Homepage = "https://github.com/starter-series/python-mcp-server-starter"
30+
Repository = "https://github.com/starter-series/python-mcp-server-starter"
31+
Issues = "https://github.com/starter-series/python-mcp-server-starter/issues"
32+
2833
[project.scripts]
29-
my-mcp-server = "my_mcp_server.server:main"
34+
my-mcp-server = "my_mcp_server.__main__:run"
3035

3136
[project.optional-dependencies]
3237
# Major-bounded so a breaking ruff/mypy/pytest release can't turn CI red overnight.
3338
dev = [
39+
"build>=1.5.0,<2",
40+
"pip-audit>=2.10.1,<3",
41+
"pip-licenses>=5.5.5,<6",
3442
"pytest>=9.0.3,<10",
3543
"pytest-asyncio>=1.4.0,<2",
3644
"pytest-cov>=7.1.0,<8",
@@ -80,3 +88,6 @@ exclude_also = [
8088
"if TYPE_CHECKING:",
8189
"if __name__ == .__main__.:",
8290
]
91+
92+
[tool.hatch.build.targets.wheel]
93+
packages = ["src/my_mcp_server"]

src/my_mcp_server/prompts/code_review.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from mcp.server.fastmcp import FastMCP
1313
from mcp.server.fastmcp.prompts.base import UserMessage
14-
from pydantic import Field, validate_call
14+
from pydantic import StringConstraints, validate_call
1515

1616
NAME = "code-review"
1717
TITLE = "Code Review"
@@ -30,7 +30,7 @@
3030
@validate_call
3131
def code_review(
3232
language: Language,
33-
code: Annotated[str, Field(min_length=1, description="Source code to review.")],
33+
code: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)],
3434
) -> list[UserMessage]:
3535
"""Render a code-review prompt as a single-user-message template.
3636

src/my_mcp_server/server.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from mcp.server.fastmcp import FastMCP
1212
from mcp.types import ToolAnnotations
13-
from pydantic import Field
13+
from pydantic import StringConstraints
1414

1515
from my_mcp_server.prompts.code_review import register as register_code_review
1616
from my_mcp_server.resources.server_info import register as register_server_info
@@ -22,14 +22,30 @@
2222
# ---------------------------------------------------------------------------
2323

2424
DEBUG = os.environ.get("MCP_DEBUG", "false").lower() == "true"
25+
VALID_LOG_LEVELS = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
26+
27+
28+
def resolve_log_level() -> str:
29+
"""Return a logging level name accepted by ``logging``."""
30+
if DEBUG:
31+
return "DEBUG"
32+
33+
candidate = os.environ.get("LOG_LEVEL", "INFO").upper()
34+
if candidate not in VALID_LOG_LEVELS:
35+
return "INFO"
36+
return candidate
37+
2538

2639
# ---------------------------------------------------------------------------
2740
# Server
2841
# ---------------------------------------------------------------------------
2942

43+
SERVER_NAME = "my-mcp-server"
44+
SERVER_INSTRUCTIONS = "An MCP server. Replace this with your description."
45+
3046
mcp = FastMCP(
31-
"my-mcp-server",
32-
instructions="An MCP server. Replace this with your description.",
47+
SERVER_NAME,
48+
instructions=SERVER_INSTRUCTIONS,
3349
)
3450

3551

@@ -50,10 +66,10 @@
5066
async def greet(
5167
name: Annotated[
5268
str,
53-
Field(
69+
StringConstraints(
70+
strip_whitespace=True,
5471
min_length=1,
5572
max_length=200,
56-
description="Name to greet (1-200 characters).",
5773
),
5874
],
5975
) -> str:
@@ -64,8 +80,12 @@ async def greet(
6480
are rejected by the protocol layer before the handler runs. The TS
6581
sibling enforces the same shape via Zod.
6682
"""
67-
logger.info("Greeting %s", name)
68-
return f"Hello, {name}!"
83+
normalized = name.strip()
84+
if not normalized:
85+
raise ValueError("name must contain at least one non-whitespace character")
86+
87+
logger.info("Greeting %s", normalized)
88+
return f"Hello, {normalized}!"
6989

7090

7191
# To add more tools, either decorate inline above or split them into modules
@@ -94,9 +114,8 @@ async def greet(
94114

95115
def main() -> None:
96116
"""Run the MCP server."""
97-
log_level = "DEBUG" if DEBUG else os.environ.get("LOG_LEVEL", "INFO").upper()
98117
logging.basicConfig(
99-
level=getattr(logging, log_level, logging.INFO),
118+
level=getattr(logging, resolve_log_level()),
100119
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
101120
)
102-
mcp.run()
121+
mcp.run(transport="stdio")

0 commit comments

Comments
 (0)