MCP Everything Reference Server 工作原理详解:条件工具注册、资源订阅、会话级资源与模拟日志机制
【免费下载链接】serversModel Context Protocol Servers项目地址: https://gitcode.com/GitHub_Trending/se/servers
本文基于 modelcontextprotocol servers 仓库中 Everything Server 的官方文档 how-it-works.md 及其对应源码,深入讲解该参考服务器的四大核心运行机制:基于客户端能力协商的条件工具注册、按 URI 追踪订阅者的资源订阅管理、仅存活于会话生命周期的会话级资源注册,以及尊重客户端日志级别设置的模拟日志推送。读完本文,你将理解 Everything Server 如何在 MCP 初始化握手之后完成能力协商与延迟注册,以及如何通过会话级状态管理安全地模拟订阅更新与日志流。
Everything Server 是整个 MCP servers 仓库中用于演示协议全部能力的参考实现。它的工厂函数createServer()定义在 server/index.ts,由三种传输管理器(stdio / sse / streamableHttp,入口见 index.ts)调用。工厂执行期间完成以下工作(对应 startup.md 中的“Server Factory”部分):
- 创建
McpServer实例,声明能力集:tools、prompts、resources(含subscribe: true与listChanged: true)、logging、tasks,并附带从 instructions.md 加载的服务器说明; - 立即注册全部工具(
registerTools)、资源(registerResources)与提示词(registerPrompts); - 安装资源订阅处理器
setSubscriptionHandlers(server); - 返回
server实例与cleanup(sessionId?)回调,用于在会话结束时停止所有模拟定时器并清理会话级状态。
这里有一个关键设计前提:大部分工具在 Server Factory 执行期间就立即注册,早于任何客户端连接;但有少数工具依赖客户端是否声明了特定能力,只能等初始化握手完成后才注册。以下按文档脉络逐节展开。
条件工具注册(Conditional Tool Registration)
问题背景:客户端能力在握手之前不可知
MCP 协议中,部分工具只有在客户端具备相应能力时才有意义:
get-roots-list:需要客户端支持 roots 能力;trigger-elicitation-request:需要客户端支持 elicitation(向用户发起交互请求)能力;trigger-sampling-request:需要客户端支持 sampling(由客户端调用 LLM 采样)能力。
而客户端的能力声明(capabilities)只有在初始化握手完成后才能从initialize响应中得知,因此这些工具不能在工厂执行阶段就注册。
实现:oninitialized处理器中延迟注册
源码 src/everything/tools/index.ts 将注册拆分为两个函数:registerTools(server)负责在工厂阶段注册的 13 个“无条件”工具(echo、get-env、get-sum、get-tiny-image、gzip-file-as-resource 等),registerConditionalTools(server)负责需要能力协商的工具。延迟注册的触发点在 src/everything/server/index.ts:
// Perform post-initialization operations server.server.oninitialized = async () => { // Register conditional tools now that client capabilities are known. // This finishes before the `notifications/initialized` handler finishes. registerConditionalTools(server); // Sync roots if the client supports them. // This is delayed until after the `notifications/initialized` handler finishes, // otherwise, the request gets lost. const sessionId = server.server.transport?.sessionId; initializeTimeout = setTimeout(() => syncRoots(server, sessionId), 350); };两个值得注意的细节:
- 注册时机:
registerConditionalTools在oninitialized中同步调用,保证在notifications/initialized处理器结束前完成,因此配合服务器声明的tools.listChanged: true能力,客户端可以在工具列表变化时收到通知并重新拉取; - roots 同步需要额外延迟:从源码结构看,
syncRoots(实现在 roots.ts)通过 350ms 的setTimeout推迟到initialized通知处理完毕之后再发起请求,注释明确指出“否则请求会丢失”。这个定时器在cleanup()中通过clearTimeout(initializeTimeout)回收,避免会话结束时悬挂。
文档列出的三个条件工具是机制的核心示例,当前源码中registerConditionalTools注册的成员更多,还包括trigger-url-elicitation、trigger-sampling-request-async、trigger-elicitation-request-async以及基于实验性 tasks API 的simulate-research-query——这说明该注册机制是可扩展的:任何依赖客户端能力的工具都应归入这一函数。
资源订阅(Resource Subscriptions)
数据模型:按 URI 追踪订阅者
订阅状态维护在 src/everything/resources/subscriptions.ts 的两个模块级 Map 中:
// Track subscriber session id lists by URI const subscriptions: Map<string, Set<string | undefined>> = new Map(); // Interval to send notifications to subscribers const subsUpdateIntervals: Map<string | undefined, NodeJS.Timeout | undefined> = new Map();subscriptions以资源 URI 为键,值为订阅该 URI 的会话 ID 集合(Map<uri, Set<sessionId>>,与文档描述一致)。注意会话 ID 允许为undefined——stdio 传输下没有会话 ID;subsUpdateIntervals记录每个会话的模拟更新定时器,保证同一会话最多只有一个活跃 interval。
订阅/退订处理器:setSubscriptionHandlers
工厂函数在 server/index.ts 中调用setSubscriptionHandlers(server),为SubscribeRequestSchema和UnsubscribeRequestSchema两类请求安装处理器:
- Subscribe 处理器:从请求中提取
uri,从extra.sessionId提取会话 ID,先发一条 info 级日志确认收到订阅,然后把会话 ID 加入对应 URI 的订阅者集合; - Unsubscribe 处理器:同样先记录日志,再从对应 URI 的集合中移除该会话 ID。
按需启停的模拟更新:toggle-subscriber-updates工具
订阅本身是被动状态,模拟更新则由工具 tools/toggle-subscriber-updates.ts 控制:
- 工具内部维护
clients: Set<string | undefined>记录当前处于“开启”状态的会话; - 会话首次调用时,执行
beginSimulatedResourceUpdates(server, sessionId):立即发送一轮更新,然后以5 秒周期setInterval持续推送; - 再次调用则
stopSimulatedResourceUpdates(sessionId)清除定时器,工具返回文本会明确提示“Started/Stopped simulated resource updates for session …”; - 会话断开或
cleanup(sessionId?)被调用时,stopSimulatedResourceUpdates(sessionId)会清除 interval 并移除该会话的会话级状态。
sendSimulatedResourceUpdates的推送逻辑(subscriptions.ts)值得细看:它遍历subscriptions中的全部 URI,若某 URI 的订阅者集合包含目标会话,则通过server.server.notification({ method: "notifications/resources/updated", params: { uri } })发送资源更新通知;若集合中已不包含该会话,则顺手将其删除——这是一种被动清理已断连订阅者的机制。相关行为在tests/resources.test.ts 中有对应测试覆盖(setSubscriptionHandlers、beginSimulatedResourceUpdates、stopSimulatedResourceUpdates均被导入验证)。
会话级资源(Session‑scoped Resources)
URI 生成与注册:resources/session.ts
会话级资源的实现在 src/everything/resources/session.ts,提供两个导出函数:
1.getSessionResourceURI(name)—— 构造固定格式的会话资源 URI:
export const getSessionResourceURI = (name: string): string => { return `demo://resource/session/${name}`; };2.registerSessionResource(server, resource, type, payload)—— 注册一个仅存活于当前会话的资源,返回resource_link:
resource对象携带uri、name、mimeType(还可含description、title、annotations、icons、_meta等元数据);type只接受"text" | "blob",payload作为字符串传入,内容会装入对应字段:text 资源返回{ uri, mimeType, text: payload },blob 资源返回{ uri, mimeType, blob: payload }(base64);- 资源通过
server.registerResource(...)注册,读取回调直接从内存中的resourceContent返回——内容不落盘、不持久化,仅服务于会话生命周期。
一个容易踩坑的细节在源码注释中写得很清楚(session.ts):模块内维护registeredResources: Map<string, RegisteredResource>,注册前若发现同一 URI 已存在,会先调用existingResource.remove()再重新注册。这是为了避免工具在会话内多次用相同 URI 创建资源时抛出 “Resource already registered” 错误。
设计意图:工具按需产出会话内工件
文档给出的典型用法正是 tools/gzip-file-as-resource.ts 实现的gzip-file-as-resource工具:拉取一个 URL 的内容,用 Node.js 内置gzipSync压缩,以mimeType: application/gzip注册为会话资源,并按参数outputType二选一返回:
resourceLink(默认):返回resource_link,客户端可以在会话内的后续请求中通过该链接读取资源;resource:直接在工具结果中内联返回完整资源对象{ uri, mimeType, blob }。
该工具的输入 schema 与可调参数(gzip-file-as-resource.ts):
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name | string | README.md.gz | 输出文件名,用于拼接会话资源 URI |
data | url | 仓库 README 的 raw 地址 | 要压缩的文件内容来源,支持 http/https/data URI |
outputType | enum | resourceLink | resourceLink返回可后续读取的链接;resource返回完整内联资源 |
抓取环节还有三组环境变量控制的安全边界:
| 环境变量 | 默认值 | 作用 |
|---|---|---|
GZIP_MAX_FETCH_SIZE | 10 MB | 单次抓取允许的最大字节数 |
GZIP_MAX_FETCH_TIME_MILLIS | 30000 | 抓取超时(毫秒) |
GZIP_ALLOWED_DOMAINS | 空(允许所有域名) | 逗号分隔的域名白名单,支持子域匹配 |
从源码结构看,fetchSafely先校验协议(仅 http/https/data)与域名白名单,再同时检查Content-Length头与实际读取字节数——注释明确指出不能信任对端返回的 Content-Length,必须监控实际读取量,超限即取消流并抛错。这套“会话级资源 + 安全抓取”的组合,展示了 MCP 工具如何在不持久化的前提下产出可被客户端二次读取的大对象。
模拟日志(Simulated Logging)
实现:server/logging.ts
模拟日志实现在 src/everything/server/logging.ts,模块级 MaplogsUpdateIntervals记录每个会话的日志定时器:
const logsUpdateIntervals: Map<string | undefined, NodeJS.Timeout | undefined> = new Map<string | undefined, NodeJS.Timeout | undefined>();beginSimulatedLogging(server, sessionId?)的工作方式:
- 构造 8 个不同级别的日志消息池:
debug、info、notice、warning、error、critical、alert、emergency;每条消息若携带 sessionId 会追加- SessionId <id>后缀,便于演示时区分来源会话; - 若该会话尚无 interval,则立即发送一条,随后
setInterval每5 秒随机抽取一条发送; - 发送统一走
server.sendLoggingMessage({ level, data }, sessionId?)。这一点是文档强调的关键:通过 SDK 的sendLoggingMessage发送,客户端配置的最低日志级别会被 SDK 自动遵守,低于该级别的模拟消息不会下发到客户端;
stopSimulatedLogging(sessionId?)则清除对应 interval 并从 Map 中删除记录。
触发与清理链路
- 按需启停:由工具 tools/toggle-simulated-logging.ts(
toggle-simulated-logging)调用上述 begin/stop 函数切换; - 传输断开兜底:任意传输(stdio 收到
SIGINT、SSE 的onclose、Streamable HTTP 的DELETE等,见 startup.md 的传输管理器说明)断开时都会触发工厂返回的cleanup(sessionId?)。工厂函数中的cleanup一次性完成四件事(server/index.ts):
cleanup: (sessionId?: string) => { // Stop any simulated logging or resource updates that may have been initiated. stopSimulatedLogging(sessionId); stopSimulatedResourceUpdates(sessionId); // Clean up task store timers taskStore.cleanup(); if (initializeTimeout) clearTimeout(initializeTimeout); },这正是文档所述“transport disconnect triggerscleanup()which also stops any active intervals”的代码依据:模拟日志与模拟订阅更新不会在客户端断开后继续空转,tasks 定时器和 roots 同步定时器也一并回收。
小结与延伸阅读
Everything Server 用一个参考实现串起了 MCP 协议的几类易错机制,how-it-works.md 所覆盖的四个主题可以归纳为两条主线:
- 能力协商时序:工厂阶段能做的立即做(无条件工具、资源、提示词、订阅处理器),必须等握手完成的(条件工具、roots 同步)放进
oninitialized,并借助listChanged能力让客户端感知工具列表变化; - 会话级状态治理:订阅表、模拟更新 interval、日志 interval、会话资源注册表全部按
sessionId(允许undefined以兼容 stdio)为键管理,并通过统一的cleanup(sessionId?)在连接断开时集中回收,防止定时器泄漏与重复注册错误。
验证方面,仓库提供了配套测试:resources.test.ts 覆盖会话资源与订阅管理,server.test.ts、tools.test.ts、registrations.test.ts 分别覆盖服务器工厂、工具注册与整体注册行为。更多背景可继续阅读该目录下的配套文档:architecture.md、structure.md、startup.md、features.md、extension.md 与 instructions.md。
【免费下载链接】serversModel Context Protocol Servers项目地址: https://gitcode.com/GitHub_Trending/se/servers
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考