跳到主内容

一文搞懂企业内部 MCP Server 开发与注册 ​

为什么企业需要自己的 MCP Tool Server ​

大模型接入企业内部系统时,最常见的做法是为每个智能体单独写函数调用:查工单、查库存、创建审批、检索知识库,每个平台都重复对接一遍。随着工具数量增加,权限、参数校验、日志审计和接口版本会迅速失控。

MCP(Model Context Protocol)的价值,是把“工具如何被发现、参数如何描述、结果如何返回”标准化。企业只需要实现一个内部 MCP Server,智能体、IDE 助手或工作流引擎即可通过统一协议发现 tools,并在授权后完成 tool-use。

本文用一个“员工差旅助手”场景,演示从零开发可运行的企业内部工具 Server,并注册到智能体工作流。示例使用 Python 标准库实现 JSON-RPC over stdio,避免初学者被框架版本差异卡住;生产环境可再替换为官方 SDK 或网关部署。

方案对比:函数调用、插件与 MCP Server ​

在动手前,需要明确三种集成方式的边界。

方案接入成本复用性权限治理适用场景
应用内 function calling低弱跟随业务服务单一智能体、少量临时工具
传统插件/OpenAPI 包装中中依赖网关已有 REST API、调用方固定
MCP Server中高可统一鉴权与审计多智能体、多客户端、企业工具市场

MCP 不是让模型直接访问数据库,也不是把业务逻辑搬进提示词。它更像一个“工具适配器”:对上暴露结构化 tool schema,对下调用经过封装的内部 API,并负责鉴权、参数校验、错误归一化和审计日志。

MCP 工具调用链路与职责边界 ​

下图展示一次典型的 tool-use:智能体先列出工具,再根据用户意图选择工具,MCP Server 校验身份和参数后访问内部系统,最后把结构化结果交还给模型生成回答。

用户提出业务请求智能体选择工具与参数MCP Server鉴权、校验、审计内部业务 API工单/审批/库存结构化结果错误码与摘要

需要强调的是,MCP Server 应保持“无业务决策”或“弱业务决策”。是否批准差旅、是否允许退款,仍应由后端系统根据制度判断;模型只负责理解意图、补全参数和解释结果,不能把最终授权交给大模型。

开发前准备:定义工具边界与安全模型 ​

本示例实现三个工具:

  • travel_policy_search:查询差旅制度,只读、低风险。
  • trip_budget_get:查询部门预算余额,需要员工身份和部门权限。
  • trip_request_create:创建出差申请,属于写操作,需要二次确认和完整审计。

建议按风险分级,而不是把所有接口一次性暴露给智能体:

  1. 只读工具可自动调用,但必须限制返回字段,避免泄露薪资、身份证号等敏感信息。
  2. 写入工具必须使用幂等键,防止模型重试造成重复单据。
  3. 删除、审批、支付类动作不要直接开放给通用智能体,应进入人工确认流程。

从零实现一个可运行的 MCP Server ​

下面代码保存为 travel_mcp_server.py,通过 python travel_mcp_server.py 启动。它使用 stdin/stdout 传输 JSON-RPC,日志写入 stderr,避免污染协议消息。

