4 Commits
v0.1.3 ... main

Author SHA1 Message Date
Sun-ZhenXing
ac1de02173 feat: add mcp TransportSecuritySettings 2025-12-24 13:23:03 +08:00
Sun-ZhenXing
66b47a16bf feat: update to python 3.13 2025-12-01 14:02:08 +08:00
Sun-ZhenXing
9c793622d7 chore: Refactor code structure for improved readability and maintainability 2025-11-01 20:51:02 +08:00
Sun-ZhenXing
14ebafdc7a feat: add rename 2025-10-10 10:21:29 +08:00
14 changed files with 1289 additions and 577 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
# Trace files
radar.duckdb*
# Environment files
.env
.env.*

View File

@@ -1 +1 @@
3.12
3.13

View File

@@ -4,6 +4,7 @@
"charliermarsh.ruff",
"fill-labs.dependi",
"EditorConfig.EditorConfig",
"tamasfe.even-better-toml"
"tamasfe.even-better-toml",
"streetsidesoftware.code-spell-checker"
]
}

View File

@@ -2,12 +2,17 @@
"python.analysis.autoImportCompletions": true,
"python.analysis.typeCheckingMode": "standard",
"python.analysis.importFormat": "absolute",
"python.testing.pytestArgs": [
"."
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "never",
"source.convertImportFormat": "never",
"source.organizeImports.ruff": "explicit"
}
},
@@ -27,6 +32,7 @@
"PYPI",
"pyproject",
"streamable",
"trixie",
"uvicorn",
"venv"
]

View File

@@ -1,8 +1,8 @@
ARG PYPI_MIRROR_URL=https://pypi.org/simple
ARG DEBIAN_MIRROR=ftp.cn.debian.org
ARG DEBIAN_MIRROR=deb.debian.org
# Base stage
FROM python:3.12-bookworm AS deps
FROM python:3.13-trixie AS deps
ARG DEBIAN_FRONTEND=noninteractive
ARG PYPI_MIRROR_URL
WORKDIR /app
@@ -21,7 +21,7 @@ RUN --mount=type=cache,target=/root/.cache/uv,id=uv-cache,sharing=locked \
uv sync --no-dev --no-install-project
# Runner stage
FROM python:3.12-slim-bookworm AS runner
FROM python:3.13-slim-trixie AS runner
ARG DEBIAN_FRONTEND=noninteractive
ARG DEBIAN_MIRROR
ARG PYPI_MIRROR_URL
@@ -70,4 +70,4 @@ HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
CMD curl -f http://localhost:${PORT}/health || exit 1
EXPOSE ${PORT}
CMD ["sh", "-c", "uv run --no-sync prod --host 0.0.0.0 --port ${PORT}"]
CMD ["sh", "-c", "uv run --no-sync mcp-template-python --host 0.0.0.0 --port ${PORT}"]

View File

@@ -1,50 +1,54 @@
.PHONY: i dev prod build clean update lint docker-build docker-run helm-install helm-upgrade helm-uninstall helm-lint
.PHONY: i dev prod build clean update lint docker-build docker-run helm-install helm-upgrade helm-uninstall helm-lint rename
args := $(wordlist 2, $(words $(MAKECMDGOALS)), $(MAKECMDGOALS))
i:
uv sync --all-extras --all-packages $(filter-out i,$(MAKECMDGOALS))
uv sync --all-extras --all-packages $(args)
dev:
uv run --no-sync python -c "__import__('mcp_template_python.__main__').__main__.dev()" $(filter-out dev,$(MAKECMDGOALS)) || echo shutdown
uv run --no-sync python -c "__import__('mcp_template_python.__main__').__main__.dev()" $(args) || echo shutdown
prod:
uv run --no-sync python -c "__import__('mcp_template_python.__main__').__main__.main()" $(filter-out prod,$(MAKECMDGOALS))
uv run --no-sync python -c "__import__('mcp_template_python.__main__').__main__.main()" $(args)
build:
uv build $(filter-out build,$(MAKECMDGOALS))
uv build $(args)
clean:
rm -rf .venv .ruff_cache dist/ build/ *.egg-info $(filter-out clean,$(MAKECMDGOALS))
rm -rf .venv .ruff_cache dist/ build/ *.egg-info $(args)
update:
uv sync --all-extras --all-packages -U $(filter-out update,$(MAKECMDGOALS))
uv sync --all-extras --all-packages -U $(args)
lint:
uv run ruff check . $(filter-out lint,$(MAKECMDGOALS))
uv run ruff check . $(args)
docker-build:
docker compose build $(filter-out docker-build,$(MAKECMDGOALS))
docker compose build $(args)
docker-run:
docker compose up -d $(filter-out docker-run,$(MAKECMDGOALS))
docker compose up -d $(args)
# Helm deployment commands
helm-lint:
helm lint helm/mcp-template-python
helm lint helm/mcp-template-python $(args)
helm-install:
helm install mcp-template-python helm/mcp-template-python
helm install mcp-template-python helm/mcp-template-python $(args)
helm-upgrade:
helm upgrade mcp-template-python helm/mcp-template-python
helm upgrade mcp-template-python helm/mcp-template-python $(args)
helm-install-prod:
helm install mcp-template-python helm/mcp-template-python -f values-production.yaml
helm install mcp-template-python helm/mcp-template-python -f values-production.yaml $(args)
helm-upgrade-prod:
helm upgrade mcp-template-python helm/mcp-template-python -f values-production.yaml
helm upgrade mcp-template-python helm/mcp-template-python -f values-production.yaml $(args)
helm-uninstall:
helm uninstall mcp-template-python
helm uninstall mcp-template-python $(args)
rename:
uv run python tools/rename.py $(args)
%:
@:
@true

