Add MCP notifications/message for log streaming to clients#3484
Add MCP notifications/message for log streaming to clients#3484anushakolan wants to merge 2 commits intomainfrom
Conversation
Aniruddh25
left a comment
There was a problem hiding this comment.
Left some suggestions for code reuse.
3b658ef to
e2a1ee8
Compare
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds support for streaming server logs to MCP clients via notifications/message, enabling real-time log consumption after logging/setLevel is invoked.
Changes:
- Introduces MCP-specific logging components (
McpLogger,McpLoggerProvider,McpLogNotificationWriter) that emit logs as JSON-RPC notifications to stdout. - Centralizes MCP↔.NET log level mapping in
McpLogLevelConverterand wires it into dynamic log level handling. - Updates MCP stdio server to enable/disable log notifications based on
logging/setLeveland standardizes JSON-RPC version usage.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Service/Telemetry/DynamicLogLevelProvider.cs | Switches MCP level parsing to shared converter. |
| src/Service/Program.cs | Registers MCP notification writer/provider and clears default log providers in MCP stdio mode. |
| src/Service.Tests/UnitTests/McpLogNotificationTests.cs | Adds unit tests around MCP logger/provider enabled behavior. |
| src/Core/Telemetry/McpLogLevelConverter.cs | Adds shared MCP↔.NET log level conversion utility. |
| src/Azure.DataApiBuilder.Mcp/Telemetry/McpLoggerProvider.cs | Adds logger provider that caches per-category MCP loggers. |
| src/Azure.DataApiBuilder.Mcp/Telemetry/McpLogger.cs | Adds ILogger implementation that forwards logs to MCP notifications. |
| src/Azure.DataApiBuilder.Mcp/Telemetry/McpLogNotificationWriter.cs | Adds stdout JSON-RPC notification writer + interface for toggling. |
| src/Azure.DataApiBuilder.Mcp/Model/McpStdioJsonRpcErrorCodes.cs | Introduces shared JSON-RPC version constant. |
| src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs | Enables notifications on logging/setLevel and uses shared JSON-RPC version constant. |
bc9426d to
350a4b5
Compare
| private void InitializeWriter() | ||
| { | ||
| // Use the same approach as McpStdioServer - get raw stdout | ||
| Stream stdout = Console.OpenStandardOutput(); |
There was a problem hiding this comment.
This StreamWriter is on the same stdout as we use in McpStdioServer.cs in RunAsync
using Stream stdout = Console.OpenStandardOutput();
I think this will have concurrency/race conditions. Maybe we can factor these two uses into a helper with a lock?
| // when the client requests logging. They'll get logs at the overridden level. | ||
| bool isLoggingEnabled = !string.Equals(level, "none", StringComparison.OrdinalIgnoreCase); | ||
| if (updated && isLoggingEnabled) | ||
| if (isLoggingEnabled) |
There was a problem hiding this comment.
without the guard from updated, won't this result in us resetting stderr in the case where it was initially set to none re-introducing noisy logs when someone set log level to none on startup?
| IMcpLogNotificationWriter? notificationWriter = _serviceProvider.GetService<IMcpLogNotificationWriter>(); | ||
| if (notificationWriter != null) | ||
| { | ||
| notificationWriter.IsEnabled = isLoggingEnabled; |
There was a problem hiding this comment.
This makes the IsEnabled flag in McpLogger.cs redudant, and gives us 2 gates. Should refactor this so that we have 1 source of truth. I dont think the 2 gates always agree currently.
| { | ||
| // Use the same approach as McpStdioServer - get raw stdout | ||
| Stream stdout = Console.OpenStandardOutput(); | ||
| _writer = new StreamWriter(stdout, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)) |
There was a problem hiding this comment.
We probably need to implement IDisposable and properly dispose of the StreamWriter.
| // Include exception details if present | ||
| if (exception != null) | ||
| { | ||
| message = $"{message} Exception: {exception.GetType().Name}: {exception.Message}"; |
There was a problem hiding this comment.
Do we intentionally drop the stack trace, inner exceptions, etc here? Do MCP clients expect to see this?
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public IDisposable? BeginScope<TState>(TState state) where TState : notnull |
There was a problem hiding this comment.
Consider supporting external scopes ie: implement ISupportExternalScope as a future task.
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public ILogger CreateLogger(string categoryName) |
There was a problem hiding this comment.
shouldnt we check _disposed to avoid creating a new McpLogger with a stale reference?
| /// </summary> | ||
| /// <param name="writer">The notification writer to use for sending log messages.</param> | ||
| /// <param name="levelFilter">A function to filter log levels. Returns true if the level should be logged.</param> | ||
| public McpLoggerProvider(IMcpLogNotificationWriter writer, Func<LogLevel, bool> levelFilter) |
There was a problem hiding this comment.
this delegate is from LogLevelProvider.ShouldLog in Program.cs, but Program.cs has already used LogLevelProvider.ShouldLog in the previous AddFilter calls, so by the time McpLogger.Lo is reached LogLevelProvider.ShouldLog is already true. The _levelFilter parameter then on the McpLoggerProvider/McpLogger is then calling the same delegate with the same shared state and gets the same answer.
Id recommend dropping this delegate and updating IsEnabled to something like
public bool IsEnabled(LogLevel logLevel) => _writer.IsEnabled && logLevel != LogLevel.None;
aaronburtle
left a comment
There was a problem hiding this comment.
Looks good once comments are resolved.
Why make this change?
Enables MCP clients (like MCP Inspector, Claude Desktop, VS Code Copilot) to receive real-time log output via MCP
notifications/message.Related: #3274 (depends on PR #3419)
What is this change?
When
logging/setLevelis called with a level other than "none", logs are sent to MCP clients as JSON-RPC notifications:{ "jsonrpc": "2.0", "method": "notifications/message", "params": { "level": "info", "logger": "Azure.DataApiBuilder.Service.Startup", "data": "Starting Data API builder..." } }New files:
McpLogNotificationWriter.cs- Writes logs as MCP notifications to stdoutMcpLogger.cs/McpLoggerProvider.cs- ILogger implementation for .NET logging pipelineMcpLogNotificationTests.cs- Unit tests (8 tests)Modified files:
Program.cs- RegistersMcpNotificationWriterandMcpLoggerProviderfor MCP modeMcpStdioServer.cs- Enables notifications whenlogging/setLevelis calledHow was this tested?
logging/setLevelis sentNote
This PR targets
dev/anushakolan/set-log-level(PR #3419) as it depends on thelogging/setLevelimplementation.