构建你的第一个生产级 MCP 服务端

作者
  • avatar
    姓名
    Nino
    职业
    Senior Tech Editor

从一个简单的“Hello World”演示转向生产级的模型上下文协议(Model Context Protocol, MCP)服务端,是开发者构建 LLM 集成工具时最关键的阶段。虽然基础示例展示了如何回显字符串,但现实世界的应用——例如管理 Jira 实例或与数据库交互——需要一种更成熟的方法来处理状态管理、验证和错误处理。当通过 n1n.ai 使用 Claude 3.5 Sonnet 或 OpenAI o3 等高性能模型时,MCP 服务端的可靠性将成为整个用户体验的瓶颈。

在本指南中,我们将构建一个完整的 Jira 演示服务端,实现五个核心生产模式:共享数据一致性、严格的输入验证、协议安全日志记录、结构化错误报告以及动态上下文资源。

玩具级服务端与生产级模式的差距

基础的 MCP 服务端通常只包含一个没有状态的工具。然而,一个生产级服务端必须解决以下工程问题:

  1. 数据组合 (Data Composition):多个工具如何共享和修改同一个数据存储。
  2. 输入验证 (Input Validation):LLM 经常会幻觉出参数或传递格式错误的代码。
  3. 错误报告 (Error Reporting):区分“软性”可重试错误和“硬性”故障。
  4. 日志规范 (Logging):避免 stdout 污染,以免破坏 JSON-RPC 协议。
  5. 动态上下文 (Dynamic Context):在 LLM 调用工具之前,为其提供实时的元数据(如项目列表)。

为了测试这些模式,我们将使用一个模拟的 Jira 实现,它不需要真实的凭据,让你能够专注于架构本身。

实现共享数据存储

在生产环境中,你的工具不是孤立的函数。它们是共享状态的接口。在我们的 Jira 演示中,我们使用一个内存字典来模拟数据库。这使得 create_issueupdate_issue 等工具能够与同一个单一事实来源(Source of Truth)进行交互。

# 共享内存数据存储
ISSUES: dict[str, dict] = {
    "PROJ-101": {
        "key": "PROJ-101",
        "summary": "parseInput() 中的 NullPointerException,当 config 为 null 时",
        "status": "Open",
        "priority": "P1",
        "issue_type": "Bug",
        "assignee": "alice"
    },
}

PROJECTS = {
    "PROJ": {"name": "核心引擎", "lead": "alice"},
    "MOBILE": {"name": "移动端应用", "lead": "bob"},
    "INFRA": {"name": "基础架构", "lead": "charlie"}
}

通过集中化这种状态,我们确保了当 LLM 通过 n1n.ai 随后调用 get_issuesearch_issues 时,对 update_issue 的工具调用能立即得到体现。

模式 1:集中式验证逻辑

LLM 是概率性的。它们可能会尝试更新一个不存在的问题,或者使用它们从未见过的项目键。不要在每个工具中重复验证逻辑,而应将其提取到辅助函数中。这可以确保一致性并减少 Bug。

def validate_issue_key(key: str) -> str | None:
    """如果键无效则返回错误消息,如果有效则返回 None。"""
    if not key or "-" not in key:
        return f"无效的问题键格式:'{key}'。预期格式:PROJECT-123"

    project = key.split("-")[0]
    if project not in PROJECTS:
        return f"未知项目:'{project}'。可用项目:{', '.join(PROJECTS)}"

    if key not in ISSUES:
        return f"未找到问题 '{key}'"
    return None

模式 2:针对 LLM 的业务规则验证

当 LLM 调用工具时,输入可能在语法上是正确的(例如是一个字符串),但在语义上是无效的(例如是不受支持的状态)。我们必须显式地验证这些业务规则。例如,如果状态不在我们的 VALID_STATUSES 集合中,我们应该返回一个清晰的错误消息,帮助 LLM 自我纠正。

VALID_STATUSES = {"Open", "In Progress", "In Review", "Done", "Closed"}

def validate_status(new_status: str):
    if new_status and new_status not in VALID_STATUSES:
        return [TextContent(
            type="text",
            text=f"无效状态:'{new_status}'。有效值:{', '.join(sorted(VALID_STATUSES))}",
            isError=True
        )]
    return None

模式 3:正确的错误处理(isError=True vs False)

MCP 允许你使用 isError=True 标记响应。这是给 LLM 的一个信号,表明操作失败了。

  • 使用 isError=True 的场景:权限被拒绝、ID 格式错误或缺少必填字段。这告诉 LLM 停止当前操作并报告错误,或者尝试完全不同的方法。
  • 使用 isError=False 的场景:搜索结果为空或“未找到”但输入有效的情况。这允许 LLM 将结果解释为成功的执行但返回为空,从而提示其尝试使用不同的参数进行重试。

使用像 n1n.ai 这样可靠的 API 桥接器可以确保这些错误标志正确传递到基础模型,从而实现更智能的恢复。

模式 4:协议安全日志记录

MCP 开发中的一个常见错误是使用 print()。在 MCP 中,stdout 是为 JSON-RPC 通信流保留的。任何多余的 print 语句都会破坏 JSON 格式并导致连接崩溃。你 必须 将所有日志重定向到 stderr

import logging
import sys

logging.basicConfig(
    stream=sys.stderr, # 关键:必须是 stderr
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger("jira-mcp")

# 安全的日志记录
logger.info("正在执行工具:create_issue,摘要为:%s", summary)

模式 5:动态资源提供上下文

不要在工具描述中硬编码项目名称,而应使用 MCP 资源(Resources)。这允许 LLM 在进行工具调用之前“读取”系统的当前状态。像 jira://projects 这样的资源为 LLM 提供了必要的上下文,以避免使用无效的项目键。

@server.list_resources()
async def list_resources() -> list[Resource]:
    return [Resource(
        uri="jira://projects",
        name="可用项目",
        description="Jira 项目列表。在调用工具前阅读此内容以了解有效的项目键。",
        mimeType="application/json"
    )]

@server.read_resource()
async def read_resource(uri: str) -> str:
    if str(uri) == "jira://projects":
        data = [{"key": k, "name": v["name"]} for k, v in PROJECTS.items()]
        return json.dumps(data, indent=2)

总结与最佳实践

构建生产级 MCP 服务端需要将 LLM 视为一个会犯错误的“用户”。通过实现共享状态、严格验证和动态资源,你可以创建一个稳健的接口,让 Claude 或 GPT-4o 等模型能够可靠地执行复杂任务。

在部署这些服务端时,请确保你的 API 基础设施能够胜任。高延迟或不稳定的 API 连接会导致 MCP 超时。使用像 n1n.ai 这样的聚合器可以提供无缝工具调用工作流所需的稳定性和速度。

n1n.ai 获取免费 API 密钥。