Effect 4.0 MCP 服务器日志能力:从 logging 能力广告到按客户端日志级别过滤的完整实现
2026/9/15 17:19:03 网站建设 项目流程

Effect 4.0 MCP 服务器日志能力:从 logging 能力广告到按客户端日志级别过滤的完整实现

【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect

导读:本篇文章围绕 Effect 仓库中 changeset.changeset/pre/fair-logs-listen.md所记录的effect: patch——"MCP servers now advertise logging and honor each client's selected log level when sending log notifications"——深入剖析 Effect 4.0 中 MCP 服务器日志功能的完整实现链路。你将看到:MCP 协议中日志能力如何被声明、客户端如何通过logging/setLevel选择接收级别、服务器如何按客户端阈值过滤notifications/message通知,以及这一切如何与 Effect 自身的LogLevel体系衔接。读完本文,你将能在自己的 MCP 服务器中实现"按客户端定制日志输出"的生产级能力。

一次 patch 背后的能力闭环

changeset 是 Changesets 驱动的版本发布声明,fair-logs-listen.md描述的是effect包的一次patch级变更,其核心语义可以拆解为两个动作:

  1. advertise logging:MCP 服务器在initialize握手阶段,向客户端声明自己支持日志发送能力(capabilities.logging)。
  2. honor each client's selected log level:服务器在发送日志通知时,会尊重每个客户端各自通过logging/setLevel选择的日志级别阈值,只投递"达到或超过该阈值"的日志。

这两个动作虽然描述简短,但对应了仓库中一条完整的实现链路:协议 Schema(McpSchema.ts)→ 运行时生命周期(internal/mcpRuntime.ts)→ 会话级状态管理(internal/mcpStatefulRuntime.ts)→ 通知投影与投递(internal/mcpProtocol.tsMcpServer.ts),并有配套的 conformance 测试(packages/effect/test/unstable/ai/McpServer/McpConformance/LoggingTest.ts)逐条验证。下面沿这条链路展开。

MCP 日志的协议基础:三个 Schema 构件

MCP(Model Context Protocol)规范将日志定义为一组独立于具体传输层的能力原语,Effect 在 McpSchema.ts 中将其建模为三个部分。

LoggingLevel:映射 syslog 严重级别的 8 级字面量

export const LoggingLevel: Schema.Literals< "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency" > = Schema.Literals([ "debug", "info", "notice", "warning", "error", "critical", "alert", "emergency" ])

这 8 个级别直接对齐 RFC 5424 第 6.2.1 节定义的 syslog 严重级别(源码注释明确给出该规范出处)。从类型系统看,LoggingLevelSchema.Literals构造的联合字面量 Schema,因此未知级别会在解码阶段就被拒绝——这为后文"拒绝未知级别"的测试行为提供了 Schema 层面的保证。

SetLevel:客户端到服务器的日志开关

