Implementing a Secure MCP Server

Implementing a Secure MCP Server

实现安全的 MCP 服务器

An MCP server often starts with a seemingly simple requirement: an AI host should find and summarise tickets about a topic. Once current domain data enters a model context, the endpoint is no longer the central question. What matters is which data becomes visible, under whose permissions it is searched, and which actions the server is allowed to expose at all. MCP 服务器的开发往往始于一个看似简单的需求:AI 主机需要查找并总结关于某个主题的工单。一旦领域数据进入模型上下文,端点本身就不再是核心问题。真正重要的是哪些数据可见、在何种权限下进行搜索,以及服务器被允许暴露哪些操作。

This question arose while implementing MCP access for Lutions, a private development project centred on a web application for project and ticket work. There was no security incident behind the decision that followed. Before the first production tool existed, the architectural position was clear: the MCP server must not become a second open product API or an autonomous agent layer. 在为 Lutions(一个专注于项目和工单管理的 Web 应用私人开发项目)实现 MCP 访问时,这一问题随之浮现。随后的决策并非源于任何安全事故。在第一个生产工具诞生之前,架构立场就已经明确:MCP 服务器绝不能成为第二个开放的产品 API 或自主代理层。

The first article in this series explains the path between host, model, MCP server, and domain system (Hermanns, 2026e). This retrospective shows how that position was put into practice in access design, domain permissions, and operations. For orientation, the host is the AI application that passes a user question and permitted MCP capabilities to a model. The MCP server then supplies bounded context or performs a permitted domain operation; it does not answer the user’s question itself. 本系列的第一篇文章解释了主机、模型、MCP 服务器和领域系统之间的路径(Hermanns, 2026e)。本文回顾了这一立场如何在访问设计、领域权限和运维中得到实践。作为背景说明,主机是指将用户问题和允许的 MCP 功能传递给模型的 AI 应用程序。MCP 服务器随后提供有界上下文或执行允许的领域操作;它本身并不直接回答用户的问题。

Cut the access surface before adding capabilities

在增加功能前缩减访问面

MCP separates resources, prompts, and tools. Tools are functions a model may request through a host; resources and prompts serve different roles in constructing context. The current specification treats these server capabilities separately and identifies tools as a security-relevant, model-controlled interface (Model Context Protocol, 2026a; Model Context Protocol, 2026b). MCP 将资源、提示词和工具分离开来。工具是模型可以通过主机请求的函数;资源和提示词在构建上下文时扮演不同的角色。当前的规范将这些服务器功能分开处理,并将工具定义为一种与安全相关、由模型控制的接口(Model Context Protocol, 2026a; Model Context Protocol, 2026b)。

In Lutions, this led to a simple but consequential choice: the production core starts with six read-only tools. They search visible tickets, load individual or multiple tickets, and return bounded comments, links, or a compact context bundle. The server describes itself accordingly as read-only, and the status view lists the same six tools as read-only. 在 Lutions 中,这导致了一个简单但影响深远的决策:生产核心从六个只读工具开始。它们负责搜索可见工单、加载单个或多个工单,并返回有限的评论、链接或紧凑的上下文包。服务器相应地将自身描述为只读,状态视图也将这六个工具列为只读。

This boundary reduces several uncertainties at once. A read tool can still touch sensitive information, so it needs the same visibility and project boundaries as the application itself. But it does not change a ticket’s workflow, ownership, priority, or external effect. The first security question therefore remains manageable: may this token see this information—and may this exact information enter the host context? 这一边界同时减少了多个不确定性。只读工具仍可能触及敏感信息,因此它需要与应用程序本身相同的可见性和项目边界。但它不会改变工单的工作流、所有权、优先级或外部影响。因此,首要的安全问题依然是可控的:该令牌是否有权查看此信息——以及这些特定信息是否可以进入主机上下文?

The narrow tool surface also helps the model. It is not a security control in itself; tool descriptions are not authorisation decisions. But it reduces misuse and makes it possible to align tool lists more closely with the actual permission context (Model Context Protocol, 2026b). 狭窄的工具面也对模型有帮助。它本身不是一种安全控制;工具描述并不等同于授权决策。但它减少了滥用的可能性,并使工具列表能够更紧密地与实际权限上下文对齐(Model Context Protocol, 2026b)。

One tool in practice: search, not free-form access

实践中的工具:搜索,而非自由访问

The gain is not only in the number of tools, but in the contract of each one. For example, search_zockets accepts no universal command. It accepts bounded search text, a result limit from 1 to 50, and optional domain filters. 收益不仅在于工具的数量,还在于每个工具的契约。例如,search_zockets 不接受通用命令。它只接受有界的搜索文本、1 到 50 的结果限制以及可选的领域过滤器。