View File

@@ -1,6 +1,6 @@
[project]
name = "mcp-template-python"
version = "0.1.3"
version = "0.1.4"
description = "MCP Template for Python Projects"
readme = "README.md"
authors = []

View File

@@ -1,2 +1,2 @@
__version__ = "0.1.2"
__version__ = "0.1.4"
__module_name__ = "mcp_template_python"

View File

@@ -1,10 +1,19 @@
from operator import add, mul, sub, truediv
from mcp.server.fastmcp import FastMCP
from mcp.server.transport_security import TransportSecuritySettings
from mcp_template_python.config import settings
mcp = FastMCP("math", instructions=settings.mcp.instructions)
mcp = FastMCP(
"math",
instructions=settings.mcp.instructions,
transport_security=TransportSecuritySettings(
allowed_hosts=settings.cors.allow_hosts.split(","),
allowed_origins=settings.cors.allow_origins.split(","),
enable_dns_rebinding_protection=settings.mcp.enable_dns_rebinding_protection,
),
)
@mcp.tool()

View File

@@ -15,7 +15,7 @@ class AppSettings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="APP_",
extra="allow",
extra="ignore",
)
mcp: MCPSettings = MCPSettings()

View File

@@ -8,9 +8,12 @@ class CORSSettings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="CORS_",
extra="allow",
extra="ignore",
)
allow_hosts: str = "*"
"""CORS allow hosts, defaults to '*'."""
allow_origins: str = "*"
"""CORS allow origins, defaults to '*'."""

View File

@@ -8,7 +8,7 @@ class MCPSettings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="MCP_",
extra="allow",
extra="ignore",
)
default_mcp: str = "math"
@@ -25,3 +25,6 @@ class MCPSettings(BaseSettings):
enable_streamable_http: bool = True
"""Enable streamable HTTP for the MCP server."""
enable_dns_rebinding_protection: bool = True
"""Enable DNS rebinding protection for MCP server."""

255
tools/rename.py Normal file
View File

