教學:部署 Python MCP 伺服器到 Azure 容器應用

在這個教學中,你將建立一個模型情境協定(MCP)伺服器,透過 FastAPI 和 MCP Python SDK 來暴露任務管理工具。 你要把伺服器部署到 Azure 容器應用程式,然後用 VS Code 從 GitHub Copilot Chat 連接。

在本教學課程中,您會:

  • 建立一個 FastAPI 應用程式來揭露 MCP 工具
  • 用 GitHub Copilot 在本地測試 MCP 伺服器
  • 將應用程式容器化並部署至 Azure 容器應用程式
  • 將 GitHub Copilot 連接到已部署的 MCP 伺服器

先決條件

建立應用程式架構

在本節中,你將使用 FastAPI 和 MCP Python SDK 建立一個新的 Python 專案。

  1. 建立專案目錄並建立虛擬環境:

    mkdir tasks-mcp-server && cd tasks-mcp-server
    python -m venv .venv
    source .venv/bin/activate
    
  2. 創建 requirements.txt

    fastapi>=0.115.0
    uvicorn>=0.30.0
    mcp[cli]>=1.2.0
    
  3. 安裝相依性:

    pip install -r requirements.txt
    
  4. 為記憶體內資料儲存建立 task_store.py

    from dataclasses import dataclass, field
    from datetime import datetime, timezone
    
    
    @dataclass
    class TaskItem:
        id: int
        title: str
        description: str
        is_complete: bool = False
        created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
    
        def to_dict(self) -> dict:
            return {
                "id": self.id,
                "title": self.title,
                "description": self.description,
                "is_complete": self.is_complete,
                "created_at": self.created_at.isoformat(),
            }
    
    
    class TaskStore:
        def __init__(self):
            self._tasks: list[TaskItem] = [
                TaskItem(1, "Buy groceries", "Milk, eggs, bread"),
                TaskItem(2, "Write docs", "Draft the MCP tutorial", True),
            ]
            self._next_id = 3
    
        def get_all(self) -> list[dict]:
            return [t.to_dict() for t in self._tasks]
    
        def get_by_id(self, task_id: int) -> dict | None:
            task = next((t for t in self._tasks if t.id == task_id), None)
            return task.to_dict() if task else None
    
        def create(self, title: str, description: str) -> dict:
            task = TaskItem(self._next_id, title, description)
            self._next_id += 1
            self._tasks.append(task)
            return task.to_dict()
    
        def toggle_complete(self, task_id: int) -> dict | None:
            task = next((t for t in self._tasks if t.id == task_id), None)
            if task is None:
                return None
            task.is_complete = not task.is_complete
            return task.to_dict()
    
        def delete(self, task_id: int) -> bool:
            task = next((t for t in self._tasks if t.id == task_id), None)
            if task is None:
                return False
            self._tasks.remove(task)
            return True
    
    
    # For demonstration only — not thread-safe.
    store = TaskStore()
    

    TaskItem資料類別以序列化方法定義資料模型to_dict()。 該 TaskStore 類別管理一個預先填充樣本資料的記憶體清單,並提供 CRUD 方法。 模組層級 store 的單例在整個應用程式中共享以簡化流程。

定義 MCP 工具

