diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f0510ef --- /dev/null +++ b/.dockerignore @@ -0,0 +1,29 @@ +# Environment files +.env.* +!.env.example + +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv + +# Docker files +Dockerfile +docker-compose.* + +# Git files +.git/ +.gitignore + +# IDE files +.idea/ +.vscode/ + +# misc +.DS_Store diff --git a/.gitignore b/.gitignore index 505a3b1..569c07a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ +# Environment files +.env +.env.* +!.env.example + # Python-generated files __pycache__/ *.py[oc] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b05b3df --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +ARG PYPI_MIRROR_URL=https://pypi.org/simple + +FROM python:3.12-bookworm AS deps +ARG PYPI_MIRROR_URL +WORKDIR /app + +COPY pyproject.toml uv.lock .python-version ./ + +ENV UV_DEFAULT_INDEX=${PYPI_MIRROR_URL} + +# Install dependencies +RUN pip -V && \ + pip config set global.index-url ${PYPI_MIRROR_URL} && \ + pip install uv +RUN uv sync --no-dev --no-install-project + +FROM python:3.12-slim-bookworm +ARG PYPI_MIRROR_URL +WORKDIR /app + +RUN pip -V && \ + pip config set global.index-url ${PYPI_MIRROR_URL} && \ + pip install --no-cache-dir uv + +COPY --from=deps /app/.venv/ ./.venv/ +COPY . ./ + +EXPOSE 3001 +CMD [ "uv", "run", "prod", "--host", "0.0.0.0" ] diff --git a/README.md b/README.md index 28eed4a..f3e8fba 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,52 @@ # MCP FastAPI 应用模板 +本项目提供了 FastAPI 集成的 MCP 应用模板。 + - [x] 支持多 MCP 挂载 - [x] 支持命令行调用 Stdio 模式 - [x] 支持 SSE / Streamable HTTP 兼容 - [x] 支持打包分发 + +## 开始 + +安装依赖: + +```bash +uv sync +``` + +开发: + +```bash +uv run dev +``` + +可通过 访问示例 MCP 接口(Streamable HTTP),或 访问 SSE 接口。 + +## 部署 + +生产: + +```bash +uv run prod +``` + +构建 Python Wheel 包: + +```bash +uv build +``` + +## Docker 部署 + +运行: + +```bash +docker compose up -d +``` + +仅构建: + +```bash +docker compose build +``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ee68916 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +x-default: &default + restart: unless-stopped + volumes: + - &localtime /etc/localtime:/etc/localtime:ro + - &timezone /etc/timezone:/etc/timezone:ro + logging: + driver: json-file + options: + max-size: 1m + +services: + app: + <<: *default + build: + context: . + dockerfile: Dockerfile + image: ${DOCKER_REGISTRY:-docker.io}/local/mcp-template-python:${BUILD_VERSION:-latest} + ports: + - "3001:3001" diff --git a/pyproject.toml b/pyproject.toml index 783cac8..c8f4198 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,12 +9,14 @@ authors = [ requires-python = ">=3.12" dependencies = [ "fastapi[standard]>=0.115.12", - "mcp>=1.9.3", + "mcp>=1.9.4", "uvicorn[standard]>=0.34.3", ] [project.scripts] mcp-template-python = "mcp_template_python.__main__:main" +dev = "mcp_template_python.__main__:dev" +prod = "mcp_template_python.__main__:main" [build-system] requires = ["hatchling"] diff --git a/src/mcp_template_python/__about__.py b/src/mcp_template_python/__about__.py index 3f5c4a7..ada572c 100644 --- a/src/mcp_template_python/__about__.py +++ b/src/mcp_template_python/__about__.py @@ -1 +1,2 @@ __version__ = "0.1.0" +__module_name__ = "mcp_template_python" diff --git a/src/mcp_template_python/__main__.py b/src/mcp_template_python/__main__.py index 33f089a..a5e740a 100644 --- a/src/mcp_template_python/__main__.py +++ b/src/mcp_template_python/__main__.py @@ -1,43 +1,72 @@ import argparse import sys +from .__about__ import __module_name__, __version__ + def main(): - parser = argparse.ArgumentParser(description="Run a MarkItDown MCP server") + parser = argparse.ArgumentParser(description="MCP Server") parser.add_argument( - "--http", + "--stdio", action="store_true", - help="Run the server with Streamable HTTP and SSE transport rather than STDIO (default: False)", + help="Run the server with STDIO (default: False)", ) parser.add_argument( - "--host", default=None, help="Host to bind to (default: 127.0.0.1)" + "--host", + default="127.0.0.1", + help="Host to bind to (default: 127.0.0.1)", ) parser.add_argument( - "--port", type=int, default=None, help="Port to listen on (default: 3001)" + "--port", + type=int, + default=3001, + help="Port to listen on (default: 3001)", + ) + parser.add_argument( + "--dev", + default=False, + action="store_true", + help="Run the server in development mode (default: False)", + ) + parser.add_argument( + "--version", + action="version", + version=f"%(prog)s {__version__}", + help="Show the version of the MCP server", ) args = parser.parse_args() - if not args.http and (args.host or args.port): - parser.error( - "Host and port arguments are only valid when using streamable HTTP or SSE transport (see: --http)." - ) - sys.exit(1) + if args.dev: + dev(args.host, args.port) + sys.exit(0) - if args.http: + if args.stdio: + from .app.math import mcp + + mcp.run() + else: import uvicorn from .server import app uvicorn.run( app, - host=args.host or "127.0.0.1", - port=args.port or 3001, + host=args.host, + port=args.port, ) - else: - from .app.math import mcp - mcp.run() + +def dev(host: str = "127.0.0.1", port: int = 3001): + """Run the MCP server in development mode.""" + import uvicorn + + uvicorn.run( + f"{__module_name__}.server:app", + host=host, + port=port, + reload=True, + ) if __name__ == "__main__": diff --git a/src/mcp_template_python/app/math.py b/src/mcp_template_python/app/math.py index a51e5d0..32839c2 100644 --- a/src/mcp_template_python/app/math.py +++ b/src/mcp_template_python/app/math.py @@ -35,7 +35,3 @@ async def div_nums(a: float, b: float) -> float: Divides the first number by the second. """ return truediv(a, b) - - -if __name__ == "__main__": - mcp.run() diff --git a/uv.lock b/uv.lock index 33fea23..b05670d 100644 --- a/uv.lock +++ b/uv.lock @@ -27,11 +27,11 @@ wheels = [ [[package]] name = "certifi" -version = "2025.4.26" +version = "2025.6.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload_time = "2025-04-26T02:12:29.51Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload_time = "2025-06-15T02:45:51.329Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload_time = "2025-04-26T02:12:27.662Z" }, + { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload_time = "2025-06-15T02:45:49.977Z" }, ] [[package]] @@ -261,7 +261,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.9.3" +version = "1.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -274,9 +274,9 @@ dependencies = [ { name = "starlette" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f2/df/8fefc0c6c7a5c66914763e3ff3893f9a03435628f6625d5e3b0dc45d73db/mcp-1.9.3.tar.gz", hash = "sha256:587ba38448e81885e5d1b84055cfcc0ca56d35cd0c58f50941cab01109405388", size = 333045, upload_time = "2025-06-05T15:48:25.681Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f2/dc2450e566eeccf92d89a00c3e813234ad58e2ba1e31d11467a09ac4f3b9/mcp-1.9.4.tar.gz", hash = "sha256:cfb0bcd1a9535b42edaef89947b9e18a8feb49362e1cc059d6e7fc636f2cb09f", size = 333294, upload_time = "2025-06-12T08:20:30.158Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/45/823ad05504bea55cb0feb7470387f151252127ad5c72f8882e8fe6cf5c0e/mcp-1.9.3-py3-none-any.whl", hash = "sha256:69b0136d1ac9927402ed4cf221d4b8ff875e7132b0b06edd446448766f34f9b9", size = 131063, upload_time = "2025-06-05T15:48:24.171Z" }, + { url = "https://files.pythonhosted.org/packages/97/fc/80e655c955137393c443842ffcc4feccab5b12fa7cb8de9ced90f90e6998/mcp-1.9.4-py3-none-any.whl", hash = "sha256:7fcf36b62936adb8e63f89346bccca1268eeca9bf6dfb562ee10b1dfbda9dac0", size = 130232, upload_time = "2025-06-12T08:20:28.551Z" }, ] [[package]] @@ -292,7 +292,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" }, - { name = "mcp", specifier = ">=1.9.3" }, + { name = "mcp", specifier = ">=1.9.4" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.34.3" }, ] @@ -307,7 +307,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.11.5" +version = "2.11.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -315,9 +315,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102, upload_time = "2025-05-22T21:18:08.761Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload_time = "2025-06-14T08:33:17.137Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229, upload_time = "2025-05-22T21:18:06.329Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload_time = "2025-06-14T08:33:14.905Z" }, ] [[package]]