构建一个模型上下文协议服务器常会遇到一个架构难题:当您需要提供动态数据时,是应该将其包装成一个工具,还是定义为一个资源?在之前的章节中,我们已明确工具是可执行函数,而资源是只读数据。然而,当数据需要即时计算或根据特定参数从数据库中获取时,这种界限就变得模糊了。计算资源是一种不映射到磁盘上静态文件的资源。相反,其内容是在请求时通过程序生成的。这使您可以使用与静态文本文件相同的标准化URI接口,提供动态系统状态,例如当前内存使用量、数据库表中的最新行或即时的API响应。架构上的区别决定是实现工具还是计算资源,这会根本性地改变大型语言模型(LLM)与您的数据交互的方式。这个选择取决于交互是作为函数调用(RPC)还是数据获取(类似REST的GET)建模更合适。当您定义一个工具时,您赋予模型自主权。模型明确决定调用该工具,并根据对话上下文选择参数。这是一个主动过程,适用于可能失败、需要复杂参数或改变系统状态的操作。当您定义一个计算资源时,您为模型提供上下文。模型会看到一个URI(统一资源标识符),并将内容视为参考资料的一部分。这是一个被动过程,适用于标识特定实体且可以安全读取而无副作用的数据。下面的决策树概括了选择这些基本要素的逻辑。digraph G { rankdir=TB; node [shape=rect, style=filled, fontname="Arial", fontsize=12, margin=0.2]; edge [fontname="Arial", fontsize=10, color="#adb5bd"]; start [label="数据访问需求", fillcolor="#e9ecef", color="#dee2e6"]; side_effect [label="它是否修改系统状态\n(创建、更新、删除)?", fillcolor="#a5d8ff", color="#74c0fc"]; complex_args [label="它是否需要多个\n复杂参数?", fillcolor="#a5d8ff", color="#74c0fc"]; identity [label="数据是否由唯一的ID或路径\n标识?", fillcolor="#a5d8ff", color="#74c0fc"]; tool_node [label="作为工具实现", fillcolor="#ffc9c9", color="#ff8787", shape=note]; resource_node [label="作为计算资源\n实现", fillcolor="#b2f2bb", color="#69db7c", shape=note]; start -> side_effect; side_effect -> tool_node [label="是"]; side_effect -> complex_args [label="否(只读)"]; complex_args -> tool_node [label="是(结构化JSON)"]; complex_args -> identity [label="否(单一ID/路径)"]; identity -> resource_node [label="是(可寻址的)"]; identity -> tool_node [label="否(搜索/查询)"]; }区分工具和资源时的逻辑流程,基于状态修改、参数复杂度和数据标识。实现动态URI要实现计算资源,您必须依赖URI模板。与每个文件都有固定路径的静态资源不同,计算资源使用模式将请求路由到处理函数。例如,在Python SDK中,您可以使用通用标识符定义资源模板。如果您正在构建一个监控服务器指标的系统,您可以定义一个URI方案,例如system://metrics/{host}。当客户端请求system://metrics/localhost时,您的处理程序会解析{host}变量,并执行逻辑以获取该特定主机的CPU和内存统计信息。假设我们要从数据库中提供用户资料。我们可以创建一个名为get_user_profile(user_id)的工具,但由于这是一个只读操作,返回标准文档,因此资源模式通常更合适。实现过程包含两个不同的阶段:列出模式和处理读取请求。阶段1:提供能力首先,服务器必须通知客户端它可以处理这些动态请求。这在resources/list能力交换期间完成。您提供一个模板,而不是每个可能用户的具体列表。阶段2:处理请求当服务器收到resources/read请求时,它会将传入的URI与您注册的模板进行匹配。如果找到匹配项,它会提取变量并将它们传递给您的逻辑处理程序。$$URI_{request} = \text{方案} + \text{://} + \text{路径} + \text{变量}$$以下是一个Python中计算资源的结构示例,用于动态获取用户数据:from mcp.server.fastmcp import FastMCP import sqlite3 mcp = FastMCP("UserDirectory") @mcp.resource("users://{user_id}/profile") def get_user_profile(user_id: str) -> str: """ 动态地从数据库中获取用户资料。 """ # 连接到数据库(在生产环境中,请使用连接池) conn = sqlite3.connect("users.db") cursor = conn.cursor() # 安全的参数替换可防止SQL注入 cursor.execute("SELECT name, email, role FROM users WHERE id = ?", (user_id,)) row = cursor.fetchone() conn.close() if row: return f"Name: {row[0]}\nEmail: {row[1]}\nRole: {row[2]}" else: raise ValueError(f"User {user_id} not found.")在此示例中,@mcp.resource装饰器充当路由器。字符串users://{user_id}/profile指示MCP服务器在客户端请求与该模式匹配的URI时调用此函数。上下文窗口与订阅使用计算资源而非工具的一个主要益处是它与资源订阅模型的结合。如果数据经常变化,资源允许客户端订阅更新。如果您使用工具获取股票价格,LLM会收到该时刻价格的快照。如果价格在五秒后变化,模型无法知晓,除非它决定再次调用该工具。如果您使用计算资源(例如,stock://AAPL/price),客户端可以订阅此URI。当您的内部逻辑检测到价格变化时,您可以向客户端发送通知。客户端随后可以自动获取新内容并更新上下文窗口,而无需LLM采取任何行动。这形成了一个响应式数据循环,仅凭工具难以实现。复杂性与验证尽管资源功能强大,但它们缺少工具所具有的输入验证模式。工具使用JSON Schema(通常通过Pydantic生成)对参数强制执行严格类型。资源完全依赖于URI的字符串解析。如果您的数据获取需要复杂的筛选,例如“查找上周注册并拥有高级订阅的所有用户”,将这些参数打包到URI字符串中会变得笨拙且非标准化。$$URI = \text{users://search?after=2023-10-01&tier=premium}$$尽管上述URI是有效的,但在资源处理程序中手动解析查询字符串与工具定义提供的结构化验证相比,更容易出错。因此,如果数据访问需要一个或两个以上简单的标识符参数,或者参数是可选且组合性的,那么工具是更优的架构选择。混合方法复杂的MCP实现通常采用混合方法。您可以提供一个用于搜索和发现的工具,它会返回一个资源URI列表。工具: search_users(query="engineering")返回一个简化对象列表:[ {"name": "Alice", "uri": "users://101/profile"}, {"name": "Bob", "uri": "users://102/profile"} ]资源: LLM读取工具的输出,看到URI,然后可以在确定特定细节相关时请求users://101/profile的全部内容。这种职责分离使您的工具侧重于“查找”,资源侧重于“读取”,从而优化了上下文窗口的使用和应用程序的逻辑流程。