使用 Azure 容器应用生成 TypeScript MCP 服务器

本文介绍如何使用 Node.js 和 TypeScript 生成模型上下文协议 (MCP) 服务器。 服务器在无服务器环境中运行工具和服务。 将此结构用作创建自定义 MCP 服务器的起点。

获取代码

浏览 TypeScript 远程模型上下文协议 (MCP) 服务器 示例。 它演示如何使用 Node.js 和 TypeScript 生成远程 MCP 服务器并将其部署到 Azure 容器应用。

跳转到 代码演练部分 ,了解此示例的工作原理。

体系结构概述

下图显示了示例应用的简单体系结构: 显示从托管代理的 Visual Studio Code 和 MCP 客户端到 MCP 服务器的体系结构的关系图。

MCP 服务器在 Azure 容器应用(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 开发工具 CLI (azd)

开发容器包含本文所需的所有依赖项。 可以在 GitHub Codespaces(在浏览器中)或使用 Visual Studio Code 在本地运行它。

若要遵循本文,请确保满足以下先决条件:

开放开发环境

按照以下步骤设置具有所有必需依赖项的预配置开发环境。

GitHub CodespacesVisual Studio Code for Web 作为界面运行 GitHub 托管的开发容器。 使用 GitHub Codespaces 进行最简单的设置,因为它附带了本文预安装的必需工具和依赖项。

重要

所有 GitHub 帐户每月最多可使用 Codespaces 60 小时,其中包含两个核心实例。 有关详细信息,请参阅 GitHub Codespaces 每月所含的存储和计算核心小时数

使用以下步骤在 main GitHub 存储库的 Azure-Samples/mcp-container-ts 分支上创建新的 GitHub Codespace。

  1. 右键单击以下按钮,然后选择 新窗口中的“打开”链接。 此操作使你能够同时打开并查看开发环境和文档。

    在GitHub Codespaces中打开

  2. “创建代码空间 ”页上,查看并选择“ 创建新代码空间”。

  3. 等待 Codespace 启动。 这可能需要几分钟时间。

  4. 在终端的屏幕底部,使用 Azure Developer 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 Developer CLI 命令,以进行 Azure 资源预配和源代码部署:

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

    Prompt 答案
    环境名称 保持简短和小写。 添加名称或别名。 例如,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 服务器

通过将 URL 添加到 mcp.json 文件夹中的文件 .vscode ,在本地 VS Code 环境中配置 MCP 服务器。

  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 类的包装器。 此类处理服务器发送的事件 (SSE) 的传输层并管理客户端连接。

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

概念

  • 合成模式SSEPServer 封装低级别 Server
  • 功能声明:服务器报出它支持工具(但不支持资源/提示)
  • 命名约定:服务器名称成为 MCP 标识的一部分

设置快速路由

以下代码片段设置 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 对象将会话 ID 映射到传输实例
  • 构造函数委派:立即设置请求处理程序

SSE 连接建立 (handleGetRequest)

当客户端向handleGetRequest终结点发出 GET 请求时,该方法/sse负责建立新的 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...
  }
}

概念

  • 传输创建:每个 GET 请求的新增 SSEServerTransport 功能
  • 会话管理:缓存中存储的自动生成会话 ID
  • 事件处理程序:在连接关闭时执行清理操作
  • MCP 连接server.connect() 建立协议连接
  • 异步流:连接设置是异步的,具有错误边界

消息处理 (handlePostRequest

该方法 handlePostRequest 处理传入的 POST 请求以处理客户端发送的 MCP 消息。 它使用查询参数中的会话 ID 来查找正确的传输实例。

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 协议请求注册以下处理程序:

  • 返回可用 TODO 工具列表的 ListToolsRequestSchema 的处理程序。
  • 一个用于 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:删除待办事项
  • list_todos:列出所有 TODO 项目
  • update_todo_text:更新现有待办事项的文本

工具定义模式

这些工具被定义为一组对象,每个对象代表一个特定的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日志记录以提高性能
  • 模式定义:具有自增 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 按会话 ID 存储 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. Azure-Samples//mcp-container-ts GitHub 存储库查找创建的活动代码空间。

  3. 打开代码空间的上下文菜单,然后选择“删除”

获取帮助

将问题记录到存储库的“问题”