共用方式為


使用 Azure Container Apps 建置 TypeScript MCP 伺服器

本文說明如何使用 Node.js 和 TypeScript 建置模型內容通訊協定 (MCP) 伺服器。 伺服器會在無伺服器環境中執行工具和服務。 使用此結構作為建立自定義 MCP 伺服器的起點。

取得程序代碼

探索 TypeScript 遠端模型內容通訊協定 (MCP) 伺服器 範例。 它示範如何使用 Node.js 和 TypeScript 來建置遠端 MCP 伺服器,並將其部署至 Azure Container Apps。

跳至 程式碼逐步解說區段 ,以瞭解此範例的運作方式。

架構概觀

下圖顯示範例應用程式的簡單架構: 此圖表顯示從裝載代理程式和 MCP 用戶端至 MCP 伺服器的 Visual Studio Code 架構。

MCP 伺服器會在 Azure Container Apps (ACA) 上以容器化應用程式的形式執行。 它會使用 Node.js/TypeScript 後端,透過模型內容通訊協定將工具提供給 MCP 用戶端。 所有工具都會使用後端 SQLite 資料庫。

費用

為了降低成本,此範例會針對大部分資源使用基本或耗用量定價層。 視需要調整階層,並在完成時刪除資源,以避免產生費用。

先決條件

  1. Visual Studio Code - 支援 MCP 伺服器開發的最新版本。
  2. GitHub Copilot Visual Studio Code 擴充功能
  3. GitHub Copilot Chat Visual Studio Code 擴充功能
  4. Azure 開發人員命令列介面 (azd)

開發容器包含本文所需的所有相依性。 您可以在 GitHub Codespaces(瀏覽器中)或使用 Visual Studio Code 在本機執行它。

若要遵循本文,請確定您符合下列必要條件:

開放的開發環境

請遵循下列步驟來設定具有所有必要相依性的預先設定開發環境。

GitHub Codespaces 會以 Visual Studio Code for Web 作為介面來執行由 GitHub 所管理的開發容器。 使用 GitHub Codespaces 進行最簡單的設定,因為它隨附為本文特別預安裝的必要工具和依賴項。

這很重要

所有 GitHub 帳戶每月都可以免費使用 Codespaces 長達 60 小時,並使用兩個核心實例。 如需詳細資訊,請參閱 GitHub Codespaces 每月包含的儲存空間和核心小時數

使用下列步驟,在 GitHub 存放庫的main分支上Azure-Samples/mcp-container-ts建立新的 GitHub Codespace。

  1. 以滑鼠右鍵按下列按鈕,然後選取 新視窗中的 [開啟連結]。 此動作可讓您將開發環境與檔並存開啟。

    在 GitHub Codespaces 中開啟

  2. 建立 Codespace 頁面上,檢視並且選擇 建立新的 Codespace

  3. 等候程式空間啟動。 這可能需要幾分鐘的時間。

  4. 使用畫面底部終端機中的 Azure 開發人員 CLI 登入 Azure。

    azd auth login
    
  5. 從終端機複製程式代碼,然後將它貼到瀏覽器中。 請依照指示向 Azure 帳戶進行驗證。

您可以在此開發容器中執行其餘的工作。

備註

若要在本機執行 MCP 伺服器:

  1. 根據範例存放庫中「本機環境設定」一節的說明,設定您的環境。
  2. 依照範例存放庫中 Visual Studio Code 設定 MCP 伺服器 一節中的指示,將您的 MCP 伺服器設定為使用本地環境。
  3. 跳至 [ 在代理程式模式中使用 TODO MCP 伺服器工具 ] 區段以繼續。

部署和執行

範例存放庫包含 MCP 伺服器 Azure 部署的所有程式代碼和組態檔。 下列步驟會逐步引導您完成範例 MCP 伺服器 Azure 部署程式。

部署至 Azure

這很重要

本節中的 Azure 資源會立即開始花費成本,即使您在命令完成之前先停止命令也一樣。

  1. 針對 Azure 資源布建和原始程式碼部署執行下列 Azure 開發人員 CLI 命令:

    azd up
    
  2. 使用下表回答提示:

    提示 回答
    環境名稱 保持簡短和小寫。 新增您的名稱或別名。 例如: my-mcp-server 。 它用來作為資源群組名稱的一部分。
    訂閱 選取要在其中建立資源的訂用帳戶。
    位置 (用於託管) 從清單中選取您附近的位置。
    Azure OpenAI 模型的位置 從清單中選取您附近的位置。 如果某個位置與您的第一個位置相符且可選,請選擇該位置。
  3. 等候應用程式部署。 部署通常需要 5 到 10 分鐘才能完成。

  4. 部署完成後,您可以使用輸出中提供的 URL 來存取 MCP 伺服器。 URL 看起來像這樣:

