16 Commits

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
Sun-ZhenXing
0ffc5b69b4 feat: update config & cors 2025-10-02 16:20:15 +08:00
Sun-ZhenXing
477888de17 fix: docker-compose env 2025-09-24 22:27:58 +08:00
Sun-ZhenXing
8567b0804a feat: update log format 2025-09-22 17:56:39 +08:00
Sun-ZhenXing
a42e3a2954 feat: add helm/** 2025-09-18 11:31:01 +08:00
Sun-ZhenXing
177d797ad2 feat: use click 2025-09-18 11:13:23 +08:00
Sun-ZhenXing
dc8d7bb12d chore: update deps 2025-09-08 19:20:10 +08:00
Sun-ZhenXing
660f11cccb chore(deps): update & add Makefile 2025-08-28 14:41:00 +08:00
Sun-ZhenXing
79c5e87526 chore: update deps 2025-08-18 11:44:59 +08:00
Sun-ZhenXing
72388cbbf6 feat: use rootless docker image 2025-08-10 18:44:38 +08:00
Sun-ZhenXing
6a6dbe25d2 chore: update deps 2025-08-07 19:05:18 +08:00
Sun-ZhenXing
a837441bc4 chore: update deps 2025-07-25 09:28:12 +08:00
Sun-ZhenXing
7941617a8f chore: move ws support to feat-ws 2025-07-21 11:44:33 +08:00
39 changed files with 2595 additions and 787 deletions

View File

@@ -19,3 +19,6 @@ trim_trailing_whitespace = false
[Dockerfile] [Dockerfile]
indent_size = 4 indent_size = 4
[Makefile]
indent_style = tab

View File

@@ -1,2 +1,2 @@
MCP_DEFAULT_HOST=127.0.0.1 APP_DEFAULT_HOST=127.0.0.1
MCP_DEFAULT_PORT=3001 APP_DEFAULT_PORT=3001

6
.gitignore vendored
View File

@@ -1,8 +1,14 @@
# Trace files
radar.duckdb*
# Environment files # Environment files
.env .env
.env.* .env.*
!.env.example !.env.example
# Production configuration
values-production.yaml
# Python-generated files # Python-generated files
__pycache__/ __pycache__/
*.py[oc] *.py[oc]

View File

@@ -1 +1 @@
3.12 3.13

View File

@@ -2,6 +2,9 @@
"recommendations": [ "recommendations": [
"ms-python.python", "ms-python.python",
"charliermarsh.ruff", "charliermarsh.ruff",
"fill-labs.dependi" "fill-labs.dependi",
"EditorConfig.EditorConfig",
"tamasfe.even-better-toml",
"streetsidesoftware.code-spell-checker"
] ]
} }

10
.vscode/settings.json vendored
View File

@@ -2,12 +2,17 @@
"python.analysis.autoImportCompletions": true, "python.analysis.autoImportCompletions": true,
"python.analysis.typeCheckingMode": "standard", "python.analysis.typeCheckingMode": "standard",
"python.analysis.importFormat": "absolute", "python.analysis.importFormat": "absolute",
"python.testing.pytestArgs": [
"."
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"[python]": { "[python]": {
"editor.defaultFormatter": "charliermarsh.ruff", "editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": "explicit", "source.fixAll": "explicit",
"source.organizeImports": "never", "source.convertImportFormat": "never",
"source.organizeImports.ruff": "explicit" "source.organizeImports.ruff": "explicit"
} }
}, },
@@ -17,14 +22,17 @@
], ],
"files.eol": "\n", "files.eol": "\n",
"cSpell.words": [ "cSpell.words": [
"dotenv",
"fastapi", "fastapi",
"fastmcp", "fastmcp",
"localtime", "localtime",
"mcps", "mcps",
"noninteractive",
"pydantic", "pydantic",
"PYPI", "PYPI",
"pyproject", "pyproject",
"streamable", "streamable",
"trixie",
"uvicorn", "uvicorn",
"venv" "venv"
] ]

View File

@@ -1,7 +1,8 @@
ARG PYPI_MIRROR_URL=https://pypi.org/simple ARG PYPI_MIRROR_URL=https://pypi.org/simple
ARG DEBIAN_MIRROR=ftp.cn.debian.org ARG DEBIAN_MIRROR=deb.debian.org
FROM python:3.12-bookworm AS deps # Base stage
FROM python:3.13-trixie AS deps
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ARG PYPI_MIRROR_URL ARG PYPI_MIRROR_URL
WORKDIR /app WORKDIR /app
@@ -13,14 +14,22 @@ ENV UV_DEFAULT_INDEX=${PYPI_MIRROR_URL}
# Install dependencies # Install dependencies
RUN pip -V && \ RUN pip -V && \
pip config set global.index-url ${PYPI_MIRROR_URL} && \ pip config set global.index-url ${PYPI_MIRROR_URL} && \
pip install uv pip install --no-cache-dir uv
# Sync dependencies
RUN --mount=type=cache,target=/root/.cache/uv,id=uv-cache,sharing=locked \ RUN --mount=type=cache,target=/root/.cache/uv,id=uv-cache,sharing=locked \
uv sync --no-dev --no-install-project uv sync --no-dev --no-install-project
FROM python:3.12-slim-bookworm AS runner # Runner stage
FROM python:3.13-slim-trixie AS runner
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ARG DEBIAN_MIRROR ARG DEBIAN_MIRROR
ARG PYPI_MIRROR_URL ARG PYPI_MIRROR_URL
# rootless user args
ARG APP_USER=app
ARG APP_UID=1000
ARG APP_GID=1000
WORKDIR /app WORKDIR /app
RUN sed -i "s/deb.debian.org/${DEBIAN_MIRROR}/g" /etc/apt/sources.list.d/debian.sources && \ RUN sed -i "s/deb.debian.org/${DEBIAN_MIRROR}/g" /etc/apt/sources.list.d/debian.sources && \
@@ -34,17 +43,31 @@ RUN pip -V && \
pip config set global.index-url ${PYPI_MIRROR_URL} && \ pip config set global.index-url ${PYPI_MIRROR_URL} && \
pip install --no-cache-dir uv pip install --no-cache-dir uv
COPY --from=deps /app/.venv/ ./.venv/ # Create non-root user/group for rootless execution
COPY . ./ RUN groupadd -g ${APP_GID} ${APP_USER} && \
useradd -m -u ${APP_UID} -g ${APP_GID} -s /bin/bash ${APP_USER}
# Copy venv and sources with proper ownership
COPY --from=deps --chown=${APP_UID}:${APP_GID} /app/.venv/ ./.venv/
COPY --chown=${APP_UID}:${APP_GID} . ./
# Ensure dependencies sync
RUN --mount=type=cache,target=/root/.cache/uv,id=uv-cache,sharing=locked \ RUN --mount=type=cache,target=/root/.cache/uv,id=uv-cache,sharing=locked \
uv sync --no-dev uv sync --no-dev && \
chown -R ${APP_UID}:${APP_GID} /app
# Environment for venv
ENV VIRTUAL_ENV=/app/.venv
ENV PATH="${VIRTUAL_ENV}/bin:${PATH}"
ARG PORT=3001 ARG PORT=3001
ENV PORT=${PORT} ENV PORT=${PORT}
# Switch to non-root user
USER ${APP_UID}:${APP_GID}
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
CMD curl -f http://localhost:${PORT}/health || exit 1 CMD curl -f http://localhost:${PORT}/health || exit 1
EXPOSE ${PORT} 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}"]

54
Makefile Normal file
View File

@@ -0,0 +1,54 @@
.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 $(args)
dev:
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()" $(args)
build:
uv build $(args)
clean:
rm -rf .venv .ruff_cache dist/ build/ *.egg-info $(args)
update:
uv sync --all-extras --all-packages -U $(args)
lint:
uv run ruff check . $(args)
docker-build:
docker compose build $(args)
docker-run:
docker compose up -d $(args)
helm-lint:
helm lint helm/mcp-template-python $(args)
helm-install:
helm install mcp-template-python helm/mcp-template-python $(args)
helm-upgrade:
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 $(args)
helm-upgrade-prod:
helm upgrade mcp-template-python helm/mcp-template-python -f values-production.yaml $(args)
helm-uninstall:
helm uninstall mcp-template-python $(args)
rename:
uv run python tools/rename.py $(args)
%:
@true

View File

@@ -6,26 +6,21 @@ This project provides an MCP application template integrated with FastAPI.
- [x] Support for multiple MCP mounting - [x] Support for multiple MCP mounting
- [x] Support for command-line invocation in Stdio mode - [x] Support for command-line invocation in Stdio mode
- [x] Support for SSE / StreamableHTTP / WebSocket - [x] Support for SSE / StreamableHTTP
- [x] Support for packaging and distribution - [x] Support for packaging and distribution
Starting from v0.1.2, we use `BetterFastMCP` to replace `FastMCP`, providing more comprehensive features than the official `FastMCP`:
- [x] Support for Pydantic models as input parameters, enabling more complex input parameter types and convenient description addition
- [x] Support for WebSocket as transport layer, access by `/{mcp_name}/websocket/ws`
## Getting Started ## Getting Started
Install dependencies: Install dependencies:
```bash ```bash
uv sync make
``` ```
Development: Development:
```bash ```bash
uv run dev make dev
``` ```
You can access the example MCP interface (Streamable HTTP) via <http://127.0.0.1:3001/math/mcp>, or access the SSE interface via <http://127.0.0.1:3001/math/compatible/sse>. You can access the example MCP interface (Streamable HTTP) via <http://127.0.0.1:3001/math/mcp>, or access the SSE interface via <http://127.0.0.1:3001/math/compatible/sse>.
@@ -33,7 +28,7 @@ You can access the example MCP interface (Streamable HTTP) via <http://127.0.0.1
Call via command line with `--stdio`: Call via command line with `--stdio`:
```bash ```bash
uv run prod --stdio make prod -- --stdio
``` ```
## Deployment ## Deployment
@@ -41,13 +36,13 @@ uv run prod --stdio
Production: Production:
```bash ```bash
uv run --no-sync prod make prod
``` ```
Build Python Wheel package: Build Python Wheel package:
```bash ```bash
uv build make build
``` ```
## Docker Deployment ## Docker Deployment

View File

@@ -6,26 +6,21 @@
- [x] 支持多 MCP 挂载 - [x] 支持多 MCP 挂载
- [x] 支持命令行调用 Stdio 模式 - [x] 支持命令行调用 Stdio 模式
- [x] 支持 SSE / StreamableHTTP / WebSocket 兼容 - [x] 支持 SSE / StreamableHTTP
- [x] 支持打包分发 - [x] 支持打包分发
从 v0.1.2 开始,我们使用 `BetterFastMCP` 替换 `FastMCP`,提供比官方 `FastMCP` 更完善的功能:
- [x] 支持入参为 Pydantic 模型,以便支持更复杂的输入参数类型并方便添加描述
- [x] 支持 WebSocket 作为传输层,通过 `/{mcp_name}/websocket/ws` 访问
## 开始 ## 开始
安装依赖: 安装依赖:
```bash ```bash
uv sync make
``` ```
开发: 开发:
```bash ```bash
uv run dev make dev
``` ```
可通过 <http://127.0.0.1:3001/math/mcp> 访问示例 MCP 接口Streamable HTTP<http://127.0.0.1:3001/math/compatible/sse> 访问 SSE 接口。 可通过 <http://127.0.0.1:3001/math/mcp> 访问示例 MCP 接口Streamable HTTP<http://127.0.0.1:3001/math/compatible/sse> 访问 SSE 接口。
@@ -33,7 +28,7 @@ uv run dev
通过 `--stdio` 来调用命令行: 通过 `--stdio` 来调用命令行:
```bash ```bash
uv run prod --stdio make prod -- --stdio
``` ```
## 部署 ## 部署
@@ -41,13 +36,13 @@ uv run prod --stdio
生产: 生产:
```bash ```bash
uv run --no-sync prod make prod
``` ```
构建 Python Wheel 包: 构建 Python Wheel 包:
```bash ```bash
uv build make build
``` ```
## Docker 部署 ## Docker 部署

View File

@@ -6,7 +6,7 @@ x-default: &default
logging: logging:
driver: json-file driver: json-file
options: options:
max-size: 1m max-size: 100m
services: services:
app: app:
@@ -16,8 +16,16 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
args: args:
- PORT=${PORT:-3001} - PORT=${PORT:-3001}
image: ${DOCKER_REGISTRY:-docker.io}/local/mcp-template-python:${BUILD_VERSION:-latest} image: ${DOCKER_REGISTRY:-docker.io}/${SERVICE_NAME:-mcp-template-python}:${BUILD_VERSION:-latest}
ports: ports:
- "${EXPOSE_PORT:-3001}:${PORT:-3001}" - "${MCP_PORT_OVERRIDE:-3001}:${PORT:-3001}"
env_file: env_file:
- .env - .env
deploy:
resources:
limits:
cpus: '0.50'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M

View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@@ -0,0 +1,24 @@
apiVersion: v2
name: mcp-template-python
description: MCP Template Python - A Model Context Protocol server template
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "latest"

View File

@@ -0,0 +1,232 @@
# MCP Template Python - Helm Chart Configuration Guide
## Quick Start
### 1. Basic Deployment
```bash
# Validate configuration
make helm-lint
# Development environment deployment
make helm-install
# Update deployment
make helm-upgrade
# Uninstall
make helm-uninstall
```
### 2. Production Environment Deployment
```bash
# Initial deployment
make helm-install-prod
# Update deployment
make helm-upgrade-prod
```
## Core Configuration
### Image Configuration
```yaml
image:
repository: docker.io/mcp-template-python
tag: "latest"
pullPolicy: IfNotPresent
```
### Service Configuration
```yaml
service:
type: ClusterIP
port: 3001
targetPort: 3001
```
### Environment Variables
```yaml
env:
MCP_DEFAULT_HOST: "0.0.0.0"
MCP_DEFAULT_PORT: "3001"
```
### Health Checks
```yaml
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 5
```
## Production Environment Configuration
### 1. Resource Configuration (Required)
```yaml
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
```
### 2. Image Registry
```yaml
image:
repository: your-registry.com/mcp-template-python
tag: "v1.0.0"
```
### 3. Auto Scaling
```yaml
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 70
```
### 4. Ingress Configuration
```yaml
ingress:
enabled: true
className: "nginx"
hosts:
- host: mcp-template-python.your-domain.com
paths:
- path: /
pathType: ImplementationSpecific
```
## Configuration File Management
### ConfigMap Configuration
```yaml
configMap:
enabled: true
data:
app.conf: |
# Application configuration
log_level=info
debug=false
```
### Mounting Configuration Files
Add to `values.yaml`:
```yaml
volumeMounts:
- name: config
mountPath: /app/config
readOnly: true
volumes:
- name: config
configMap:
name: mcp-template-python-config
```
## Security Configuration
Application runs as non-root user:
```yaml
securityContext:
runAsNonRoot: true
runAsUser: 1000
capabilities:
drop:
- ALL
```
## Pre-Deployment Checklist
### Development Environment
- [ ] Update image repository in `values.yaml`
- [ ] Confirm port configuration
- [ ] Set environment variables
### Production Environment
- [ ] Update image tag in `values-production.yaml`
- [ ] Set resource limits and requests
- [ ] Configure domain name and TLS
- [ ] Confirm replica count
- [ ] Check health check paths
## Common Commands
```bash
# Check deployment status
kubectl get pods -l app.kubernetes.io/name=mcp-template-python
# Check service
kubectl get svc mcp-template-python
# View logs
kubectl logs -l app.kubernetes.io/name=mcp-template-python
# Enter container
kubectl exec -it deployment/mcp-template-python -- /bin/bash
# View configuration
helm get values mcp-template-python
```
## Troubleshooting
### Common Issues
1. **Pod Startup Failure**
- Check if image exists
- Confirm resource configuration is reasonable
- View Pod events: `kubectl describe pod <pod-name>`
2. **Health Check Failure**
- Confirm application provides `/health` endpoint
- Check port configuration is correct
- Adjust probe delay times
3. **Service Inaccessible**
- Confirm Service configuration
- Check network policies
- Verify Ingress configuration
### Getting Help
```bash
# View Chart information
helm show chart helm/mcp-template-python
# View all configuration options
helm show values helm/mcp-template-python
# Validate template rendering
helm template mcp-template-python helm/mcp-template-python
```
---
**Note**: Before production deployment, make sure to update the resource configuration and domain settings in `values-production.yaml`.

View File

@@ -0,0 +1,22 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "mcp-template-python.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "mcp-template-python.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "mcp-template-python.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "mcp-template-python.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

View File

@@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "mcp-template-python.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "mcp-template-python.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "mcp-template-python.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "mcp-template-python.labels" -}}
helm.sh/chart: {{ include "mcp-template-python.chart" . }}
{{ include "mcp-template-python.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "mcp-template-python.selectorLabels" -}}
app.kubernetes.io/name: {{ include "mcp-template-python.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "mcp-template-python.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "mcp-template-python.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,12 @@
{{- if .Values.configMap.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "mcp-template-python.fullname" . }}-config
labels:
{{- include "mcp-template-python.labels" . | nindent 4 }}
data:
{{- with .Values.configMap.data }}
{{- toYaml . | nindent 2 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,83 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mcp-template-python.fullname" . }}
labels:
{{- include "mcp-template-python.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "mcp-template-python.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "mcp-template-python.labels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "mcp-template-python.serviceAccountName" . }}
{{- with .Values.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
{{- with .Values.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
env:
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- with .Values.livenessProbe }}
livenessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.readinessProbe }}
readinessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.volumeMounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.volumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,32 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "mcp-template-python.fullname" . }}
labels:
{{- include "mcp-template-python.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "mcp-template-python.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,43 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "mcp-template-python.fullname" . }}
labels:
{{- include "mcp-template-python.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- with .Values.ingress.className }}
ingressClassName: {{ . }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- with .pathType }}
pathType: {{ . }}
{{- end }}
backend:
service:
name: {{ include "mcp-template-python.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "mcp-template-python.fullname" . }}
labels:
{{- include "mcp-template-python.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: {{ .Values.service.targetPort | default .Values.service.port }}
protocol: TCP
name: http
selector:
{{- include "mcp-template-python.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,13 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "mcp-template-python.serviceAccountName" . }}
labels:
{{- include "mcp-template-python.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "mcp-template-python.fullname" . }}-test-connection"
labels:
{{- include "mcp-template-python.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "mcp-template-python.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View File

@@ -0,0 +1,145 @@
# Default values for mcp-template-python.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
replicaCount: 1
# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/
image:
repository: docker.io/mcp-template-python
# This sets the pull policy for images.
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
imagePullSecrets: []
# This is to override the chart name.
nameOverride: ""
fullnameOverride: ""
# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
serviceAccount:
# Specifies whether a service account should be created
create: true
# Automatically mount a ServiceAccount's API credentials?
automount: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
# This is for setting Kubernetes Annotations to a Pod.
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
podAnnotations: {}
# This is for setting Kubernetes Labels to a Pod.
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
podLabels: {}
podSecurityContext:
fsGroup: 1000
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
runAsNonRoot: true
runAsUser: 1000
# Environment variables
env:
# From .env.example
MCP_DEFAULT_HOST: "0.0.0.0"
MCP_DEFAULT_PORT: "3001"
# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/
service:
# This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types
type: ClusterIP
# This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports
port: 3001
targetPort: 3001
# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/
ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
# Additional volumes on the output Deployment definition.
volumes: []
# - name: foo
# secret:
# secretName: mysecret
# optional: false
# Additional volumeMounts on the output Deployment definition.
volumeMounts: []
# - name: foo
# mountPath: "/etc/foo"
# readOnly: true
nodeSelector: {}
tolerations: []
affinity: {}
# ConfigMap configuration
configMap:
enabled: true
data: {}
# Add any configuration files here

View File

@@ -1,23 +1,21 @@
[project] [project]
name = "mcp-template-python" name = "mcp-template-python"
version = "0.1.2" version = "0.1.4"
description = "Add your description here" description = "MCP Template for Python Projects"
readme = "README.md" readme = "README.md"
authors = [ authors = []
{ name = "Sun-ZhenXing", email = "1006925066@qq.com" } requires-python = ">=3.10"
]
requires-python = ">=3.12"
dependencies = [ dependencies = [
"fastapi[standard]>=0.116.1", "click>=8.0.0",
"mcp[cli]>=1.12.0", "fastapi[standard]>=0.115.0",
"pydantic-settings>=2.10.1", "mcp[cli]>=1.10.0",
"rich>=14.0.0", "pydantic>=2.11.7",
"pydantic-settings>=2.5.0",
"rich-toolkit>=0.15.1",
] ]
[project.scripts] [project.scripts]
mcp-template-python = "mcp_template_python.__main__:main" mcp-template-python = "mcp_template_python.__main__:main"
dev = "mcp_template_python.__main__:dev"
prod = "mcp_template_python.__main__:main"
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
@@ -28,3 +26,14 @@ path = "src/mcp_template_python/__about__.py"
[tool.hatch.build.targets.sdist] [tool.hatch.build.targets.sdist]
only-include = ["src/mcp_template_python"] only-include = ["src/mcp_template_python"]
[tool.ruff.lint]
select = ["E4", "E7", "E9", "F", "TID"]
[tool.ruff.lint.flake8-tidy-imports]
ban-relative-imports = "parents"
[dependency-groups]
dev = [
"ruff>=0.12.7",
]

View File

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

View File

@@ -1,88 +1,95 @@
import argparse
import sys import sys
import click
from .__about__ import __module_name__, __version__ from .__about__ import __module_name__, __version__
from .app import MCP_MAP
from .config import settings from .config import settings
def main(): def run_server(
from .app import MCP_MAP module: str | None = None,
host: str = settings.default_host,
port: int = settings.default_port,
reload: bool = False,
**kwargs,
):
"""Run the MCP server in development mode."""
import uvicorn
parser = argparse.ArgumentParser(description="MCP Server") if module is None:
module = f"{__module_name__}.server:app"
parser.add_argument( uvicorn.run(
module,
host=host,
port=port,
reload=reload,
log_config=None,
**kwargs,
)
@click.command()
@click.option(
"--stdio", "--stdio",
action="store_true", is_flag=True,
help="Run the server with STDIO (default: False)", help="Run the server with STDIO (default: False)",
) )
parser.add_argument( @click.option(
"--mcp", "--mcp",
type=str, type=click.Choice(list(MCP_MAP.keys()), case_sensitive=False),
default=settings.default_mcp, default=settings.mcp.default_mcp,
choices=list(MCP_MAP.keys()), help=f"Select the MCP to run in STDIO mode (default: {settings.mcp.default_mcp})",
help=f"Select the MCP to run in STDIO mode (default: {settings.default_mcp})",
) )
parser.add_argument( @click.option(
"--host", "--host",
default=settings.default_host, default=settings.default_host,
help=f"Host to bind to (default: {settings.default_host})", help=f"Host to bind to (default: {settings.default_host})",
) )
parser.add_argument( @click.option(
"--port", "--port",
type=int, type=int,
default=settings.default_port, default=settings.default_port,
help=f"Port to listen on (default: {settings.default_port})", help=f"Port to listen on (default: {settings.default_port})",
) )
parser.add_argument( @click.option(
"--dev", "--dev",
default=False, is_flag=True,
action="store_true",
help="Run the server in development mode (default: False)", help="Run the server in development mode (default: False)",
) )
parser.add_argument( @click.version_option(
"--version", version=__version__,
action="version", prog_name="mcp-template-python",
version=f"%(prog)s {__version__}",
help="Show the version of the MCP server", help="Show the version of the MCP server",
) )
args = parser.parse_args() def main(
stdio: bool,
if args.dev: mcp: str,
dev(args.host, args.port) host: str,
sys.exit(0) port: int,
dev: bool,
if args.stdio:
mcp = MCP_MAP.get(args.mcp)
if mcp is None:
print(f"Error: MCP '{args.mcp}' not found.")
sys.exit(1)
mcp.run()
else:
import uvicorn
from .server import app
uvicorn.run(
app,
host=args.host,
port=args.port,
)
def dev(
host: str = settings.default_host,
port: int = settings.default_port,
): ):
"""Run the MCP server in development mode.""" """MCP Server"""
import uvicorn if stdio:
selected_mcp = MCP_MAP.get(mcp)
uvicorn.run( if selected_mcp is None:
f"{__module_name__}.server:app", click.echo(f"Error: MCP '{mcp}' not found.", err=True)
sys.exit(1)
selected_mcp.run()
else:
run_server(
host=host, host=host,
port=port, port=port,
reload=True, reload=dev,
) )
@click.command()
def dev():
"""Run the server in development mode."""
run_server(reload=True)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -1,9 +1,19 @@
from operator import add, mul, sub, truediv from operator import add, mul, sub, truediv
from ..config import settings from mcp.server.fastmcp import FastMCP
from ..lib.better_mcp import BetterFastMCP from mcp.server.transport_security import TransportSecuritySettings
mcp = BetterFastMCP("math", settings=settings.instructions) from mcp_template_python.config import settings
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() @mcp.tool()

View File

@@ -1,50 +0,0 @@
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""
Configuration settings for the MCP template application.
"""
model_config = SettingsConfigDict(
env_prefix="MCP_",
env_file=".env",
env_file_encoding="utf-8",
extra="allow",
)
app_title: str = "MCP Template Application"
"""Title of the MCP application, defaults to 'MCP Template Application'."""
app_description: str = "A template application for MCP using FastAPI."
"""Description of the MCP application, defaults to 'A template application for MCP using FastAPI.'"""
default_mcp: str = "math"
"""Default MCP to be used by the application."""
default_host: str = "127.0.0.1"
"""Default host for the MCP server, defaults to 127.0.0.1."""
default_port: int = 3001
"""Default port for the MCP server, defaults to 3001."""
instructions: str | None = None
"""Instructions to be used by the MCP server, defaults to None."""
enable_helpers_router: bool = True
"""Enable the helpers router for the MCP server."""
enable_sse: bool = True
"""Enable Server-Sent Events (SSE) for the MCP server."""
enable_streamable_http: bool = True
"""Enable streamable HTTP for the MCP server."""
enable_websocket: bool = False
"""Enable WebSocket for the MCP server."""
websocket_path: str = "/ws"
"""Path for the WebSocket endpoint."""
settings = Settings()

View File

@@ -0,0 +1,3 @@
from .app import settings
__all__ = ["settings"]

View File

@@ -0,0 +1,46 @@
from dotenv import load_dotenv
from pydantic_settings import BaseSettings, SettingsConfigDict
from mcp_template_python.config.cors import CORSSettings
from .mcp import MCPSettings
load_dotenv()
class AppSettings(BaseSettings):
"""
Configuration settings for the MCP template application.
"""
model_config = SettingsConfigDict(
env_prefix="APP_",
extra="ignore",
)
mcp: MCPSettings = MCPSettings()
"""MCP settings, defaults to MCPSettings()."""
cors: CORSSettings = CORSSettings()
"""CORS settings, defaults to CORSSettings()."""
title: str = "MCP Template Application"
"""Title of the MCP application, defaults to 'MCP Template Application'."""
description: str = "A template application for MCP using FastAPI."
"""Description of the MCP application, defaults to 'A template application for MCP using FastAPI.'"""
default_host: str = "127.0.0.1"
"""Default host for the MCP server, defaults to 127.0.0.1."""
default_port: int = 3001
"""Default port for the MCP server, defaults to 3001."""
log_level: str = "INFO"
"""Logging level for the MCP server, defaults to 'info'."""
rich_console: bool = False
"""Enable rich console output, defaults to False."""
settings = AppSettings()

View File

@@ -0,0 +1,27 @@
from pydantic_settings import BaseSettings, SettingsConfigDict
class CORSSettings(BaseSettings):
"""
Configuration settings for CORS (Cross-Origin Resource Sharing).
"""
model_config = SettingsConfigDict(
env_prefix="CORS_",
extra="ignore",
)
allow_hosts: str = "*"
"""CORS allow hosts, defaults to '*'."""
allow_origins: str = "*"
"""CORS allow origins, defaults to '*'."""
allow_credentials: bool = True
"""CORS allow credentials, defaults to True."""
allow_methods: str = "*"
"""CORS allow methods, defaults to '*'."""
allow_headers: str = "*"
"""CORS allow headers, defaults to '*'."""

View File

@@ -0,0 +1,30 @@
from pydantic_settings import BaseSettings, SettingsConfigDict
class MCPSettings(BaseSettings):
"""
Configuration settings for the MCP template application.
"""
model_config = SettingsConfigDict(
env_prefix="MCP_",
extra="ignore",
)
default_mcp: str = "math"
"""Default MCP to be used by the application."""
instructions: str | None = None
"""Instructions to be used by the MCP server, defaults to None."""
enable_helpers_router: bool = True
"""Enable the helpers router for the MCP server."""
enable_sse: bool = True
"""Enable Server-Sent Events (SSE) for the MCP server."""
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."""

View File

@@ -1,159 +0,0 @@
import logging
from typing import Literal
from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
from mcp.server.auth.middleware.bearer_auth import (
BearerAuthBackend,
RequireAuthMiddleware,
)
from mcp.server.fastmcp import FastMCP
from mcp.server.websocket import websocket_server
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.routing import Mount, Route
from starlette.websockets import WebSocket
from ..config import settings
logger = logging.getLogger(__name__)
class BetterFastMCP(FastMCP):
def run(
self,
transport: Literal["stdio", "sse", "streamable-http", "ws"] = "stdio",
mount_path: str | None = None,
) -> None:
import anyio
if transport == "ws":
anyio.run(self.run_ws_async)
else:
super().run(transport=transport, mount_path=mount_path)
async def run_ws_async(self) -> None:
"""Run the server using WebSocket transport."""
import uvicorn
starlette_app = self.ws_app()
config = uvicorn.Config(
app=starlette_app,
host=self.settings.host,
port=self.settings.port,
log_level=self.settings.log_level.lower(),
)
server = uvicorn.Server(config)
await server.serve()
def ws_app(self) -> Starlette:
"""Return an instance of the Websocket server app."""
async def handle_ws(websocket: WebSocket):
async with websocket_server(
websocket.scope, websocket.receive, websocket.send
) as (ws_read_stream, ws_write_stream):
await self._mcp_server.run(
ws_read_stream,
ws_write_stream,
self._mcp_server.create_initialization_options(),
raise_exceptions=self.settings.debug,
)
# Create routes
routes: list[Route | Mount] = []
middleware: list[Middleware] = []
required_scopes = []
# Set up auth if configured
if self.settings.auth:
required_scopes = self.settings.auth.required_scopes or []
# Add auth middleware if token verifier is available
if self._token_verifier:
middleware = [
Middleware(
AuthenticationMiddleware,
backend=BearerAuthBackend(self._token_verifier),
),
Middleware(AuthContextMiddleware),
]
# Add auth endpoints if auth server provider is configured
if self._auth_server_provider:
from mcp.server.auth.routes import create_auth_routes
routes.extend(
create_auth_routes(
provider=self._auth_server_provider,
issuer_url=self.settings.auth.issuer_url,
service_documentation_url=self.settings.auth.service_documentation_url,
client_registration_options=self.settings.auth.client_registration_options,
revocation_options=self.settings.auth.revocation_options,
)
)
# Set up routes with or without auth
if self._token_verifier:
# Determine resource metadata URL
resource_metadata_url = None
if self.settings.auth and self.settings.auth.resource_server_url:
from pydantic import AnyHttpUrl
resource_metadata_url = AnyHttpUrl(
str(self.settings.auth.resource_server_url).rstrip("/")
+ "/.well-known/oauth-protected-resource"
)
routes.append(
Route(
settings.websocket_path,
endpoint=RequireAuthMiddleware(
handle_ws, required_scopes, resource_metadata_url
),
)
)
else:
# Auth is disabled, no wrapper needed
routes.append(
Route(
settings.websocket_path,
endpoint=handle_ws,
)
)
# Add protected resource metadata endpoint if configured as RS
if self.settings.auth and self.settings.auth.resource_server_url:
from mcp.server.auth.handlers.metadata import (
ProtectedResourceMetadataHandler,
)
from mcp.server.auth.routes import cors_middleware
from mcp.shared.auth import ProtectedResourceMetadata
protected_resource_metadata = ProtectedResourceMetadata(
resource=self.settings.auth.resource_server_url,
authorization_servers=[self.settings.auth.issuer_url],
scopes_supported=self.settings.auth.required_scopes,
)
routes.append(
Route(
"/.well-known/oauth-protected-resource",
endpoint=cors_middleware(
ProtectedResourceMetadataHandler(
protected_resource_metadata
).handle,
["GET", "OPTIONS"],
),
methods=["GET", "OPTIONS"],
)
)
routes.extend(self._custom_starlette_routes)
return Starlette(
debug=self.settings.debug,
routes=routes,
middleware=middleware,
lifespan=lambda app: self.session_manager.run(),
)

View File

@@ -1,7 +1,7 @@
from fastapi import APIRouter from fastapi import APIRouter
from ..app import MCP_MAP from mcp_template_python.app import MCP_MAP
from ..models.helpers import ArgumentsRequest from mcp_template_python.models.helpers import ArgumentsRequest
router = APIRouter(prefix="/v1", tags=["helpers"]) router = APIRouter(prefix="/v1", tags=["helpers"])

View File

@@ -1,6 +1,7 @@
import contextlib import contextlib
from fastapi import FastAPI from fastapi import FastAPI, Response
from fastapi.middleware.cors import CORSMiddleware
from .__about__ import __version__ from .__about__ import __version__
from .app import MCP_MAP from .app import MCP_MAP
@@ -17,20 +18,25 @@ async def lifespan(app: FastAPI):
app = FastAPI( app = FastAPI(
title=settings.app_title, title=settings.title,
description=settings.app_description, description=settings.description,
version=__version__, version=__version__,
lifespan=lifespan, lifespan=lifespan,
) )
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors.allow_origins.split(","),
allow_credentials=settings.cors.allow_credentials,
allow_methods=settings.cors.allow_methods.split(","),
allow_headers=settings.cors.allow_headers.split(","),
)
@app.get("/") @app.get("/")
async def root(): async def root():
"""Root endpoint.""" """Root endpoint."""
return { return Response("<script>location.href='/docs'</script>")
"message": "Welcome!",
"tools": list(MCP_MAP.keys()),
}
@app.get("/health") @app.get("/health")
@@ -41,13 +47,11 @@ async def health():
} }
if settings.enable_helpers_router: if settings.mcp.enable_helpers_router:
app.include_router(helpers_router) app.include_router(helpers_router)
for name, mcp in MCP_MAP.items(): for name, mcp in MCP_MAP.items():
if settings.enable_sse: if settings.mcp.enable_sse:
app.mount(f"/{name}/compatible", mcp.sse_app()) app.mount(f"/{name}/compatible", mcp.sse_app())
if settings.enable_streamable_http: if settings.mcp.enable_streamable_http:
app.mount(f"/{name}", mcp.streamable_http_app()) app.mount(f"/{name}", mcp.streamable_http_app())
if settings.enable_websocket:
app.mount(f"/{name}/websocket", mcp.ws_app())

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())

1694
uv.lock generated

File diff suppressed because it is too large Load Diff