@@ -0,0 +1,255 @@
import argparse
import re
import shutil
from pathlib import Path
from typing import List
IGNORED_DIRS = {
".venv",
"venv",
".git",
"__pycache__",
".pytest_cache",
".mypy_cache",
".ruff_cache",
"node_modules",
".idea",
".vscode",
"dist",
"build",
"*.egg-info",
}
IGNORED_EXTENSIONS = {
".pyc",
".pyo",
".pyd",
".so",
".dll",
".dylib",
".exe",
".bin",
".dat",
".db",
".sqlite",
".lock",
".jpg",
".jpeg",
".png",
".gif",
".ico",
".svg",
".pdf",
".zip",
".tar",
".gz",
".bz2",
".xz",
}
TEXT_EXTENSIONS = {
".py",
".txt",
".md",
".rst",
".toml",
".yaml",
".yml",
".json",
".ini",
".cfg",
".conf",
".sh",
".bat",
".ps1",
".html",
".css",
".js",
".ts",
".xml",
".Dockerfile",
".gitignore",
".dockerignore",
"Makefile",
"Dockerfile",
}
def normalize_name(name: str) -> tuple[str, str]:
"""Normalize name to underscore and hyphen versions."""
underscore_name = name.replace("-", "_")
hyphen_name = name.replace("_", "-")
return underscore_name, hyphen_name
def should_ignore_path(path: Path, root: Path) -> bool:
try:
relative = path.relative_to(root)
parts = relative.parts
for part in parts:
for ignored in IGNORED_DIRS:
if ignored.endswith("*"):
pattern = ignored.replace("*", ".*")
if re.match(pattern, part):
return True
elif part == ignored:
return True
return False
except ValueError:
return True
def should_process_file(file_path: Path) -> bool:
if file_path.suffix in IGNORED_EXTENSIONS:
return False
if file_path.suffix in TEXT_EXTENSIONS:
return True
if not file_path.suffix and file_path.name in TEXT_EXTENSIONS:
return True
return False
def collect_paths_to_rename(
root: Path, old_underscore: str, old_hyphen: str
) -> List[Path]:
"""Collect all paths that need to be renamed."""
paths_to_rename = []
for path in root.rglob("*"):
if should_ignore_path(path, root):
continue
name = path.name
if old_underscore in name or old_hyphen in name:
paths_to_rename.append(path)
paths_to_rename.sort(key=lambda p: len(p.parts), reverse=True)
return paths_to_rename
def rename_path(
old_path: Path,
old_underscore: str,
old_hyphen: str,
new_underscore: str,
new_hyphen: str,
dry_run: bool = False,
) -> Path:
"""Rename a single path."""
old_name = old_path.name
new_name = old_name
new_name = new_name.replace(old_underscore, new_underscore)
new_name = new_name.replace(old_hyphen, new_hyphen)
if new_name != old_name:
new_path = old_path.parent / new_name
if not dry_run:
if old_path.exists() and not new_path.exists():
shutil.move(str(old_path), str(new_path))
return new_path
return old_path
def replace_in_file(
file_path: Path,
old_underscore: str,
old_hyphen: str,
new_underscore: str,
new_hyphen: str,
dry_run: bool = False,
) -> int:
"""Replace old name in file content."""
if not should_process_file(file_path):
return 0
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
except (UnicodeDecodeError, PermissionError):
return 0
new_content = content
new_content = new_content.replace(old_underscore, new_underscore)
new_content = new_content.replace(old_hyphen, new_hyphen)
changes = content.count(old_underscore) + content.count(old_hyphen)
if new_content != content and changes > 0:
if not dry_run:
with open(file_path, "w", encoding="utf-8") as f:
f.write(new_content)
return changes
return 0
def rename_project(root: Path, old_name: str, new_name: str, dry_run: bool = False):
"""Rename the entire project."""
old_underscore, old_hyphen = normalize_name(old_name)
new_underscore, new_hyphen = normalize_name(new_name)
print(f"\nRenaming project{' (dry-run)' if dry_run else ''}:")
print(f" {old_underscore} / {old_hyphen} -> {new_underscore} / {new_hyphen}")
paths_to_rename = collect_paths_to_rename(root, old_underscore, old_hyphen)
if paths_to_rename:
for path in paths_to_rename:
rename_path(
path, old_underscore, old_hyphen, new_underscore, new_hyphen, dry_run
)
total_changes = 0
for file_path in root.rglob("*"):
if not file_path.is_file():
continue
if should_ignore_path(file_path, root):
continue
changes = replace_in_file(
file_path, old_underscore, old_hyphen, new_underscore, new_hyphen, dry_run
)
if changes > 0:
total_changes += changes
print(f"\nCompleted: {total_changes} replacements")
def main():
"""Main function."""
parser = argparse.ArgumentParser(description="Rename project package name")
parser.add_argument("old_name", help="Old project name (xx_xx_xx or xx-xx-xx)")
parser.add_argument("new_name", help="New project name (yy_yy_yy or yy-yy-yy)")
parser.add_argument("--dry-run", action="store_true", help="Preview mode only")
parser.add_argument("--root", type=str, default=".", help="Project root directory")
args = parser.parse_args()
root = Path(args.root).resolve()
if not root.exists():
print(f"Error: Directory not found: {root}")
return 1
rename_project(root, args.old_name, args.new_name, args.dry_run)
return 0
if __name__ == "__main__":
exit(main())

1520
uv.lock generated

File diff suppressed because it is too large Load Diff