export class SetLevel extends Rpc.make("logging/setLevel", { payload: { ...RequestMeta.fields, level: LoggingLevel // 客户端希望接收的最低级别 }, success: Schema.Struct({}), error: McpError }) {}

logging/setLevel客户端 → 服务器方向的请求,携带level字段。源码 JSDoc 明确了语义:服务器应把"等于该级别及更高(更严重)"的日志作为notifications/message发送给客户端。也就是说,这是一个阈值开关,而不是"只发某个级别"的精确匹配。

LoggingMessageNotification:服务器到客户端的日志载体

export class LoggingMessageNotification extends Rpc.make("notifications/message", { payload: Schema.Struct({ ...NotificationMeta.fields, level: LoggingLevel, // 该日志消息的严重级别 logger: optional(Schema.String), // 发出日志的 logger 名称(可选) data: Schema.Any // 任意 JSON 可序列化数据 }) }) {}

notifications/message服务器 → 客户端方向的通知,其 payload 由三部分构成:

字段类型说明
levelLoggingLevel日志消息的严重级别
loggerstring \| undefined可选,发出日志的 logger 名称
dataSchema.Any日志数据,任何 JSON 可序列化类型(字符串、数字、对象、数组等)

data: Schema.Any意味着日志内容极为灵活——一个纯字符串、一个结构化错误对象、甚至嵌套数组都可以直接投递,测试用例也专门验证了这一点(见下文"测试验证")。

能力广告:initialize 阶段声明 logging 支持

MCP 客户端只有在明确知道服务器支持日志时,才会发起logging/setLevel。这一能力声明发生在initialize握手阶段,由服务器在其capabilities中携带logging字段。

Effect 的ServerCapabilitiesSchema 在 McpSchema.ts 中为logging预留了位置:

export class ServerCapabilities extends Schema.Opaque<ServerCapabilities>()(Schema.Struct({ ... /** * Present if the server supports sending log messages to the client. */ logging: optional(Schema.Struct({})), ... })) {}

真正的"广告"行为发生在运行时层。在 internal/mcpRuntime.ts 的initialize生命周期回调中,服务器会构造自己的能力集:

const capabilities: McpCore.CanonicalServerCapabilities = { completions: true, logging: true, // ← 无论是否注册了工具/资源/提示词,logging 总是被声明 ...(presence.tools ? { tools: { listChanged: true } } : {}), ...(presence.resources ? { resources: { listChanged: true, subscribe: httpRequest === undefined } } : {}), ...(presence.prompts ? { prompts: { listChanged: true } } : {}), ...(options.serverInfo.extensions ? { extensions: options.serverInfo.extensions } : {}) }

注意这里的细节:logging: true无条件的,不像toolsresourcesprompts那样依赖注册情况。从源码结构看,这体现了日志能力是 MCP 服务器的"基础设施级"能力——只要用 Effect 运行 MCP 服务器,客户端就能在 initialize 响应中看到capabilities.logging,并据此决定是否启用日志接收。conformance 测试也对此给出了硬性断言(MUST advertise logging when log notifications are supported,见下文)。

客户端选择日志级别:setLevel 的会话级存储

当客户端决定接收日志时,会发送logging/setLevel请求。Effect 的服务端运行时在 internal/mcpRuntime.ts 将这一请求委托给有状态运行时的setLogLevel

setLogLevel: stateful.setLogLevel,

其实现位于 internal/mcpStatefulRuntime.ts:

setLogLevel: (level, clientId, headers) => Effect.sync(() => { const session = resolveSession(clientId, headers) if (session !== undefined) { session.logLevel = { _tag: "Mcp", level } } }),

关键点在于:日志级别是按会话(session)存储的resolveSession会根据客户端 ID(stdio/自定义传输)或Mcp-Session-Id头(HTTP 传输)解析到当前客户端对应的会话对象,然后把用户选择的级别写入该会话的logLevel字段。这意味着:

  • 每个客户端的选择是相互隔离的;
  • 服务器可以同时服务多个客户端,各自拥有不同的日志阈值。

这一点在测试should isolate background HTTP log levels when concurrent sessions select different thresholds中得到了显式验证:两个并发 HTTP 会话分别选择debugemergency,同一服务器发出的同一条 debug 日志,只有选择了debug的会话能收到,选择了emergency的会话只能收到 emergency 级别的日志。

阈值过滤:canDeliver 的投递判定

advertise logging与"honor each client's selected log level"之间的桥梁,是canDeliver——服务器在把每条日志通知投递给某个客户端之前,都会先经过这一判定。其实现同样在 internal/mcpStatefulRuntime.ts:

canDeliver: (clientId, headers, notification, fallbackLogLevel) => { const session = resolveSession(clientId, headers) if (notification._tag === "LoggingMessage") { const minimum = session?.logLevel return minimum?._tag === "Mcp" ? McpProtocol.mcpLogLevels[notification.level].order >= McpProtocol.mcpLogLevels[minimum.level].order : LogLevel.isGreaterThanOrEqualTo( McpProtocol.mcpLogLevels[notification.level].effect, minimum?.level ?? fallbackLogLevel ) } return notification._tag !== "ResourceUpdated" || session?.resourceSubscriptions?.has(notification.uri) === true }

逻辑分为两条路径:

  1. 客户端显式设置过 MCP 级别session.logLevel._tag === "Mcp"):比较两个级别的order数值。当前日志的order大于等于客户端所选级别的order才投递;
  2. 客户端没有显式设置:退化为用 Effect 的LogLevel.isGreaterThanOrEqualTo与服务器默认日志级别(fallbackLogLevel)比较。

这里的order来自 internal/mcpProtocol.ts 中维护的 MCP 级别 → Effect 日志级别的完整映射表:

MCP 级别Effect LogLevelorder
debugDebug0
infoInfo1
noticeInfo2
warningWarn3
errorError4
criticalFatal5
alertFatal6
emergencyFatal7

这张表同时服务于两个目的:判定阈值order比较)和级别转换effect字段),后者用于把 MCP 级别映射为 Effect 自身的LogLevel,使 MCP 日志能与 Effect 的日志系统无缝衔接。注意notice被映射为Infoalert/emergency被映射为Fatal——这是 MCP 8 级 syslog 语义到 Effect 5 级(Debug/Info/Warn/Error/Fatal)的归并。

投递判定发生在通知发送路径上。服务器发出的日志通知首先进入notifications/message的投影管线(makeNotificationProjector在 internal/mcpProtocol.ts 中把内部LoggingMessage通知投影为协议层的LoggingMessageNotification),随后按目标客户端逐一分发,canDeliver就是分发前的闸门。

与 Effect 日志体系的衔接:CurrentLogLevel

除了通知投递的阈值过滤,本次变更还涉及另一个方向:当服务器在处理某个客户端的请求时,Effect 代码中的日志应该以何种级别输出

在 McpServer.ts 中,服务器启动时会读取当前的默认日志级别:

const defaultLogLevel = yield* CurrentLogLevel

这个默认值被传入运行时(defaultLogLevel字段),在 initialize 时作为会话的初始logLevel(见mcpRuntime.ts第 498 行的logLevel: options.defaultLogLevel),也是canDeliver的兜底比较基准。

更重要的是,在McpServer.ts的中间件层(第 860-934 行),每个来自客户端的请求都会被注入与客户端会话匹配的CurrentLogLevel服务:

return Effect.provideService( ..., CurrentLogLevel, runtime.effectLogLevel(client.id, headers, defaultLogLevel) )

effectLogLevel的实现(internal/mcpStatefulRuntime.ts)会将会话中存储的 MCP 级别转换为 Effect 级别:

const effectLogLevel = (clientId, headers, fallback): LogLevel.LogLevel => { const session = resolveSession(clientId, headers) return session?.logLevel._tag === "Mcp" ? McpProtocol.mcpLogLevels[session.logLevel.level].effect : session?.logLevel.level ?? fallback }

由此形成完整闭环:客户端设置 MCP 日志级别 → 会话存储 → 请求处理时转换为 Effect 的 CurrentLogLevel → Effect 日志 API 在该请求上下文中按此级别输出。conformance 测试SHOULD update the minimum level for subsequent operations验证的正是这一点:调用logging/setLevel设置为debug后,后续工具调用中通过 Effect 读取到的当前级别为Debug

现代协议(2026-07-28)的每请求日志级别

上述会话级阈值主要服务于 2024-11-05 至 2025-11-25 的"有状态"协议规范。而在更新的2026-07-28协议适配器(stateless modern)中,日志过滤还支持**请求级(request-scoped)**方式:客户端可以在单个请求的_meta中携带io.modelcontextprotocol/logLevel字段。

McpServer.tsprovideInvocationContext(第 163-186 行)处理了这一情形:

const requestMetadata = invocation.requestContext.requestMetadata const logLevel = Predicate.hasProperty(requestMetadata, "io.modelcontextprotocol/logLevel") ? requestMetadata["io.modelcontextprotocol/logLevel"] : undefined if (isLoggingLevel(logLevel)) { provided = Effect.provideService( provided, CurrentLogLevel, McpProtocolInternal.mcpLogLevels[logLevel].effect ) }

即:如果请求元数据中声明了合法的 MCP 日志级别,就用它覆盖当前请求上下文的CurrentLogLevel;如果声明了未知级别,isLoggingLevel(基于Schema.is(McpSchema.LoggingLevel))判定失败,最终由InvalidParams错误拒绝——测试should reject a request when its request-scoped log level is unknown验证了这一点。

需要注意,在 stateless 现代协议下,日志通知不再像有状态协议那样按会话广播给所有已初始化客户端,而是跟随产生它的请求/订阅流投递,测试MUST not deliver logs from background or another request on an HTTP subscription stream专门约束了这一行为边界。

测试验证:LoggingTest 的 conformance 覆盖

本次变更的协议行为并非只停留在实现层面,仓库在 packages/effect/test/unstable/ai/McpServer/McpConformance/LoggingTest.ts 中提供了覆盖四个协议版本(2024-11-05、2025-03-26、2025-06-18、2025-11-25)与 stateless 现代协议的 conformance 测试套件。值得关注的断言包括:

  • 能力广告MUST advertise logging when log notifications are supported——initialize 响应的capabilities必须包含logging
  • 级别合法性MUST accept every specified log level(8 个级别逐一通过)与MUST reject an unknown log level(如verbose返回INVALID_PARAMS_ERROR_CODE)。
  • 阈值语义SHOULD send notifications at the selected level and higherMUST not send notifications below the selected level——在 stdio 传输上设置warning后,warning级别的日志被投递,debug级别的日志被过滤。
  • 通知格式MUST emit log messages as notifications without an identifier——notifications/message是纯通知,不带idresult字段;MUST allow arbitrary JSON-compatible log data——data字段接受字符串、数字、布尔、null、数组、嵌套对象。
  • 传输完整性SCENARIO does not corrupt the stdio protocol stream with log output——日志通知以结构化帧发送,不会污染 stdio 上的 JSON-RPC 请求/响应流。
  • 并发隔离should isolate background HTTP log levels when concurrent sessions select different thresholds——不同 HTTP 会话的日志阈值互不干扰。

这些测试同时覆盖了 HTTP 与 stdio 两种传输形态,说明日志能力在两种传输上行为一致。

小结:从 changeset 到可运行的服务器能力

回看这次patch,其价值可以总结为三层:

  1. 协议完备:8 级 syslog 级别的 Schema、logging/setLevel请求与notifications/message通知,构成了完整的 MCP 日志协议面(McpSchema.ts);
  2. 实现严谨:initialize 阶段无条件声明capabilities.logging,会话级存储每个客户端的级别选择,canDeliver按阈值过滤投递,并利用mcpLogLevels映射表与 Effect 的CurrentLogLevel/LogLevel体系无缝衔接(internal/mcpRuntime.ts、internal/mcpStatefulRuntime.ts);
  3. 行为可验证:跨四个协议版本的 conformance 测试把"广告能力""接受/拒绝级别""阈值投递""会话隔离"等行为固化为可回归的断言(LoggingTest.ts)。

对于使用 Effect 构建 MCP 服务器的开发者,这意味着:服务器天然支持客户端按需调节日志输出,无需在业务代码中手工维护每个客户端的日志开关——只需像测试中那样调用server.notifications["notifications/message"]发送日志,剩下的能力声明、阈值过滤与级别映射都由运行时完成。

【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询