在本節中,你定義 AI 模型可調用的 MCP 工具,並將 MCP 伺服器掛載到 FastAPI 應用程式中。

  1. 創建 mcp_server.py

    from mcp.server.fastmcp import FastMCP
    from task_store import store
    
    mcp = FastMCP("TasksMCP", stateless_http=True)
    
    
    @mcp.tool()
    async def list_tasks() -> list[dict]:
        """List all tasks with their ID, title, description, and completion status."""
        return store.get_all()
    
    
    @mcp.tool()
    async def get_task(task_id: int) -> dict | None:
        """Get a single task by its numeric ID.
    
        Args:
            task_id: The numeric ID of the task to retrieve.
        """
        return store.get_by_id(task_id)
    
    
    @mcp.tool()
    async def create_task(title: str, description: str) -> dict:
        """Create a new task with the given title and description. Returns the created task.
    
        Args:
            title: A short title for the task.
            description: A detailed description of what the task involves.
        """
        return store.create(title, description)
    
    
    @mcp.tool()
    async def toggle_task_complete(task_id: int) -> str:
        """Toggle a task's completion status between complete and incomplete.
    
        Args:
            task_id: The numeric ID of the task to toggle.
        """
        task = store.toggle_complete(task_id)
        if task:
            status = "complete" if task["is_complete"] else "incomplete"
            return f"Task {task['id']} is now {status}."
        return f"Task with ID {task_id} not found."
    
    
    @mcp.tool()
    async def delete_task(task_id: int) -> str:
        """Delete a task by its numeric ID.
    
        Args:
            task_id: The numeric ID of the task to delete.
        """
        if store.delete(task_id):
            return f"Task {task_id} deleted."
        return f"Task with ID {task_id} not found."
    

    重點︰

    • FastMCP("TasksMCP", stateless_http=True) 在 Python SDK 中使用 Stateless HTTP 模式建立 MCP 伺服器。 可串流的 HTTP 端點預設使用 /mcp 子路徑。
    • 每個 @mcp.tool() 函式都成為可調用的工具。 docstring 和參數註解功能幫助 AI 模型了解如何使用每個工具。
  2. 創建 app.py。 此檔案定義了掛載 MCP 伺服器的 FastAPI 應用程式:

    from contextlib import AsyncExitStack, asynccontextmanager
    
    from fastapi import FastAPI
    from fastapi.responses import JSONResponse
    
    from mcp_server import mcp
    
    
    @asynccontextmanager
    async def lifespan(app: FastAPI):
        async with AsyncExitStack() as stack:
            await stack.enter_async_context(mcp.session_manager.run())
            yield
    
    
    app = FastAPI(lifespan=lifespan)
    app.mount("/", mcp.streamable_http_app())
    
    
    @app.get("/health")
    async def health():
        return JSONResponse({"status": "healthy"})
    

    MCP 伺服器應用程式會掛載在根節點(/)。 SDK 的可串流 HTTP 端點預設為 /mcp,因此完整的端點路徑為 /mcp

    容器應用程式健康探測則使用獨立 /health 端點。 MCP 端點預期會收到 JSON-RPC POST 請求,並不適合作為健康檢查。

在本機測試 MCP 伺服器

在部署到 Azure 之前,先透過本地運行 MCP 伺服器並從 GitHub Copilot 連接來確認它正常運作。

  1. 開始申請:

    uvicorn app:app --reload --port 8080
    
  2. 打開 VS Code,然後打開 Copilot 聊天 ,選擇 Agent 模式。

  3. 選擇 工具 按鈕,然後選擇 新增更多工具...>新增 MCP 伺服器

  4. 選取HTTP(HTTP 或 Server-Sent 事件)

  5. 輸入伺服器網址: http://localhost:8080/mcp

  6. 輸入伺服器 ID: tasks-mcp

  7. 選取 [工作區設定]。

  8. 在新的 Copilot 聊天提示中,輸入 :「Show me all tasks」

  9. 當 Copilot 提示確認 MCP 工具時,選擇 繼續

您應該會看到從記憶體內存放區傳回的工作清單。

小提示

試試其他提示,例如「建立任務以檢視 PR」、「標記任務 1 為完成」或「刪除任務 2」。

將應用程式容器化

把應用程式打包成 Docker 容器,這樣你可以在本地測試再部署到 Azure。

  1. 建立 Dockerfile

    FROM python:3.12-slim
    
    WORKDIR /app
    
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt
    
    COPY . .
    
    EXPOSE 8080
    CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]
    

    Dockerfile 使用 Python 3.12 slim 基礎映像,從 requirements.txt 安裝相依性,然後複製應用程式程式碼。 Uvicorn 在 8080 埠口提供 FastAPI 應用程式。

  2. 確認容器是否在本地建置並執行:

    docker build -t tasks-mcp-server .
    docker run -p 8080:8080 tasks-mcp-server
    

    確認健全狀態端點回覆:curl http://localhost:8080/health