https://<env-name>.<container-id>.<region>.azurecontainerapps.io
  1. 將 URL 複製到剪貼簿。 在下一節中,您將需要用到它。

在 Visual Studio Code 中設定 MCP 伺服器

在本機 VS Code 環境中設定 MCP 伺服器,方法是將 URL 新增到 mcp.json 資料夾中的 .vscode 檔案。

  1. mcp.json 資料夾開啟 .vscode 檔案。

  2. 在檔案中找出mcp-server-sse-remote區段。 看起來應該像這樣:

        "mcp-server-sse-remote": {
        "type": "sse",
        "url": "https://<container-id>.<location>.azurecontainerapps.io/sse"
    }
    
  3. 將現有的 url 值取代為您在上一個步驟中複製的 URL。

  4. mcp.json 檔案儲存在 .vscode 資料夾中。

在代理程式模式中使用 TODO MCP 伺服器工具

修改 MCP 伺服器之後,您可以以代理程式模式使用其提供的工具。 若在代理程式模式中使用 MCP 工具:

  1. 開啟 [聊天] 檢視 (Ctrl+Alt+I),然後從下拉式清單中選取 [代理程式模式]。

  2. 選取 [ 工具] 按鈕以檢視可用的工具清單。 您可以選擇性地選取或取消選取想要使用的工具。 您可以在搜尋欄中輸入文字以搜尋工具。

  3. 在聊天輸入方塊中輸入提示,例如「我需要在星期三傳送電子郵件給我的經理」,並注意如何視需要自動叫用工具,如下列螢幕快照所示:

    顯示 MCP 伺服器工具調用的螢幕快照。

備註

根據預設,叫用工具時,您必須在工具執行之前確認動作。 否則,工具可能會在本機計算機上執行,而且可能會執行修改檔案或數據的動作。

使用 [繼續] 按鈕下拉選單的選項,以自動確認目前會話、工作區,或所有未來使用的特定工具。

探索範例程序代碼

本節提供 MCP 伺服器範例中密鑰檔案和程式代碼結構的概觀。 程式代碼會組織成數個主要元件:

  • index.ts:MCP 伺服器的主要進入點,其會設定 Express.js HTTP 伺服器和路由。
  • server.ts:管理 Server-Sent 事件 (SSE) 連線和 MCP 通訊協定處理的傳輸層。
  • tools.ts:包含 MCP 伺服器的商業規則和公用程式函式。
  • types.ts:定義在MCP伺服器中使用的 TypeScript 類型和介面。

index.ts - 伺服器如何啟動及接受 HTTP 連線

檔案 index.ts 是 MCP 伺服器的主要進入點。 它會初始化伺服器、設定 Express.js HTTP 伺服器,並定義 Server-Sent 事件 (SSE) 端點的路由。

建立 MCP 伺服器實例

下列代碼段會使用 StreamableHTTPServer 類別初始化 MCP 伺服器,這是核心 MCP Server 類別的包裝函式。 這個類別會處理 Server-Sent 事件 (SSE) 的傳輸層,並管理用戶端連線。

const server = new StreamableHTTPServer(
  new Server(
    {
      name: 'todo-http-server',
      version: '1.0.0',
    },
    {
      capabilities: {
        tools: {},
      },
    }
  )
);

概念

  • 組合模式SSEPServer 包裝低階 Server 類別
  • 功能宣告:伺服器宣布支援工具(但不支援資源/提示)
  • 命名慣例:伺服器名稱會成為MCP識別的一部分

設定 Express 路由

下列代碼段會設定 Express.js 伺服器以處理 SSE 連線和訊息處理的傳入 HTTP 要求:

router.post('/messages', async (req: Request, res: Response) => {
  await server.handlePostRequest(req, res);
});

router.get('/sse', async (req: Request, res: Response) => {
  await server.handleGetRequest(req, res);
});

概念

  • 兩個端點模式:用於建立 SSE 連線的 GET、用於傳送訊息的 POST
  • 委派模式:快速路由會立即委派給 SSEPServer

程式生命週期管理

下列代碼段會處理伺服器的生命週期,包括啟動伺服器,並在終止訊號上正常關閉它:

process.on('SIGINT', async () => {
  log.error('Shutting down server...');
  await server.close();
  process.exit(0);
});

