首页 / 文章

初学者版MCP:构建服务端与客户端,然后将其应用于企业环境。

面向初学者的MCP:N×M问题、用通俗语言解释的核心概念、基于Node.js的TypeScript帮助台服务器与客户端,以及迈向安全HTTP部署的步骤。

发布者:REACTAPP.TOP阅读约 14 分钟3185 词

来源: MCP Tools Inside Enterprise Applications: A Beginner-Friendly Deep Dive

最近凡是使用过人工智能智能体的人,都应该接触过MCP,即模型上下文协议。它出现在 Claude Desktop 中,也出现在 IDE 插件、企业内部的“AI 门户”项目中,而且越来越多地被应用于生产级企业软件中。

这是一篇面向初学者的入门指南,无需任何 MCP 使用经验。文章从该协议要解决的问题讲起,介绍其架构,使用 Node.js 和 TypeScript 构建实际的服务器与客户端,随后探讨企业为这一切所构建的安全机制、治理体系与可观测性方案。其中的图表为纯文本 ASCII 格式,可直接粘贴到笔记或代码文档中。

1. MCP 所解决的问题

在MCP出现之前,要让智能体执行实际任务,比如读取CRM记录、提交ITSM工单或查询数据仓库,唯一的办法就是为该智能体及对应系统编写定制的、一次性的集成代码。

当存在多个智能体(如支持机器人、编程助手、内部知识助手)以及多个系统(Salesforce、Jira、人力资源数据库、仓库系统)时,这种情况还能勉强忍受。但最终会导致大量手工编写的连接器堆积如山。