python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""企业差旅工具 MCP Server(教学版,可直接通过 stdio 测试)"""
import sys
import json
import uuid
import traceback
from typing import Any, Dict

SERVER_NAME = "travel-tools"
SERVER_VERSION = "1.0.0"

# 生产环境应替换为真实内部 API;这里用内存数据演示
MOCK_POLICY = {
    "hotel": "一线城市酒店标准不超过 500 元/晚,需保留发票。",
    "flight": "高铁 5 小时以内优先选择二等座;超标准需直属主管审批。"
}

MOCK_BUDGET = {"platform": 12800.5, "sales": 3600.0}
MOCK_REQUESTS = []


def log(message: str) -> None:
    # stdout 只能输出协议消息,诊断日志必须写 stderr
    print(f"[mcp-log] {message}", file=stderr_safe(), flush=True)


def stderr_safe():
    return sys.stderr


def tool_definitions() -> list[Dict[str, Any]]:
    """返回工具清单。description 会直接影响模型选择工具的准确率。"""
    return [
        {
            "name": "travel_policy_search",
            "description": "查询公司差旅制度。适用于询问酒店标准、交通工具、报销规则。",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "topic": {
                        "type": "string",
                        "enum": ["hotel", "flight"],
                        "description": "制度主题:hotel 表示酒店,flight 表示交通"
                    }
                },
                "required": ["topic"]
            }
        },
        {
            "name": "trip_budget_get",
            "description": "查询当前员工所在部门的差旅预算余额。",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "department": {
                        "type": "string",
                        "enum": ["platform", "sales"],
                        "description": "部门标识"
                    }
                },
                "required": ["department"]
            }
        },
        {
            "name": "trip_request_create",
            "description": "创建出差申请。仅在用户确认城市、日期和事由后调用。",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "出差城市"},
                    "start_date": {"type": "string", "description": "开始日期,格式 YYYY-MM-DD"},
                    "end_date": {"type": "string", "description": "结束日期,格式 YYYY-MM-DD"},
                    "reason": {"type": "string", "description": "出差事由"},
                    "idempotency_key": {
                        "type": "string",
                        "description": "幂等键,由客户端生成,防止重复创建"
                    }
                },
                "required": ["city", "start_date", "end_date", "reason", "idempotency_key"]
            }
        }
    ]


def call_tool(name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
    """执行工具并返回 MCP content。"""
    if name == "travel_policy_search":
        topic = arguments.get("topic")
        if topic not in MOCK_POLICY:
            raise ValueError("topic 只能是 hotel 或 flight")
        data = {"topic": topic, "policy": MOCK_POLICY[topic]}

    elif name == "trip_budget_get":
        department = arguments.get("department")
        if department not in MOCK_BUDGET:
            raise PermissionError("无权查询该部门预算")
        data = {"department": department, "budget_remaining": MOCK_BUDGET[department]}

    elif name == "trip_request_create":
        required = ["city", "start_date", "end_date", "reason", "idempotency_key"]
        missing = [key for key in required if not arguments.get(key)]
        if missing:
            raise ValueError(f"缺少必填字段:{','.join(missing)}")

        # 真实系统应根据 idempotency_key 去重
        for item in MOCK_REQUESTS:
            if item["idempotency_key"] == arguments["idempotency_key"]:
                data = item
                break
        else:
            data = {
                "request_id": f"TR-{uuid.uuid4().hex[:8].upper()}",
                "status": "pending_manager_approval",
                **arguments
            }
            MOCK_REQUESTS.append(data)
    else:
        raise ValueError(f"未知工具:{name}")

    return {
        "content": [
            {"type": "text", "text": json.dumps(data, ensure_ascii=False)}
        ],
        "isError": False
    }


def handle_request(request: Dict[str, Any]) -> Dict[str, Any] | None:
    req_id = request.get("id")
    method = request.get("method")
    params = request.get("params", {})

    # notification 没有 id,不需要响应
    if req_id is None:
        log(f"收到通知:{method}")
        return None

    if method == "initialize":
        result = {
            "protocolVersion": "2024-11-05",
            "capabilities": {"tools": {"listChanged": False}},
            "serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION}
        }
    elif method == "tools/list":
        result = {"tools": tool_definitions()}
    elif method == "tools/call":
        try:
            result = call_tool(params.get("name", ""), params.get("arguments", {}))
        except Exception as exc:
            log(traceback.format_exc())
            result = {
                "content": [{"type": "text", "text": f"工具调用失败:{exc}"}],
                "isError": True
            }
    else:
        return {
            "jsonrpc": "2.0",
            "id": req_id,
            "error": {"code": -32601, "message": f"方法不存在:{method}"}
        }

    return {"jsonrpc": "2.0", "id": req_id, "result": result}


def main() -> None:
    for line in sys.stdin:
        line = line.strip()
        if not line:
            continue
        try:
            request = json.loads(line)
            response = handle_request(request)
            if response is not None:
                print(json.dumps(response, ensure_ascii=False), flush=True)
        except json.JSONDecodeError:
            error = {
                "jsonrpc": "2.0",
                "id": None,
                "error": {"code": -32700, "message": "JSON 解析失败"}
            }
            print(json.dumps(error, ensure_ascii=False), flush=True)


if __name__ == "__main__":
    main()

可以用下面的输入逐行测试,注意每条消息必须是一行 JSON:

text
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"travel_policy_search","arguments":{"topic":"hotel"}}}

注册到智能体:客户端配置与调用策略 ​

支持 MCP 的智能体宿主通常允许配置一个 stdio 类型 Server。配置思路如下,不同平台字段名可能略有差异:

json
{
  "mcpServers": {
    "travel-tools": {
      "command": "python",
      "args": ["/opt/company/mcp/travel_mcp_server.py"],
      "env": {
        "EMPLOYEE_ID": "${EMPLOYEE_ID}",
        "INTERNAL_API_TOKEN": "${INTERNAL_API_TOKEN}"
      }
    }
  }
}

注册后,智能体工作流一般经历四步:识别用户意图、拉取 tools/list、由模型生成 name + arguments、宿主调用 tools/call。对于写操作,建议在工作流中增加确认节点,而不是让模型立即执行。

可复用的系统提示词片段如下:

text
你是企业差旅助手。调用工具时必须遵守:
1. 涉及制度查询,优先使用 travel_policy_search,不要编造公司标准。
2. 创建出差申请前,必须向用户确认城市、开始日期、结束日期和事由。
3. 调用 trip_request_create 时必须生成唯一 idempotency_key。
4. 工具返回错误时,先解释缺失字段或权限问题,再向用户索要补充信息。
5. 不得猜测预算、审批结果或报销金额。

生产化:从演示 Server 到企业服务 ​

演示代码适合理解协议,真正上线还需要补齐四类能力。

第一是传输与部署。stdio 适合本地 IDE 和桌面助手;多人共享的智能体更适合 Streamable HTTP 或网关模式,由平台统一处理连接、扩缩容和版本路由。

第二是身份透传。不要让所有请求共用一个超级 Token。客户端应把员工身份、租户 ID、会话 ID 传入 Server,Server 再换发短期、最小权限的内部 API Token。

第三是数据最小化。工具返回给模型的内容应经过字段白名单过滤。例如预算接口只返回余额,不返回部门负责人手机号;工单接口只返回摘要,不返回客户合同全文。

第四是可观测性。每次 tool-use 至少记录请求 ID、员工 ID、工具名、参数摘要、结果状态、耗时和错误码。参数中的敏感字段要脱敏,避免审计日志变成新的数据泄露源。

避坑总结:MCP 落地最容易踩的 5 个坑 ​

  1. 把调试信息打印到 stdout,导致协议解析失败。 现象是客户端提示 JSON 解析错误或连接立即断开。MCP stdio 通道中 stdout 只能返回协议消息,所有日志必须写 stderr,或接入统一日志系统。

  2. 工具描述过于笼统,模型频繁选错工具。 例如把描述写成“查询信息”,模型无法判断查制度还是查预算。修正方法是在 description 中写清适用意图、典型问法和边界,并为枚举字段提供业务含义。

  3. 参数 Schema 只写类型不写约束。 日期、金额、城市编码如果不限制格式,模型可能传“下周一”“大概三千”。应使用 format、enum、正则或服务端强校验,并在错误信息中返回可修复提示。

  4. 写接口没有幂等设计,重试后重复创建单据。 常见现象是网络超时后模型再次调用,生成两张出差申请。所有创建类工具都必须要求 idempotency_key,后端按键去重,而不是依赖模型记住是否成功。

  5. 把敏感数据和高权限动作一次性暴露给模型。 模型可能在总结时复述身份证号、薪资或客户信息。上线前应按工具分级、字段脱敏、权限校验和人工确认建立治理流程;删除、审批、支付不要做成自动 tool-use。

MCP 的核心收益不是“又多了一个协议”,而是让企业工具可以被安全、标准、可审计地复用。先从只读、低频、边界清晰的内部工具开始,跑通鉴权、Schema、审计和错误处理,再逐步接入写操作,才是智能体工作流落地最稳妥的路径。

免责声明

本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。