news 2026/9/5 19:51:12

MCP Everything Reference Server 工作原理详解:条件工具注册、资源订阅、会话级资源与模拟日志机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MCP Everything Reference Server 工作原理详解:条件工具注册、资源订阅、会话级资源与模拟日志机制

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实例,声明能力集:toolspromptsresources(含subscribe: truelistChanged: true)、loggingtasks,并附带从 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); };

两个值得注意的细节:

  1. 注册时机registerConditionalToolsoninitialized中同步调用,保证在notifications/initialized处理器结束前完成,因此配合服务器声明的tools.listChanged: true能力,客户端可以在工具列表变化时收到通知并重新拉取;
  2. roots 同步需要额外延迟:从源码结构看,syncRoots(实现在 roots.ts)通过 350ms 的setTimeout推迟到initialized通知处理完毕之后再发起请求,注释明确指出“否则请求会丢失”。这个定时器在cleanup()中通过clearTimeout(initializeTimeout)回收,避免会话结束时悬挂。

文档列出的三个条件工具是机制的核心示例,当前源码中registerConditionalTools注册的成员更多,还包括trigger-url-elicitationtrigger-sampling-request-asynctrigger-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),为SubscribeRequestSchemaUnsubscribeRequestSchema两类请求安装处理器:

  • 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 中有对应测试覆盖(setSubscriptionHandlersbeginSimulatedResourceUpdatesstopSimulatedResourceUpdates均被导入验证)。

会话级资源(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对象携带urinamemimeType(还可含descriptiontitleannotationsicons_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):

参数类型默认值说明
namestringREADME.md.gz输出文件名,用于拼接会话资源 URI
dataurl仓库 README 的 raw 地址要压缩的文件内容来源,支持 http/https/data URI
outputTypeenumresourceLinkresourceLink返回可后续读取的链接;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: Map<string | undefined, NodeJS.Timeout | undefined> = new Map<string | undefined, NodeJS.Timeout | undefined>();

beginSimulatedLogging(server, sessionId?)的工作方式:

  1. 构造 8 个不同级别的日志消息池:debuginfonoticewarningerrorcriticalalertemergency;每条消息若携带 sessionId 会追加- SessionId <id>后缀,便于演示时区分来源会话;
  2. 若该会话尚无 interval,则立即发送一条,随后setInterval5 秒随机抽取一条发送;
  3. 发送统一走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 所覆盖的四个主题可以归纳为两条主线:

  1. 能力协商时序:工厂阶段能做的立即做(无条件工具、资源、提示词、订阅处理器),必须等握手完成的(条件工具、roots 同步)放进oninitialized,并借助listChanged能力让客户端感知工具列表变化;
  2. 会话级状态治理:订阅表、模拟更新 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 19:44:54

GPT-4o Vision API实战:从本地图片识别到结构化输出的完整工作流

不需要引子铺垫&#xff0c;直接聊正经事。最近不少朋友拿着本地一堆图片问我&#xff1a;怎么才能让多模态大模型帮我把这些图里的信息自动整理出来&#xff1f;要真正落地跑通一个“本地图片识别 → 多模态 AI 分析 → 结构化输出”的工作流&#xff0c;大多数人卡住的地方根…

作者头像 李华
网站建设 2026/9/5 19:42:04

3步跑通微信聊天记录导出:把十年的对话完整存进自己硬盘

3步跑通微信聊天记录导出&#xff1a;把十年的对话完整存进自己硬盘 【免费下载链接】WeChatMsg 提取微信聊天记录&#xff0c;将其导出成HTML、Word、CSV文档永久保存&#xff0c;对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Trending/we/WeC…

作者头像 李华
网站建设 2026/9/5 19:40:36

AI检测为何不能直接读MP4?从视频解码到张量转换的完整链路解析

咱们直接聊一个很多做视觉算法的人都绕不过去的问题&#xff1a;你辛辛苦苦训练好的AI检测模型&#xff0c;为什么不能直接扔给它一个MP4文件让它识别&#xff1f;这个事我第一次接触的时候也懵过&#xff0c;想当然以为AI既然能“看”视频&#xff0c;那肯定能直接处理MP4。结…

作者头像 李华
网站建设 2026/9/5 19:38:50

Linux开源软件完整清单:按角色拿走你的工具栈

Linux开源软件完整清单&#xff1a;按角色拿走你的工具栈 【免费下载链接】Awesome-Linux-Software &#x1f427; A list of awesome Linux softwares 项目地址: https://gitcode.com/GitHub_Trending/aw/Awesome-Linux-Software 这是一份社区维护的 Linux 开源软件精…

作者头像 李华