BEFORE MCP — the N x M integration problem

  ┌───────────┐        ┌─────────────┐
  │  Agent A  │───────▶│  CRM API    │  (custom connector #1)
  └───────────┘        └─────────────┘
  ┌───────────┐        ┌─────────────┐
  │  Agent A  │───────▶│  Ticketing  │  (custom connector #2)
  └───────────┘        └─────────────┘
  ┌───────────┐        ┌─────────────┐
  │  Agent B  │───────▶│  CRM API    │  (custom connector #3 -
  └───────────┘        └─────────────┘   yes, AGAIN, for a different agent)
  ┌───────────┐        ┌─────────────┐
  │  Agent B  │───────▶│  Data       │  (custom connector #4)
  └───────────┘        │  Warehouse  │
                       └─────────────┘
  N agents x M systems = N x M custom, non-reusable integrations.
  Every new agent re-implements auth, retries, schemas, error handling.

USB解决了硬件外设的同类问题,而ODBC和JDBC则解决了数据库的同类问题:存在太多自定义连接器,且缺乏统一标准。MCP是Anthropic给出的解决方案,它以开放协议的形式发布,并非仅限于Claude使用,因此任何人工智能应用和任何后端系统都能使用这种通用语言进行交互。

AFTER MCP — one protocol, many servers, many clients

  ┌───────────┐                          ┌───────────────────┐
  │  Agent A  │──┐                   ┌─▶│  MCP Server: CRM   │
  └───────────┘  │    ┌───────────┐  │   └───────────────────┘
                 ├───▶│    MCP   │───┤  ┌───────────────────┐
  ┌───────────┐  │    │  (shared  │  ├─▶│ MCP Server: Ticket │
  │  Agent B  │──┘    │  protocol)│  │   └───────────────────┘
  └───────────┘       └───────────┘  │  ┌──────────────────┐
                                     └─▶│ MCP Server: DW   │
                                        └──────────────────┘
  Any MCP-compatible agent can now talk to any MCP server.
  Build the connector once, reuse it everywhere.

其核心内容可以用一句话概括。MCP为AI应用提供了一种统一的方式,用于查找工具、调用工具、读取数据以及使用提示词模板,这样只需编写一次集成代码,所有符合标准的客户端都可以重复使用。

2. 用通俗语言解释核心概念

有四个术语被较为宽泛地使用,因此在编写任何代码之前先明确其定义。

  • 主机。最终用户实际使用的基于AI的应用程序,比如内部聊天助手、IDE插件或支持协作者,它包含大型语言模型。
  • MCP客户端。主机内部的组件,负责管理与单个MCP服务器的连接。一个与三个后端交互的主机通常会运行三个客户端,每个服务器对应一个。
  • MCP服务器。这是一种轻量级程序,依据MCP规范展示某个系统的功能(工具、数据、提示语)。作为企业级开发者,通常需要自行构建这类服务器。
  • 传输机制。客户端与服务器通过该机制进行通信。目前主要有两种:stdio,即客户端以本地子进程的形式启动服务器,并通过标准输入输出进行消息交换(适用于本地开发和桌面应用);以及Streamable HTTP,即服务器通过HTTPS远程运行(这是共享式、集中式服务器的常见选择)。
  • 在底层,所有消息都使用JSON-RPC 2.0这种简单且成熟的格式来传递请求、响应和通知。SDK会自动处理相关编码,但了解这种数据格式就能明白为何MCP调试工具会与你已使用的API工具类似,都包含请求ID、方法名、参数以及结果或错误信息。

    服务器能提供的功能:三种基本元素

    • 工具是可调用的函数,用于让模型执行操作或获取计算结果,由模型根据用户的请求决定何时调用这些函数。示例:create_support_ticket、get_employee_details。
    • 资源是只读数据,主机可将其作为上下文附加,比如文件或记录。由应用程序或用户决定要附加什么内容。示例:知识库文章、政策PDF文件、日志文件。
  • 提示词是可重复使用的、带参数的指令模板,用户可像使用斜杠命令一样选择它们。例如:“为事后分析总结此事件。”
  • 在大多数企业集成场景中,工具承担了约90%的复杂工作,因此会首先被构建起来。

    3. 三层架构

    ┌─────────────────────────── HOST APPLICATION ───────────────────────────┐
    │   e.g. an internal AI assistant, IDE plugin, support copilot           │
    │                                                                        │
    │   ┌───────────────┐        ┌───────────────┐       ┌───────────────┐   │
    │   │  MCP Client 1 │        │  MCP Client 2 │       │  MCP Client 3 │   │
    │   └───────┬───────┘        └───────┬───────┘       └───────┬───────┘   │
    └───────────┼────────────────────────┼───────────────────────┼───────────┘
                │ JSON-RPC over          │ JSON-RPC over         │ JSON-RPC over
                │ stdio / HTTPS          │ stdio / HTTPS         │ stdio / HTTPS
                ▼                        ▼                       ▼
       ┌──────────────────┐     ┌──────────────────┐     ┌─────────────────┐
       │   MCP Server     │     │   MCP Server     │     │   MCP Server    │
       │  wraps HR system │     │  wraps Ticketing │     │  wraps Data     │
       │  (tools: lookup, │     │  (tools: create, │     │  Warehouse      │
       │   update)        │     │   status, close) │     │  (tools: query) │
       └──────────────────┘     └──────────────────┘     └─────────────────┘
    

    每个MCP服务器都应做到规模小巧且功能专注,每个后端系统都对应独立的服务器,而非由一个庞大的服务器承担所有功能。这种设计理念借鉴了微服务架构,具有较小的影响范围、独立的部署能力、独立的扩展性,且由负责该后端系统的团队自行管理。

    4. 构建第一个MCP服务器(Node.js与TypeScript)

    示例是一个IT服务台MCP服务器,它包含两个工具:一个用于创建支持工单,另一个用于查询工单状态。该示例体现了企业中非常常见的模式,即对内部的工单系统或ITSM API进行封装。

    4.1 项目设置

    mkdir helpdesk-mcp-server && cd helpdesk-mcp-server
    npm init -y
    npm install @modelcontextprotocol/sdk zod
    npm install -D typescript tsx @types/node
    npx tsc --init
    

    @modelcontextprotocol/sdk 是官方提供的包。它负责处理网络传输中的 JSON-RPC 协议、传输方式以及基础数据类型,因此无需手动操作原始的 JSON-RPC 数据。zod 是一个用于模式验证的库。在处理程序代码执行之前,会先根据预设模式检查 MCP 工具的输入,这在企业环境中尤为重要,因为这样可以阻止格式错误或恶意请求的进入。

    4.2 服务器代码

    // src/server.ts
    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
    import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
    import { z } from "zod";
    
    // --- A stand-in for a real internal ticketing API client ---
    // In a real enterprise server this would call your ITSM system
    // (ServiceNow, Jira Service Management, Zendesk, an internal API, etc.)
    const ticketStore = new Map<string, { status: string; subject: string }>();
    let nextId = 1000;
    // 1. Create the server instance.
    //    "name" and "version" identify this server to any client that connects.
    const server = new McpServer({
      name: "helpdesk-mcp-server",
      version: "1.0.0",
    });
    // 2. Register a tool: create_support_ticket
    server.registerTool(
      "create_support_ticket",
      {
        title: "Create Support Ticket",
        description:
          "Creates a new IT helpdesk ticket for the requesting employee.",
        inputSchema: {
          subject: z.string().describe("Short summary of the issue"),
          priority: z.enum(["low", "medium", "high", "urgent"]),
          employeeId: z.string().describe("Requesting employee's ID"),
        },
        outputSchema: {
          ticketId: z.string(),
          status: z.string(),
        },
      },
      async ({ subject, priority, employeeId }) => {
        const ticketId = `TCK-${nextId++}`;
        ticketStore.set(ticketId, { status: "open", subject });
        const output = { ticketId, status: "open" };
        // MCP tool results return a "content" array (what a human/LLM reads)
        // and, optionally, "structuredContent" (typed data other code can use).
        return {
          content: [
            {
              type: "text",
              text: `Created ticket ${ticketId} (priority: ${priority}) for employee ${employeeId}.`,
            },
          ],
          structuredContent: output,
        };
      }
    );
    // 3. Register a second tool: get_ticket_status
    server.registerTool(
      "get_ticket_status",
      {
        title: "Get Ticket Status",
        description: "Looks up the current status of an existing support ticket.",
        inputSchema: {
          ticketId: z.string(),
        },
        outputSchema: {
          status: z.string(),
        },
      },
      async ({ ticketId }) => {
        const ticket = ticketStore.get(ticketId);
        if (!ticket) {
          // Returning isError lets the model know the call failed
          // WITHOUT crashing the whole conversation.
          return {
            content: [{ type: "text", text: `No ticket found with ID ${ticketId}.` }],
            isError: true,
          };
        }
        return {
          content: [{ type: "text", text: `Ticket ${ticketId} is currently "${ticket.status}".` }],
          structuredContent: { status: ticket.status },
        };
      }
    );
    // 4. Wire the server to a transport and start listening.
    //    stdio is perfect for local development and desktop-hosted tools.
    const transport = new StdioServerTransport();
    await server.connect(transport);
    

    4.3 各部分的职能

    • McpServer 实例。该对象是服务器的核心,负责记录所有已注册的工具、资源及提示词,并在客户端连接时处理MCP握手(能力协商)流程。由于SDK会通过registerTool调用自动生成工具列表,因此无需手动编写列表。
    • 使用Zod实现的inputSchema。它不仅仅是文档工具,SDK还利用它生成JSON Schema,用以告知客户端和大型语言模型哪些参数是有效的,同时会对接收到的每个请求进行验证。如果客户端传入priority: "banana"这样的无效值,会在进入处理逻辑之前就被拒绝。对企业而言,这种以Schema为依据的验证是重要的安全控制措施,而非可有可无的附加功能。
  • outputSchema 和 structuredContent。 工具既可以返回人类可读的摘要(content),也可以返回机器可读的对象(structuredContent)。文本会显示在聊天记录中,而调用方应用则可以解析这些结构化数据来驱动用户界面或执行后续逻辑。
  • isError: true。 MCP 明确区分了工具级错误与协议级错误。当该状态出现时表示操作失败,但对话仍应继续,模型可以读取错误信息并决定是重新尝试、询问用户还是使用其他工具。你当然不希望因为某个工单编号不存在就导致整个智能体会话崩溃。
  • 传输机制(StdioServerTransport)。负责传输字节数据的组件。对于开发者集成开发环境或桌面助手这类本地工具而言,使用标准输入输出方式最为简单,主机会以子进程的形式启动服务器。而在企业环境中集中运行的系统则会采用基于HTTP的传输机制,相关内容将在第6节中介绍。
  • 4.4 运行方式

    npx tsx src/server.ts
    

    单独来看,服务器不会显示任何内容,因为它正在等待客户端通过标准输入和标准输出进行连接。接下来是客户端部分。

    5. 企业应用中的客户端

    初学者通常先构建服务器,然后会好奇到底是什么在与之通信。这里是一个最简单的客户端示例,它被设想为支撑公司人工智能助手的内部 Node.js 后端的一部分。

    // src/client.ts
    import { Client } from "@modelcontextprotocol/sdk/client/index.js";
    import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
    
    async function main() {
      // 1. Describe how to launch the server. Here we spawn it as a
      //    local subprocess - in production you'd more commonly point
      //    this at a remote HTTP-based server instead (see Section 6).
      const transport = new StdioClientTransport({
        command: "npx",
        args: ["tsx", "src/server.ts"],
      });
      // 2. Create a client and connect. This performs the MCP
      //    handshake and capability negotiation automatically.
      const client = new Client({ name: "internal-ai-assistant", version: "1.0.0" });
      await client.connect(transport);
      // 3. Discover what tools this server offers - this is the same
      //    mechanism an LLM uses to "learn" what it can do.
      const { tools } = await client.listTools();
      console.log("Available tools:", tools.map((t) => t.name));
      // 4. Call a tool, just like the LLM would.
      const result = await client.callTool({
        name: "create_support_ticket",
        arguments: {
          subject: "VPN keeps disconnecting",
          priority: "high",
          employeeId: "E-4821",
        },
      });
      console.log(result.content);
      await client.close();
    }
    main();
    

    从概念层面看其重要性

    核心理念是发现。客户端调用listTools()后,会收到一份服务器提供的所有工具的机器可读目录,每款工具都附有其结构描述。这正是MCP具备动态性的原因:由大语言模型驱动的宿主无需在提示词中预先嵌入特定工具。在建立连接时,它会询问服务器自身能做什么,而服务器的回复会成为该对话中模型被允许执行的操作范围的一部分。

    企业治理正是依托这一机制实现的。如果某些工具不应被特定用户或环境使用,最有效的控制方式就是将其排除在listTools()的返回结果之外,而非寄希望于模型会主动选择不使用这些工具。

    6. 从本地原型到企业级部署

    stdio配置适合学习和本地开发工具的使用,但它并不符合大多数企业运行基础设施的方式:基础设施集中托管、需要身份验证、会有日志记录,并且能同时被多个主机应用程序访问。这时就需要Streamable HTTP传输协议和API网关了。

    以下是将同一个服务台服务器改造成通过HTTP运行并置于普通Express应用之后的示例:

    // src/httpServer.ts
    import express from "express";
    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
    import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
    
    const app = express();
    app.use(express.json());
    app.post("/mcp", async (req, res) => {
      // In a real enterprise deployment, authentication middleware would
      // run BEFORE this point - verifying a bearer token, checking scopes,
      // and attaching the caller's identity to the request.
      const server = buildHelpdeskServer(); // same registerTool calls as before
      const transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: undefined, // stateless mode: simplest to scale horizontally
      });
      res.on("close", () => transport.close());
      await server.connect(transport);
      await transport.handleRequest(req, res, req.body);
    });
    app.listen(3000, () => console.log("MCP server listening on :3000"));
    

    这一重大变化与其说是关乎代码,不如说是关乎责任归属。在stdio中隐含的三个问题,在基于HTTP的架构中则变成了明确的企业级关注点。

    1. 认证流程被置于服务器之前,通常在中间件或网关中完成,这样服务器就可以假定req已包含经过验证的身份信息。
    2. 无状态设计是经过深思熟虑的决定。由于设置了sessionIdGenerator: undefined,传输层不会为每个连接保存会话信息,因此负载均衡器后面的任意实例都能响应请求,从而可以像普通无状态API那样实现水平扩展。
    3. 可观测性不再是可选功能。每一次工具调用都相当于一次真实的网络请求,因此应当像处理任何内部API调用一样对其进行日志记录、追踪和速率限制。

    7. 企业级考量清单

    让MCP服务器运行起来只是整个任务的五分之一而已。剩下的五分之四在于确保其在处理真实员工或客户数据的生产环境中安全运行,具体措施如下。

    • 身份认证与授权。在网关层使用OAuth2或OIDC机制,将调用用户或服务账户的身份与特定的权限范围对应起来,从而避免任何工具都能被任意用户调用。
    • 最小权限工具设计。优先选择功能有限的工具,如get_ticket_status,而非功能广泛的工具,如run_sql_query。功能明确、职责单一的工具更易于理解和审计。
  • 输入验证。应像上文提到的Zod那样,将模式验证视为真正的安全屏障,而非仅仅是类型检查的便利手段。
  • 破坏性操作需人工干预。对于任何不可逆的操作,如删除记录、发送外部邮件或批准付款,都应在工具执行前获得用户的明确确认。
  • 审计日志记录。需记录调用每个工具的用户、所使用的参数以及操作结果。这有助于在事后解释由人工智能驱动的操作,而在医疗和金融等受监管领域,这是硬性要求。
  • 速率限制与超时设置。应采用与其他内部API流量相同的控制措施,以防止陷入循环的代理程序对后端造成冲击。
  • 幂等性。确保具有副作用的工具在相同输入下可以安全地被调用两次,因为代理程序有时会重试。
  • 版本控制。随着工具架构的变更,需有意识地对它们进行版本管理,以避免旧版本主机出现隐性故障。
  • 8. 这些概念在现实企业中的应用场景

    • IT部门与内部支持团队。以服务台为例:创建和查询工单、重置密码、检查系统状态。
    • 销售团队与CRM辅助系统。基于真实的CRM数据而非模型预测来获取账户历史记录并起草后续邮件。
    • 数据与分析助手。提供一组经过预先审批的只读查询工具,用于访问数据仓库,替代不受限制的SQL查询。
    • 医疗工作流程。这类工具用于查询预约时间、检查保险资格或识别护理协调中的漏洞,由于数据极为敏感,始终需要严格的基于角色的访问控制与审计追踪。在此类场景中,第7节中提到的专用工具、人工干预以及完整审计日志原则往往是合规要求。
    • 开发者工具。这类IDE助手可查询内部文档、启动持续集成流程或查看某项服务的值班人员。

    它们的共同点在于:MCP并不会让模型变得更聪明。变化在于,AI现在能够以可审计的方式访问真实系统,而无需再编造看似合理的答案。

    9. 初学者常遇到的陷阱

    • 一个功能齐全的巨型服务器。按后端系统或域名进行划分。这符合良好的微服务边界设计,有助于将影响范围控制在较小范围内。
    • 忽略description字段。该模型在决定何时以及如何调用某个功能时严重依赖工具和参数的描述。模糊的描述会导致使用错误的工具,或使用正确的工具但传入错误的参数。
    • 忘记设置isError。工具处理程序抛出的原始异常可能会中断整个客户端连接,而模型应当被告知该操作失败并尝试其他方法。
    • 将stdio直接用于生产环境。stdio适用于本地开发和桌面环境。多租户的生产环境需要通过真正的网关来实现HTTP传输。
  • 没有审计追踪功能。除非你能明确是谁让AI执行操作以及它具体做了什么,否则应避免将其用于生产数据。
  • 10. 简要总结

    抛开那些流行术语后,MCP的核心理念其实非常简单:为AI应用提供一种通用方式,用于查找并调用工具、获取数据以及重复使用提示模板,这样只需完成一次集成工作即可服务于所有符合标准的客户。对企业而言,正是这种标准化使得AI智能体从华而不实的演示工具转变为可以被管理、保护,并能与其他系统一同在生产环境中运行的实际工具。

    对于初学者而言,合理的路径是:

    1. 像帮助台示例那样,使用stdio在本地构建一个小型MCP服务器。
    2. 添加一个能够调用这些工具的客户端,以便完整查看整个操作流程。
    3. 将服务器置于带认证的HTTP背后,或在其前方设置网关。
    4. 只有这样,才能对任何可能造成破坏的操作启用审计日志、速率限制以及人工审批。

    从小型本地环境开始,逐步引入企业级考量。试图从第一天就设计出完美的生产服务器,是更为不可靠的做法。