概念

  • 正常停機:在按 Ctrl+C 時進行適當的清理
  • 非同步清理:伺服器關閉作業是非同步的
  • 資源管理:SSE 連線很重要

傳輸層: server.ts

檔案 server.ts 會實作 MCP 伺服器的傳輸層,特別是處理 Server-Sent 事件 (SSE) 連線和路由 MCP 通訊協定訊息。

設定 SSE 用戶端連線並建立傳輸

類別 SSEPServer 是處理 MCP 伺服器中 Server-Sent 事件 (SSE) 的主要傳輸層。 它會使用 類別 SSEServerTransport 來管理個別用戶端連線。 它會管理多個傳輸及其生命週期。

export class SSEPServer {
  server: Server;
  transport: SSEServerTransport | null = null;
  transports: Record<string, SSEServerTransport> = {};

  constructor(server: Server) {
    this.server = server;
    this.setupServerRequestHandlers();
  }
}

概念

  • 狀態管理:追蹤目前的傳輸和所有傳輸
  • 會話對應transports 物件會將會話標識碼對應至傳輸實例
  • 建構函式委派:立即設定要求處理程式

SSE 連線建立 (handleGetRequest

handleGetRequest 用戶端向 /sse 端點提出 GET 要求時,此方法會負責建立新的 SSE 連線。

async handleGetRequest(req: Request, res: Response) {
  log.info(`GET ${req.originalUrl} (${req.ip})`);
  try {
    log.info("Connecting transport to server...");
    this.transport = new SSEServerTransport("/messages", res);
    TransportsCache.set(this.transport.sessionId, this.transport);

    res.on("close", () => {
      if (this.transport) {
        TransportsCache.delete(this.transport.sessionId);
      }
    });

    await this.server.connect(this.transport);
    log.success("Transport connected. Handling request...");
  } catch (error) {
    // Error handling...
  }
}

概念

  • 傳輸創建:為每一個SSEServerTransport GET 請求新建一個實例
  • 會話管理:自動產生的會話標識碼儲存在快取中
  • 事件處理程式:連線關閉時清除
  • MCP 連線server.connect() 建立通訊協定連線
  • 異步流程:連線設定是異步的,並具有錯誤界限

訊息處理 (handlePostRequest

方法會 handlePostRequest 處理傳入 POST 要求,以處理用戶端傳送的 MCP 訊息。 它會使用查詢參數中的會話標識碼來尋找正確的傳輸實例。

async handlePostRequest(req: Request, res: Response) {
  log.info(`POST ${req.originalUrl} (${req.ip}) - payload:`, req.body);

  const sessionId = req.query.sessionId as string;
  const transport = TransportsCache.get(sessionId);
  if (transport) {
    await transport.handlePostMessage(req, res, req.body);
  } else {
    log.error("Transport not initialized. Cannot handle POST request.");
    res.status(400).json(/* error response */);
  }
}

概念

  • 會話查閱:使用 sessionId 查詢參數來尋找傳輸
  • 會話驗證:先驗證 SSE 連線。
  • 訊息委派:傳輸負責處理實際的訊息處理任务
  • 錯誤回應:遺漏會話的適當 HTTP 錯誤碼

MCP 通訊協定處理程式設定 (setupServerRequestHandlers

方法 setupServerRequestHandlers 會針對 MCP 通訊協定要求註冊下列處理程式:

  • ListToolsRequestSchema 的處理程式會傳回 TODO 工具的可用清單。
  • 一個用來尋找並執行要求工具的 CallToolRequestSchema 處理程式,使用所提供的參數。

此方法會使用 Zod 架構 來定義預期的要求和回應格式。

private setupServerRequestHandlers() {
  this.server.setRequestHandler(ListToolsRequestSchema, async (_request) => {
    return {
      tools: TodoTools,
    };
  });
  
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const { name, arguments: args } = request.params;
    
    const tool = TodoTools.find((tool) => tool.name === name);
    if (!tool) {
      return this.createJSONErrorResponse(`Tool "${name}" not found.`);
    }
    
    const response = await tool.execute(args as any);
    return { content: [{ type: "text", text: response }] };
  });
}

概念

  • Schema-Based 路由:使用 Zod 架構來處理類型安全的要求
  • 工具探索ListToolsRequestSchema 傳回靜態TodoTools陣列
  • 工具執行CallToolRequestSchema 尋找和執行工具
  • 錯誤處理:優雅地處理不明工具
  • 回應格式:MCP 相容的響應結構
  • 類型安全:TypeScript 類型可確保正確的自變數通過

商務規則: tools.ts

檔案 tools.ts 會定義 MCP 用戶端可用的實際功能:

  • 工具元資料(名稱、描述、架構)
  • 輸入驗證架構
  • 工具執行邏輯
  • 與資料庫層整合

此 MCP 伺服器定義四個 TODO 管理工具:

  • add_todo:建立新的 TODO 待辦事項
  • complete_todo:將 TODO 項目標示為已完成
  • delete_todo:刪除 TODO 待辦事項
  • list_todos:列出所有 TODO 事項
  • update_todo_text:更新已有 TODO 項目中的文字

工具定義模式

工具會定義為 對象的陣列,每個物件都代表特定的TODO作業。 ** 在下列代碼段中,將定義此工具addTodo

{
  name: "addTodo",
  description: "Add a new TODO item to the list...",
  inputSchema: {
    type: "object",
    properties: {
      text: { type: "string" },
    },
    required: ["text"],
  },
  outputSchema: { type: "string" },
  async execute({ text }: { text: string }) {
    const info = await addTodo(text);
    return `Added TODO: ${text} (id: ${info.lastInsertRowid})`;
  },
}

每個工具定義都有:

  • name:工具的唯一標識符
  • description:工具用途的簡短描述
  • inputSchema:定義預期的輸入格式的 Zod 架構
  • outputSchema:定義預期的輸出格式的 Zod 架構
  • execute:實作工具邏輯的函式

這些工具定義會匯入 , server.ts 並透過 ListToolsRequestSchema 處理程序公開。

概念

  • 模組化工具設計:每個工具都是獨立的物件
  • JSON 架構驗證inputSchema 定義預期的參數
  • 類型安全性:TypeScript 類型符合架構定義
  • 異步執行:所有工具執行都是異步的
  • 資料庫整合:呼叫匯入的資料庫函式
  • Human-Readable 回應:傳回格式化字串,而非原始數據

工具陣列匯出

工具會匯出為靜態陣列,使其易於匯入和使用於伺服器中。 每個工具都是具有其元數據和執行邏輯的物件。 此結構可讓 MCP 伺服器根據用戶端要求動態探索和執行工具。

export const TodoTools = [
  { /* addTodo */ },
  { /* listTodos */ },
  { /* completeTodo */ },
  { /* deleteTodo */ },
  { /* updateTodoText */ },
];

概念

  • 靜態註冊:模組載入時間定義的工具
  • 陣列結構:簡單的陣列讓工具能輕鬆迭代
  • 匯入/匯出:與伺服器邏輯的清楚分離

工具運行錯誤管理

每個工具的 execute 函式會有效地處理錯誤,並傳回清晰的訊息,而不是擲回例外狀況。 此方法可確保 MCP 伺服器提供順暢的用戶體驗。

工具會處理各種錯誤案例:

async execute({ id }: { id: number }) {
  const info = await completeTodo(id);
  if (info.changes === 0) {
    return `TODO with id ${id} not found.`;
  }
  return `Marked TODO ${id} as completed.`;
}

概念

  • 資料庫回應檢查:用來 info.changes 偵測失敗
  • 優雅降級:返回描述性錯誤訊息而不是拋出錯誤例外
  • User-Friendly 錯誤:適用於 AI 解譯的訊息

資料層: db.ts

檔案 db.ts 會管理 SQLite 資料庫連線,並處理 TODO 應用程式的 CRUD 作業。 它會使用連結 better-sqlite3 庫進行同步數據庫存取。

資料庫初始化

資料庫初始化時會藉由連線到 SQLite,並在資料表不存在時建立資料表。 下列代碼段顯示初始化程式:

const db = new Database(":memory:", {
  verbose: log.info,
});

try {
  db.pragma("journal_mode = WAL");
  db.prepare(
    `CREATE TABLE IF NOT EXISTS ${DB_NAME} (
     id INTEGER PRIMARY KEY AUTOINCREMENT,
     text TEXT NOT NULL,
     completed INTEGER NOT NULL DEFAULT 0
   )`
  ).run();
  log.success(`Database "${DB_NAME}" initialized.`);
} catch (error) {
  log.error(`Error initializing database "${DB_NAME}":`, { error });
}

概念

  • In-Memory 資料庫:memory: 表示重新啟動時遺失的數據(僅限示範/測試)
  • WAL 模式:Write-Ahead 記錄以提升效能
  • Schema Definition:具自動遞增 ID 的簡單 TODO 資料表
  • 錯誤處理:初始化失敗的正常處理
  • 記錄整合:記錄資料庫作業以進行偵錯

CRUD 作業模式

檔案 db.ts 提供四個主要的 CRUD 作業以管理 TODO 項目:

建立作業

export async function addTodo(text: string) {
  log.info(`Adding TODO: ${text}`);
  const stmt = db.prepare(`INSERT INTO todos (text, completed) VALUES (?, 0)`);
  return stmt.run(text);
}

讀取作業

export async function listTodos() {
  log.info("Listing all TODOs...");
  const todos = db.prepare(`SELECT id, text, completed FROM todos`).all() as Array<{
    id: number;
    text: string;
    completed: number;
  }>;
  return todos.map(todo => ({
    ...todo,
    completed: Boolean(todo.completed),
  }));
}

更新工作

export async function completeTodo(id: number) {
  log.info(`Completing TODO with ID: ${id}`);
  const stmt = db.prepare(`UPDATE todos SET completed = 1 WHERE id = ?`);
  return stmt.run(id);
}

刪除操作

export async function deleteTodo(id: number) {
  log.info(`Deleting TODO with ID: ${id}`);
  const row = db.prepare(`SELECT text FROM todos WHERE id = ?`).get(id) as
    | { text: string }
    | undefined;
  if (!row) {
    log.error(`TODO with ID ${id} not found`);
    return null;
  }
  db.prepare(`DELETE FROM todos WHERE id = ?`).run(id);
  log.success(`TODO with ID ${id} deleted`);
  return row;
}

概念

  • 預處理語句:防止 SQL 注入攻擊
  • 類型轉換:明確定義的 TypeScript 類型用於查詢結果
  • 數據轉換:將 SQLite 整數轉換成布爾值
  • 原子性操作:每個函式都是單一資料庫交易
  • 傳回值一致性:函式會傳回作業元數據
  • 防禦性程式設計:檢查刪除前模式

架構設計

資料庫架構是使用簡單的 SQL 語句在 db.ts 檔案中定義。 資料表 todos 有三個字段:

CREATE TABLE todos (
  id INTEGER PRIMARY KEY AUTOINCREMENT,  -- Unique identifier
  text TEXT NOT NULL,                    -- TODO description  
  completed INTEGER NOT NULL DEFAULT 0   -- Boolean as integer
);

輔助工具: helpers/ 目錄

目錄 helpers/ 會提供伺服器的公用程式函式和類別。

結構化日誌用於偵錯與監控:helpers/logs.ts

檔案 helpers/logs.ts 會為 MCP 伺服器提供結構化記錄公用程式。 它會使用 debug 庫來記錄和在控制台中顯示色彩編碼的輸出。

export const logger = (namespace: string) => {
  const dbg = debug('mcp:' + namespace);
  const log = (colorize: ChalkInstance, ...args: any[]) => {
    const timestamp = new Date().toISOString();
    const formattedArgs = [timestamp, ...args].map((arg) => {
      if (typeof arg === 'object') {
        return JSON.stringify(arg, null, 2);
      }
      return arg;
    });
    dbg(colorize(formattedArgs.join(' ')));
  };

  return {
    info(...args: any[]) { log(chalk.cyan, ...args); },
    success(...args: any[]) { log(chalk.green, ...args); },
    warn(...args: any[]) { log(chalk.yellow, ...args); },
    error(...args: any[]) { log(chalk.red, ...args); },
  };
};

SSE 傳輸的工作階段管理: helpers/cache.ts

檔案 helpers/cache.ts 會使用 Map ,依會話標識符儲存 SSE 傳輸。 此方法可讓伺服器快速尋找及管理作用中的連線。

import type { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse";

export const TransportsCache = new Map<string, SSEServerTransport>();

備註

TransportsCache是簡單的記憶體內部快取。 在生產環境中,請考慮使用更健全的解決方案,例如 Redis 或用於會話管理的資料庫。

執行流程摘要

下圖說明從用戶端到 MCP 伺服器和返回的完整要求旅程,包括工具執行和資料庫作業:

此圖顯示從用戶端到 MCP 伺服器和返回的完整要求旅程圖。

清理 GitHub Codespaces

刪除 GitHub Codespaces 環境,以最大化每核心的免費時數。

這很重要

如需 GitHub 帳戶免費記憶體和核心時數的詳細資訊,請參閱 GitHub Codespaces 每月包含的記憶體和核心時數

  1. 登入至 GitHub Codespaces 儀錶板

  2. 查找從 GitHub 存放庫建立的活動 Azure-Samples//mcp-container-ts Codespaces。

  3. 開啟 codespace 的上下文選單,然後選取 [刪除]

尋求幫助

將您的問題提交到儲存庫的問題