Python + OIDC PyPI Publishing + CI/CD.
Build your MCP server. One-click publish. Zero secrets needed.
English | 한국어
Part of Starter Series — Stop explaining CI/CD to your AI every time. Clone and start.
Docker Deploy · Discord Bot · Telegram Bot · Browser Extension · Electron App · npm Package · React Native · VS Code Extension · MCP Server (TS) · MCP Server (Python) · Cloudflare Pages
- MCP SDK —
mcp(FastMCP) with stdio transport - Python 3.11+ — Type hints, async/await, hatchling build
- All three MCP primitives — Tools, Resources, and Prompts with working examples
- Safety Annotations — readOnly/destructive/idempotent hints on every tool
- Validated Prompts — pydantic
@validate_callrejects bad args before the handler runs - Response Helpers —
ok()anderr()for consistent tool responses - Config — Environment variable parsing pattern
- CI — gitleaks, ruff, license compliance, pytest (3.11/3.12/3.13)
- CD — OIDC trusted publishing to PyPI (zero secrets needed)
- Dependabot — Automated dependency + GitHub Actions updates
Via create-starter (recommended):
npx @starter-series/create my-mcp-server --template mcp-server-python
cd my-mcp-server
python -m venv .venv && source .venv/bin/activate
pip install -e '.[dev]'Or clone directly:
git clone https://github.com/starter-series/python-mcp-server-starter my-mcp-server
cd my-mcp-server
python -m venv .venv && source .venv/bin/activate
pip install -e '.[dev]'Tool names must be globally unique across all MCP servers a client connects to. Prefix with your module name (e.g.,
mymodule_actioninstead ofaction).
Add directly to src/my_mcp_server/server.py:
@mcp.tool(
annotations=ToolAnnotations(
readOnlyHint=True,
destructiveHint=False,
idempotentHint=True,
openWorldHint=False,
),
)
async def your_tool(input: str) -> str:
"""What your tool does.
Args:
input: Input parameter.
"""
return f"Processed: {input}"Create src/my_mcp_server/tools/your_tool.py:
from mcp.server.fastmcp import FastMCP
from mcp.types import ToolAnnotations
def register(mcp: FastMCP) -> None:
@mcp.tool(
annotations=ToolAnnotations(
readOnlyHint=True,
destructiveHint=False,
idempotentHint=True,
),
)
async def your_tool(input: str) -> str:
"""What your tool does."""
return f"Processed: {input}"Then in server.py:
from my_mcp_server.tools.your_tool import register
register(mcp)Resources expose read-only data to the client at a stable URI (contrast with Tools, which perform actions).
See src/my_mcp_server/resources/server_info.py for the example. Pattern:
from mcp.server.fastmcp import FastMCP
def register(mcp: FastMCP) -> None:
@mcp.resource(
"info://your/resource",
name="your-resource",
description="What this resource exposes.",
mime_type="application/json",
)
async def your_resource() -> str:
return "..." # str, bytes, or JSON-serializable objectThen in server.py:
from my_mcp_server.resources.your_resource import register as register_your_resource
register_your_resource(mcp)Prompts are reusable, parameterized message templates. Arguments are validated via pydantic before the handler runs.
See src/my_mcp_server/prompts/code_review.py for the example. Pattern:
from typing import Annotated, Literal
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.prompts.base import UserMessage
from pydantic import Field, validate_call
@validate_call
def your_prompt(
mode: Literal["short", "long"],
topic: Annotated[str, Field(min_length=1)],
) -> list[UserMessage]:
return [UserMessage(content=f"Write a {mode} note about {topic}.")]
def register(mcp: FastMCP) -> None:
mcp.prompt(name="your-prompt", title="Your Prompt")(your_prompt)Environment variables:
| Variable | Default | Description |
|---|---|---|
MCP_DEBUG |
false |
Enable debug logging |
LOG_LEVEL |
INFO |
Log level (DEBUG/INFO/WARNING/ERROR) |
Add your own in server.py.
# Run tests
pytest -v
# Lint
ruff check .
# Run the server (stdio)
python -m my_mcp_server| Check | Tool |
|---|---|
| Secret scanning | gitleaks |
| Large file detection | find (>5 MB) |
| License compliance | pip-licenses (blocks GPL/AGPL) |
| Lint + format | ruff |
| Tests | pytest (Python 3.11, 3.12, 3.13) |
- Bump version in
pyproject.toml - Go to Actions → Publish to PyPI → Run workflow
- OIDC handles auth — no
PYPI_TOKENsecret needed
Setup: PyPI OIDC trusted publishing docs
src/my_mcp_server/
├── __init__.py # Version
├── __main__.py # python -m entry point
├── server.py # FastMCP server + inline tools + helpers
├── tools/
│ ├── __init__.py
│ └── greet.py # Example modular tool
├── resources/
│ ├── __init__.py
│ └── server_info.py # Example resource (info://server/status)
└── prompts/
├── __init__.py
└── code_review.py # Example prompt (validated args)
tests/
├── test_tools.py # Tool tests
├── test_server_info.py # Resource tests
└── test_code_review.py # Prompt tests
.github/
├── workflows/
│ ├── ci.yml # Lint, test, security
│ ├── cd.yml # PyPI OIDC publish
│ ├── stale.yml # Stale issue management
│ └── maintenance.yml # Weekly health check
└── dependabot.yml # Dependency updates
pip install -e ".[dev]" # Install with dev deps
python -m my_mcp_server # Run server
pytest -v # Run tests
ruff check . # Lint
ruff format . # FormatMIT