| 1 | import { assert, describe, it } from "@effect/vitest" |
| 2 | import type * as McpProtocol from "effect/ai/McpProtocol" |
| 3 | import * as McpSchema from "effect/ai/McpSchema" |
| 4 | import * as McpServer from "effect/ai/McpServer" |
| 5 | import * as Context from "effect/Context" |
| 6 | import * as Deferred from "effect/Deferred" |
| 7 | import * as Effect from "effect/Effect" |
| 8 | import * as Fiber from "effect/Fiber" |
| 9 | import * as Layer from "effect/Layer" |
| 10 | import * as Schema from "effect/Schema" |
| 11 | import { initializeHttpSession, makeHttpHarness } from "../TestUtils/McpHttpHarness.ts" |
| 12 | import { makeMcpSseReader, readMcpHttpResponse } from "../TestUtils/McpHttpResponse.ts" |
| 13 | import { makeServerLayer } from "../TestUtils/McpServerLayer.ts" |
| 14 | import { makeMcpStdioHarness } from "../TestUtils/McpStdioHarness.ts" |
| 15 | import { McpConformance, type McpConformanceLayer } from "./McpConformance.ts" |
| 16 | |
| 17 | const levels = ["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"] as const |
| 18 | const effectLevels = { |
| 19 | debug: "Debug", |
| 20 | info: "Info", |
| 21 | notice: "Info", |
| 22 | warning: "Warn", |
| 23 | error: "Error", |
| 24 | critical: "Fatal", |
| 25 | alert: "Fatal", |
| 26 | emergency: "Fatal" |
| 27 | } as const |
| 28 | |
| 29 | const setLevel = (level: string) => |
| 30 | Effect.gen(function*() { |
| 31 | const test = yield* McpConformance |
| 32 | const initialized = yield* test.initialize({ server: "features" }) |
| 33 | yield* test.notifyInitialized(initialized) |
| 34 | const response = yield* test.send(initialized, { |
| 35 | jsonrpc: "2.0", |
| 36 | id: 2, |
| 37 | method: "logging/setLevel", |
| 38 | params: { level } |
| 39 | }) |
| 40 | return { initialized, response, test } |
| 41 | }) |
| 42 | |
| 43 | export const suite = (protocol: McpProtocol.ProtocolAdapter, layer: McpConformanceLayer) => |
| 44 | it.layer(layer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => { |
| 45 | describe("Logging", () => { |
| 46 | // Logging has the same protocol surface in the legacy specifications. |
| 47 | describe("Capabilities", () => { |
| 48 | it.effect("MUST advertise logging when log notifications are supported", () => |
| 49 | Effect.gen(function*() { |
| 50 | const test = yield* McpConformance |
| 51 | const initialized = yield* test.initialize({ server: "features" }) |
| 52 | assert.property(initialized.message.result.capabilities, "logging") |
| 53 | })) |
| 54 | }) |
| 55 | |
| 56 | describe("Setting Log Level", () => { |
| 57 | // https://modelcontextprotocol.io/specification/2024-11-05/server/utilities/logging#setting-log-level |
| 58 | // https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/logging#setting-log-level |
| 59 | // https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging#setting-log-level |
| 60 | // https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/logging#setting-log-level |
| 61 | // These HTTP cases exercise the shared logging threshold contract. The 2024 adapter run checks |
| 62 | // Effect compatibility through this HTTP harness, not the original 2024 HTTP+SSE transport. |
| 63 | for (const level of ["debug", "emergency"] as const) { |
| 64 | it.effect(`should filter background HTTP logs when the destination session selects ${level}`, () => |
| 65 | Effect.gen(function*() { |
| 66 | const release = yield* Deferred.make<void>() |
| 67 | const serverReady = yield* Deferred.make<McpServer.McpServer["Service"]>() |
| 68 | const harness = yield* makeHttpHarness( |
| 69 | Layer.effectDiscard(Effect.gen(function*() { |
| 70 | const server = yield* McpServer.McpServer |
| 71 | yield* server.addTool({ |
| 72 | tool: new McpSchema.Tool({ name: "Wait", inputSchema: { type: "object" } }), |
| 73 | annotations: Context.empty(), |
| 74 | handle: () => |
| 75 | Effect.gen(function*() { |
| 76 | yield* server.notifications["notifications/message"]({ level: "emergency", data: "ready" }) |
| 77 | yield* Deferred.await(release) |
| 78 | return new McpSchema.CallToolResult({ content: [] }) |
| 79 | }) |
| 80 | }) |
| 81 | yield* Deferred.succeed(serverReady, server) |
| 82 | })).pipe(Layer.provideMerge(makeServerLayer({ name: "BackgroundLogging", protocols: [protocol] }))) |
| 83 | ) |
| 84 | const headers = yield* initializeHttpSession(harness, protocol) |
| 85 | const configured = yield* harness.post({ |
| 86 | jsonrpc: "2.0", |
| 87 | id: "set-level", |
| 88 | method: "logging/setLevel", |
| 89 | params: { level } |
| 90 | }, headers).pipe(Effect.flatMap(readMcpHttpResponse)) |
| 91 | assert.deepInclude(configured, { id: "set-level", result: {} }) |
| 92 | const response = makeMcpSseReader( |
| 93 | yield* harness.post({ |
| 94 | jsonrpc: "2.0", |
| 95 | id: "wait", |
| 96 | method: "tools/call", |
| 97 | params: { name: "Wait", arguments: {} } |
| 98 | }, headers) |
| 99 | ) |
| 100 | yield* Effect.addFinalizer(() => response.cancel) |
| 101 | assert.deepInclude(yield* response.take(), { |
| 102 | method: "notifications/message", |
| 103 | params: { level: "emergency", data: "ready" } |
| 104 | }) |
| 105 | const server = yield* Deferred.await(serverReady) |
| 106 | yield* server.notifications["notifications/message"]({ level: "debug", data: "debug-log" }) |
| 107 | yield* server.notifications["notifications/message"]({ level: "error", data: "error-log" }) |
| 108 | yield* server.notifications["notifications/message"]({ level: "emergency", data: "sentinel" }) |
| 109 | const messages: Array<unknown> = [] |
| 110 | while (true) { |
| 111 | const message = yield* response.take() |
| 112 | assert.strictEqual(message.method, "notifications/message") |
| 113 | messages.push(message.params) |
| 114 | if ((message.params as { data: string }).data === "sentinel") break |
| 115 | } |
| 116 | assert.deepStrictEqual( |
| 117 | messages, |
| 118 | level === "debug" ? |
| 119 | [ |
| 120 | { level: "debug", data: "debug-log" }, |
| 121 | { level: "error", data: "error-log" }, |
| 122 | { level: "emergency", data: "sentinel" } |
| 123 | ] : |
| 124 | [{ level: "emergency", data: "sentinel" }] |
| 125 | ) |
| 126 | yield* Deferred.succeed(release, undefined) |
| 127 | assert.deepInclude(yield* response.take(), { id: "wait", result: { content: [] } }) |
| 128 | assert.deepStrictEqual(yield* response.drain(), []) |
| 129 | })) |
| 130 | } |
| 131 | |
| 132 | it.effect("should isolate background HTTP log levels when concurrent sessions select different thresholds", () => |
| 133 | Effect.gen(function*() { |
| 134 | const release = yield* Deferred.make<void>() |
| 135 | const serverReady = yield* Deferred.make<McpServer.McpServer["Service"]>() |
| 136 | const harness = yield* makeHttpHarness( |
| 137 | Layer.effectDiscard(Effect.gen(function*() { |
| 138 | const server = yield* McpServer.McpServer |
| 139 | yield* server.addTool({ |
| 140 | tool: new McpSchema.Tool({ name: "Wait", inputSchema: { type: "object" } }), |
| 141 | annotations: Context.empty(), |
| 142 | handle: () => |
| 143 | Effect.gen(function*() { |
| 144 | yield* server.notifications["notifications/message"]({ level: "emergency", data: "ready" }) |
| 145 | yield* Deferred.await(release) |
| 146 | return new McpSchema.CallToolResult({ content: [] }) |
| 147 | }) |
| 148 | }) |
| 149 | yield* Deferred.succeed(serverReady, server) |
| 150 | })).pipe( |
| 151 | Layer.provideMerge(makeServerLayer({ name: "IsolatedBackgroundLogging", protocols: [protocol] })) |
| 152 | ) |
| 153 | ) |
| 154 | const responses = yield* Effect.forEach(["debug", "emergency"] as const, (level) => |
| 155 | Effect.gen(function*() { |
| 156 | const headers = yield* initializeHttpSession(harness, protocol) |
| 157 | const configured = yield* harness.post({ |
| 158 | jsonrpc: "2.0", |
| 159 | id: "set-level", |
| 160 | method: "logging/setLevel", |
| 161 | params: { level } |
| 162 | }, headers).pipe(Effect.flatMap(readMcpHttpResponse)) |
| 163 | assert.deepInclude(configured, { id: "set-level", result: {} }) |
| 164 | const response = makeMcpSseReader( |
| 165 | yield* harness.post({ |
| 166 | jsonrpc: "2.0", |
| 167 | id: "wait", |
| 168 | method: "tools/call", |
| 169 | params: { name: "Wait", arguments: {} } |
| 170 | }, headers) |
| 171 | ) |
| 172 | yield* Effect.addFinalizer(() => response.cancel) |
| 173 | assert.deepInclude(yield* response.take(), { |
| 174 | method: "notifications/message", |
| 175 | params: { level: "emergency", data: "ready" } |
| 176 | }) |
| 177 | return { level, response } |
| 178 | })) |
| 179 | const server = yield* Deferred.await(serverReady) |
| 180 | yield* server.notifications["notifications/message"]({ level: "debug", data: "debug-log" }) |
| 181 | yield* server.notifications["notifications/message"]({ level: "error", data: "error-log" }) |
| 182 | yield* server.notifications["notifications/message"]({ level: "emergency", data: "sentinel" }) |
| 183 | for (const { level, response } of responses) { |
| 184 | const messages: Array<unknown> = [] |
| 185 | while (true) { |
| 186 | const message = yield* response.take() |
| 187 | assert.strictEqual(message.method, "notifications/message") |
| 188 | messages.push(message.params) |
| 189 | if ((message.params as { data: string }).data === "sentinel") break |
| 190 | } |
| 191 | assert.deepStrictEqual( |
| 192 | messages, |
| 193 | level === "debug" ? |
| 194 | [ |
| 195 | { level: "debug", data: "debug-log" }, |
| 196 | { level: "error", data: "error-log" }, |
| 197 | { level: "emergency", data: "sentinel" } |
| 198 | ] : |
| 199 | [{ level: "emergency", data: "sentinel" }] |
| 200 | ) |
| 201 | } |
| 202 | yield* Deferred.succeed(release, undefined) |
| 203 | for (const { response } of responses) { |
| 204 | assert.deepInclude(yield* response.take(), { id: "wait", result: { content: [] } }) |
| 205 | assert.deepStrictEqual(yield* response.drain(), []) |
| 206 | } |
| 207 | })) |
| 208 | |
| 209 | it.effect("MUST accept every specified log level", () => |
| 210 | Effect.forEach(levels, (level) => |
| 211 | Effect.gen(function*() { |
| 212 | const { response, test } = yield* setLevel(level) |
| 213 | const result = yield* test.decodeResult(response) |
| 214 | assert.deepStrictEqual(result.result, {}) |
| 215 | }), { concurrency: 1 })) |
| 216 | it.effect("MUST reject an unknown log level", () => |
| 217 | Effect.gen(function*() { |
| 218 | const { response, test } = yield* setLevel("verbose") |
| 219 | const error = yield* test.decodeError(response) |
| 220 | assert.strictEqual(error.error.code, McpSchema.INVALID_PARAMS_ERROR_CODE) |
| 221 | })) |
| 222 | it.effect("SHOULD update the minimum level for subsequent operations", () => |
| 223 | Effect.gen(function*() { |
| 224 | const { initialized, test } = yield* setLevel("debug") |
| 225 | const response = yield* test.send(initialized, { |
| 226 | jsonrpc: "2.0", |
| 227 | id: 3, |
| 228 | method: "tools/call", |
| 229 | params: { name: "LogLevelTool", arguments: {} } |
| 230 | }) |
| 231 | const result = yield* test.decodeResult(response).pipe( |
| 232 | Effect.flatMap((message) => Schema.decodeUnknownEffect(McpSchema.CallToolResult)(message.result)) |
| 233 | ) |
| 234 | assert.deepStrictEqual(result.content, [{ type: "text", text: "Debug" }]) |
| 235 | })) |
| 236 | it.effect("SHOULD send notifications at the selected level and higher", () => |
| 237 | Effect.gen(function*() { |
| 238 | const fixture = yield* makeMcpStdioHarness(protocol) |
| 239 | yield* fixture.initialize() |
| 240 | const response = yield* fixture.sendRequest("logging/setLevel", { level: "warning" }, 2) |
| 241 | assert.deepStrictEqual(response.result, {}) |
| 242 | |
| 243 | yield* fixture.server.notifications["notifications/message"]({ |
| 244 | level: "warning", |
| 245 | logger: "conformance", |
| 246 | data: "at-threshold" |
| 247 | }) |
| 248 | const notification = yield* fixture.awaitOutboundMethod("notifications/message") |
| 249 | const payload = yield* Schema.decodeUnknownEffect( |
| 250 | McpSchema.LoggingMessageNotification.payloadSchema |
| 251 | )(notification.params) |
| 252 | assert.deepStrictEqual(payload, { |
| 253 | level: "warning", |
| 254 | logger: "conformance", |
| 255 | data: "at-threshold" |
| 256 | }) |
| 257 | })) |
| 258 | it.effect("MUST not send notifications below the selected level", () => |
| 259 | Effect.gen(function*() { |
| 260 | const fixture = yield* makeMcpStdioHarness(protocol) |
| 261 | yield* fixture.initialize() |
| 262 | const response = yield* fixture.sendRequest("logging/setLevel", { level: "warning" }, 2) |
| 263 | assert.deepStrictEqual(response.result, {}) |
| 264 | |
| 265 | yield* fixture.server.notifications["notifications/message"]({ |
| 266 | level: "debug", |
| 267 | logger: "conformance", |
| 268 | data: "below-threshold" |
| 269 | }) |
| 270 | yield* fixture.server.notifications["notifications/message"]({ |
| 271 | level: "warning", |
| 272 | logger: "conformance", |
| 273 | data: "allowed-sentinel" |
| 274 | }) |
| 275 | const notification = yield* fixture.awaitOutboundMethod("notifications/message") |
| 276 | assert.deepNestedInclude(notification, { |
| 277 | params: { |
| 278 | level: "warning", |
| 279 | logger: "conformance", |
| 280 | data: "allowed-sentinel" |
| 281 | } |
| 282 | }) |
| 283 | })) |
| 284 | }) |
| 285 | |
| 286 | describe("Log Message Notifications", () => { |
| 287 | it.effect("SCHEMA preserves the log level, logger name, and data", () => |
| 288 | Effect.gen(function*() { |
| 289 | const payload = yield* Schema.decodeUnknownEffect( |
| 290 | McpSchema.LoggingMessageNotification.payloadSchema |
| 291 | )({ |
| 292 | level: "warning", |
| 293 | logger: "database", |
| 294 | data: { |
| 295 | message: "slow query", |
| 296 | durationMs: 120 |
| 297 | } |
| 298 | }) |
| 299 | |
| 300 | assert.deepStrictEqual(payload, { |
| 301 | level: "warning", |
| 302 | logger: "database", |
| 303 | data: { |
| 304 | message: "slow query", |
| 305 | durationMs: 120 |
| 306 | } |
| 307 | }) |
| 308 | })) |
| 309 | it.effect("MUST allow arbitrary JSON-compatible log data", () => |
| 310 | Effect.forEach([ |
| 311 | "message", |
| 312 | 42, |
| 313 | true, |
| 314 | null, |
| 315 | ["one", { nested: "two" }], |
| 316 | { nested: { value: 1 } } |
| 317 | ], (data) => |
| 318 | Schema.decodeUnknownEffect( |
| 319 | McpSchema.LoggingMessageNotification.payloadSchema |
| 320 | )({ |
| 321 | level: "info", |
| 322 | data |
| 323 | }).pipe( |
| 324 | Effect.map((payload) => assert.deepStrictEqual(payload.data, data)) |
| 325 | ))) |
| 326 | it.effect("MUST emit log messages as notifications without an identifier", () => |
| 327 | Effect.gen(function*() { |
| 328 | const fixture = yield* makeMcpStdioHarness(protocol) |
| 329 | yield* fixture.initialize() |
| 330 | yield* fixture.server.notifications["notifications/message"]({ |
| 331 | level: "warning", |
| 332 | logger: "database", |
| 333 | data: { message: "slow query" } |
| 334 | }) |
| 335 | |
| 336 | const notification = yield* fixture.awaitOutboundMethod("notifications/message") |
| 337 | assert.strictEqual(notification.jsonrpc, "2.0") |
| 338 | assert.strictEqual(notification.method, "notifications/message") |
| 339 | yield* Schema.decodeUnknownEffect( |
| 340 | McpSchema.LoggingMessageNotification.payloadSchema |
| 341 | )(notification.params) |
| 342 | assert.notProperty(notification, "id") |
| 343 | assert.notProperty(notification, "result") |
| 344 | })) |
| 345 | it.effect("SCENARIO does not corrupt the stdio protocol stream with log output", () => |
| 346 | Effect.gen(function*() { |
| 347 | const fixture = yield* makeMcpStdioHarness(protocol) |
| 348 | yield* fixture.initialize() |
| 349 | yield* fixture.takeFrame |
| 350 | yield* fixture.server.notifications["notifications/message"]({ |
| 351 | level: "info", |
| 352 | logger: "conformance", |
| 353 | data: "stdio-integrity-diagnostic" |
| 354 | }) |
| 355 | const ping = yield* fixture.sendRequest("ping", {}, 2).pipe(Effect.forkChild) |
| 356 | |
| 357 | const notificationFrame = yield* fixture.takeFrame |
| 358 | const responseFrame = yield* fixture.takeFrame |
| 359 | assert.isObject(notificationFrame) |
| 360 | assert.isObject(responseFrame) |
| 361 | assert.deepInclude(notificationFrame, { |
| 362 | method: "notifications/message" |
| 363 | }) |
| 364 | assert.deepNestedInclude(notificationFrame, { |
| 365 | "params.data": "stdio-integrity-diagnostic" |
| 366 | }) |
| 367 | assert.deepInclude(responseFrame, { jsonrpc: "2.0", id: 2, result: {} }) |
| 368 | yield* Fiber.join(ping) |
| 369 | })) |
| 370 | }) |
| 371 | }) |
| 372 | }) |
| 373 | |
| 374 | export const statelessModernSuite = ( |
| 375 | protocol: McpProtocol.ProtocolAdapter, |
| 376 | layer: McpConformanceLayer |
| 377 | ) => |
| 378 | it.layer(layer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => { |
| 379 | const callLogLevelTool = Effect.fnUntraced(function*(level?: string) { |
| 380 | const test = yield* McpConformance |
| 381 | const discovered = yield* test.initialize({ server: "features" }) |
| 382 | const response = yield* test.send(discovered, { |
| 383 | jsonrpc: "2.0", |
| 384 | id: 2, |
| 385 | method: "tools/call", |
| 386 | params: { |
| 387 | name: "LogLevelTool", |
| 388 | arguments: {}, |
| 389 | ...(level === undefined |
| 390 | ? {} |
| 391 | : { _meta: { "io.modelcontextprotocol/logLevel": level } }) |
| 392 | } |
| 393 | }) |
| 394 | return { response, test } |
| 395 | }) |
| 396 | |
| 397 | describe("Logging > Stateless modern", () => { |
| 398 | // https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/logging#per-request-log-level |
| 399 | for (const source of ["background", "another request"] as const) { |
| 400 | it.effect(`MUST not deliver logs from ${source} on an HTTP subscription stream`, () => |
| 401 | Effect.gen(function*() { |
| 402 | const serverReady = yield* Deferred.make<McpServer.McpServer["Service"]>() |
| 403 | const harness = yield* makeHttpHarness( |
| 404 | Layer.effectDiscard( |
| 405 | Effect.gen(function*() { |
| 406 | const server = yield* McpServer.McpServer |
| 407 | yield* server.addTool({ |
| 408 | tool: new McpSchema.Tool({ name: "EmitLogs", inputSchema: { type: "object" } }), |
| 409 | annotations: Context.empty(), |
| 410 | handle: () => |
| 411 | server.notifications["notifications/message"]({ level: "error", data: "private-log" }).pipe( |
| 412 | Effect.as(new McpSchema.CallToolResult({ content: [] })) |
| 413 | ) |
| 414 | }) |
| 415 | yield* Deferred.succeed(serverReady, server) |
| 416 | }) |
| 417 | ).pipe(Layer.provideMerge(makeServerLayer({ name: "LoggingConformance", protocols: [protocol] }))) |
| 418 | ) |
| 419 | const response = yield* harness.post({ |
| 420 | jsonrpc: "2.0", |
| 421 | id: "logs-forbidden", |
| 422 | method: "subscriptions/listen", |
| 423 | params: { |
| 424 | notifications: { toolsListChanged: true }, |
| 425 | _meta: { |
| 426 | "io.modelcontextprotocol/protocolVersion": protocol.protocolVersion, |
| 427 | "io.modelcontextprotocol/clientCapabilities": {} |
| 428 | } |
| 429 | } |
| 430 | }, { |
| 431 | "MCP-Protocol-Version": protocol.protocolVersion, |
| 432 | "Mcp-Method": "subscriptions/listen" |
| 433 | }) |
| 434 | const stream = makeMcpSseReader(response) |
| 435 | yield* Effect.addFinalizer(() => stream.cancel) |
| 436 | assert.strictEqual((yield* stream.take()).method, "notifications/subscriptions/acknowledged") |
| 437 | const server = yield* Deferred.await(serverReady) |
| 438 | if (source === "background") { |
| 439 | yield* server.notifications["notifications/message"]({ level: "error", data: "private-log" }) |
| 440 | } else { |
| 441 | const result = yield* harness.post({ |
| 442 | jsonrpc: "2.0", |
| 443 | id: "originating-request", |
| 444 | method: "tools/call", |
| 445 | params: { |
| 446 | name: "EmitLogs", |
| 447 | arguments: {}, |
| 448 | _meta: { |
| 449 | "io.modelcontextprotocol/protocolVersion": protocol.protocolVersion, |
| 450 | "io.modelcontextprotocol/clientCapabilities": {}, |
| 451 | "io.modelcontextprotocol/logLevel": "warning" |
| 452 | } |
| 453 | } |
| 454 | }, { |
| 455 | "MCP-Protocol-Version": protocol.protocolVersion, |
| 456 | "Mcp-Method": "tools/call", |
| 457 | "Mcp-Name": "EmitLogs" |
| 458 | }) |
| 459 | const originatingStream = makeMcpSseReader(result) |
| 460 | yield* Effect.addFinalizer(() => originatingStream.cancel) |
| 461 | assert.deepInclude(yield* originatingStream.take(), { |
| 462 | method: "notifications/message", |
| 463 | params: { level: "error", data: "private-log" } |
| 464 | }) |
| 465 | const completed = yield* originatingStream.take() |
| 466 | assert.deepInclude(completed, { id: "originating-request" }) |
| 467 | assert.property(completed, "result") |
| 468 | assert.deepStrictEqual(yield* originatingStream.drain(), []) |
| 469 | } |
| 470 | // Delivery is acknowledged before publishing the sentinel, so no timing window is needed. |
| 471 | yield* server.notifications["notifications/tools/list_changed"]({}) |
| 472 | assert.strictEqual((yield* stream.take()).method, "notifications/tools/list_changed") |
| 473 | })) |
| 474 | } |
| 475 | |
| 476 | // https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/logging#capabilities |
| 477 | it.effect("should advertise logging when request-scoped log filtering is supported", () => |
| 478 | Effect.gen(function*() { |
| 479 | const test = yield* McpConformance |
| 480 | const discovered = yield* test.initialize({ server: "features" }) |
| 481 | |
| 482 | assert.property(discovered.message.result.capabilities, "logging") |
| 483 | })) |
| 484 | |
| 485 | // https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/logging#log-levels |
| 486 | it.effect("should apply every specified log level to the request that declares it", () => |
| 487 | Effect.forEach(levels, (level) => |
| 488 | Effect.gen(function*() { |
| 489 | const { response, test } = yield* callLogLevelTool(level) |
| 490 | const result = yield* test.decodeResult(response).pipe( |
| 491 | Effect.flatMap((message) => Schema.decodeUnknownEffect(McpSchema.CallToolResult)(message.result)) |
| 492 | ) |
| 493 | assert.deepStrictEqual(result.content, [{ type: "text", text: JSON.stringify(effectLevels[level]) }]) |
| 494 | }), { concurrency: 1 })) |
| 495 | |
| 496 | // https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/logging#log-levels |
| 497 | it.effect("should reject a request when its request-scoped log level is unknown", () => |
| 498 | Effect.gen(function*() { |
| 499 | const { response, test } = yield* callLogLevelTool("verbose") |
| 500 | const error = yield* test.decodeError(response) |
| 501 | |
| 502 | assert.strictEqual(error.error.code, McpSchema.INVALID_PARAMS_ERROR_CODE) |
| 503 | })) |
| 504 | |
| 505 | // https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/logging#log-message-notifications |
| 506 | it.effect("should preserve the level, logger, and JSON data when decoding a log notification", () => |
| 507 | Effect.gen(function*() { |
| 508 | const payload = yield* Schema.decodeUnknownEffect( |
| 509 | McpSchema.LoggingMessageNotification.payloadSchema |
| 510 | )({ |
| 511 | level: "warning", |
| 512 | logger: "database", |
| 513 | data: { message: "slow query", durationMs: 120 } |
| 514 | }) |
| 515 | |
| 516 | assert.deepStrictEqual(payload, { |
| 517 | level: "warning", |
| 518 | logger: "database", |
| 519 | data: { message: "slow query", durationMs: 120 } |
| 520 | }) |
| 521 | })) |
| 522 | }) |
| 523 | }) |