MCP Everything Reference Server 工作原理详解:条件工具注册、资源订阅、会话级资源与模拟日志机制 📅 发布时间:2026/9/5 19:51:22 👁 浏览次数: 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: Mapstring, Setstring | undefined new Map(); // Interval to send notifications to subscribers const subsUpdateIntervals: Mapstring | undefined, NodeJS.Timeout | undefined new Map();subscriptions以资源 URI 为键值为订阅该 URI 的会话 ID 集合Mapuri, SetsessionId与文档描述一致。注意会话 ID 允许为undefined——stdio 传输下没有会话 IDsubsUpdateIntervals记录每个会话的模拟更新定时器保证同一会话最多只有一个活跃 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: Setstring | 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 ResourcesURI 生成与注册resources/session.ts会话级资源的实现在 src/everything/resources/session.ts提供两个导出函数1.getSessionResourceURI(name)—— 构造固定格式的会话资源 URIexport const getSessionResourceURI (name: string): string { return demo://resource/session/${name}; };2.registerSessionResource(server, resource, type, payload)—— 注册一个仅存活于当前会话的资源返回resource_linkresource对象携带uri、name、mimeType还可含description、title、annotations、icons、_meta等元数据type只接受text | blobpayload作为字符串传入内容会装入对应字段text 资源返回{ uri, mimeType, text: payload }blob 资源返回{ uri, mimeType, blob: payload }base64资源通过server.registerResource(...)注册读取回调直接从内存中的resourceContent返回——内容不落盘、不持久化仅服务于会话生命周期。一个容易踩坑的细节在源码注释中写得很清楚session.ts模块内维护registeredResources: Mapstring, 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参数类型默认值说明namestringREADME.md.gz输出文件名用于拼接会话资源 URIdataurl仓库 README 的 raw 地址要压缩的文件内容来源支持 http/https/data URIoutputTypeenumresourceLinkresourceLink返回可后续读取的链接resource返回完整内联资源抓取环节还有三组环境变量控制的安全边界环境变量默认值作用GZIP_MAX_FETCH_SIZE10 MB单次抓取允许的最大字节数GZIP_MAX_FETCH_TIME_MILLIS30000抓取超时毫秒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: Mapstring | undefined, NodeJS.Timeout | undefined new Mapstring | 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.tstoggle-simulated-logging调用上述 begin/stop 函数切换传输断开兜底任意传输stdio 收到SIGINT、SSE 的onclose、Streamable HTTP 的DELETE等见 startup.md 的传输管理器说明断开时都会触发工厂返回的cleanup(sessionId?)。工厂函数中的cleanup一次性完成四件事server/index.tscleanup: (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),仅供参考