部署至 Azure 容器應用程式

在你將應用程式容器化後,使用 Azure CLI 部署到 Azure 容器應用程式。 這個 az containerapp up 指令會在雲端建立容器映像,所以你不需要在機器上安裝 Docker。

  1. 設定環境變數:

    RESOURCE_GROUP="mcp-tutorial-rg"
    LOCATION="eastus"
    ENVIRONMENT_NAME="mcp-env"
    APP_NAME="tasks-mcp-server-py"
    
  2. 建立資源群組與容器應用程式環境:

    az group create --name $RESOURCE_GROUP --location $LOCATION
    
    az containerapp env create \
        --name $ENVIRONMENT_NAME \
        --resource-group $RESOURCE_GROUP \
        --location $LOCATION
    
  3. 部署容器應用程式:

    az containerapp up \
        --name $APP_NAME \
        --resource-group $RESOURCE_GROUP \
        --environment $ENVIRONMENT_NAME \
        --source . \
        --ingress external \
        --target-port 8080
    
  4. 設定 CORS:

    az containerapp ingress cors enable \
        --name $APP_NAME \
        --resource-group $RESOURCE_GROUP \
        --allowed-origins "*" \
        --allowed-methods "GET,POST,DELETE,OPTIONS" \
        --allowed-headers "*"
    

    備註

    在實際執行環境中,將萬用字元來源替換成特定的可信來源。 請參閱 容器應用程式上的 Secure MCP 伺服器

  5. 驗證部署:

    APP_URL=$(az containerapp show \
        --name $APP_NAME \
        --resource-group $RESOURCE_GROUP \
        --query "properties.configuration.ingress.fqdn" -o tsv)
    
    curl https://$APP_URL/health
    

將 GitHub Copilot 連接到已部署的伺服器

現在 MCP 伺服器在 Azure 運行,設定 VS Code 將 GitHub Copilot 連接到已部署的端點。

  1. 建立或更新 .vscode/mcp.json

    {
        "servers": {
            "tasks-mcp-server": {
                "type": "http",
                "url": "https://<your-app-fqdn>/mcp"
            }
        }
    }
    

    用部署輸出的 FQDN 替換 <your-app-fqdn>

  2. 在 VS Code 中,以客服模式開啟 Copilot 聊天。

  3. 驗證 tasks-mcp-server 會出現在工具清單中。 如果需要,選擇 開始

  4. 用像 「建立任務以部署預備環境」這樣的提示來測試。

為互動使用設定縮放

預設情況下,Azure 容器應用程式可以擴展到零複本。 對於服務互動客戶端如 Copilot 的 MCP 伺服器,冷啟動會造成明顯延遲。 設定最小副本數量,以維持至少一個實例在運行:

az containerapp update \
    --name $APP_NAME \
    --resource-group $RESOURCE_GROUP \
    --min-replicas 1

安全性考慮

本教學使用未認證的 MCP 伺服器以簡化操作。 在正式環境中運行 MCP 伺服器前,請先檢視以下建議。 當由大型語言模型 (LLM) 技術支援的 Agent 呼叫您的 MCP 伺服器時,請注意提示注入攻擊。

  • 認證與授權:用 Microsoft Entra ID 保護你的 MCP 伺服器。 請參閱 容器應用程式上的 Secure MCP 伺服器
  • 輸入驗證:務必驗證工具參數。 使用 Pydantic 強制工具輸入的資料驗證。
  • HTTPS:Azure 容器應用程式預設會強制執行 HTTPS,並自動使用 TLS 憑證。
  • 最低權限:只公開你使用情境所需的工具。 避免使用未經確認就進行破壞性操作的工具。
  • CORS:在執行環境中只允許受信任的網域作為來源。
  • 日誌與監控:記錄 MCP 工具的調用以進行稽核。 使用 Azure 監視器 和日誌分析。

清理資源

如果你不打算繼續使用這個應用程式,請刪除資源群組,移除你在教學中建立的所有資源:

az group delete --resource-group $RESOURCE_GROUP --yes --no-wait

下一個步驟