This is a simplified, readable version of the server-side schema: 以下是服务器端模式的简化、可读版本:

{
  "name": "search_zockets",
  "readOnly": true,
  "input": {
    "query": "string, maximum 200 characters",
    "limit": "integer, 1 to 50, default 10",
    "status": "optional",
    "priority": "optional",
    "type": "optional",
    "projectKey": "optional"
  }
}

The schema does not replace authorisation. It does, however, prevent search from becoming a hidden “perform arbitrary access” function. Server-side validation and the user context still determine which results can be returned at all. 该模式不能取代授权。然而,它确实防止了搜索功能变成隐藏的“执行任意访问”函数。服务器端验证和用户上下文仍然决定了最终能返回哪些结果。

What a request becomes

请求的演变

A user might ask for open risks and decisions on a topic. The host first uses search_zockets to find visible candidates. Only then does it use get_zocket_context_bundle to load a bounded selection of tickets, optionally visible comments, and links. The model’s answer is created only from that limited material. 用户可能会询问关于某个主题的公开风险和决策。主机首先使用 search_zockets 查找可见的候选对象。然后,它才使用 get_zocket_context_bundle 加载有限的工单选择、可选的可见评论和链接。模型的回答仅基于这些有限的材料生成。

In Lutions, research follows a clear sequence: search first, retrieve targeted context second, answer last. This does not prevent a model from misunderstanding content. It does prevent a request from beginning with unbounded access to all tickets or a blanket full-text export. That is the first practical benefit of a read core: current ticket information is available for analysis without abandoning the research boundary or domain permissions. 在 Lutions 中,研究遵循明确的顺序:先搜索,再检索目标上下文,最后回答。这并不能防止模型误解内容,但确实防止了请求从对所有工单的无限制访问或全量全文导出开始。这是只读核心的第一个实际好处:在不放弃研究边界或领域权限的前提下,当前的工单信息可用于分析。

Permissions remain in the domain system

权限保留在领域系统中

An MCP connection does not create its own trust zone. It must not reveal tickets or information to an AI host that the associated user could not see in Lutions directly. The Lutions endpoint therefore uses the existing API-token authentication. For read access, issues:read is the domain basis. mcp:connect additionally marks MCP access and can be enforced after the documented migration path. Project-bound tokens remain limited to their permitted projects over MCP as well, and the endpoint has its own rate limit. MCP 连接不会创建自己的信任区。它绝不能向 AI 主机泄露关联用户在 Lutions 中无法直接查看的工单或信息。因此,Lutions 端点使用现有的 API 令牌认证。对于只读访问,issues:read 是领域基础。mcp:connect 额外标记了 MCP 访问,并可在记录的迁移路径后强制执行。项目绑定令牌在 MCP 上也仅限于其允许的项目,且端点拥有自己的速率限制。

Figure 1: Test configuration for a read-only MCP connection with mcp:connect and issues:read. Token secrets are not displayed in either the UI or this article. 图 1:使用 mcp:connectissues:read 的只读 MCP 连接测试配置。令牌密钥未在 UI 或本文中显示。

The MCP server does not invent its own permission model. It translates a request into a domain operation and must respect the same boundaries that apply outside MCP. This follows a core MCP security principle: data access and actions require effective access controls, and the protocol does not enforce that architecture by itself (Model Context Protocol, 2026a). In retrospect, this is one of the most durable decisions. MCP is an additional integration surface, not a bypass around the UI, API, and policies. Without that boundary, scopes easily become labels and tool descriptions become false security promises. MCP 服务器不会发明自己的权限模型。它将请求转换为领域操作,并必须遵守 MCP 之外适用的相同边界。这遵循了 MCP 的核心安全原则:数据访问和操作需要有效的访问控制,而协议本身并不强制执行该架构(Model Context Protocol, 2026a)。回想起来,这是最持久的决策之一。MCP 是一个额外的集成面,而不是绕过 UI、API 和策略的捷径。如果没有这个边界,作用域很容易变成标签,工具描述也会变成虚假的安全承诺。

Audit should explain, not duplicate

审计应解释,而非复制

Tool calls quickly create a temptation to log everything. For a ticket system, that would be the wrong reflex. Complete arguments, ticket descriptions, comment text, and result payloads would turn audit data into a second, difficult-to-control copy of domain content. Lutions therefore records a tool call as mcp 工具调用很快会让人产生记录一切的冲动。对于工单系统来说,这是一种错误的本能。完整的参数、工单描述、评论文本和结果负载会将审计数据变成领域内容的第二个、难以控制的副本。因此,Lutions 将工具调用记录为 mcp…