Skip to content

Add MCP notifications/message for log streaming to clients#3484

Open
anushakolan wants to merge 2 commits intomainfrom
dev/anushakolan/mcp-notifications
Open

Add MCP notifications/message for log streaming to clients#3484
anushakolan wants to merge 2 commits intomainfrom
dev/anushakolan/mcp-notifications

Conversation

@anushakolan
Copy link
Copy Markdown
Contributor

@anushakolan anushakolan commented Apr 29, 2026

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/setLevel is 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 stdout
  • McpLogger.cs / McpLoggerProvider.cs - ILogger implementation for .NET logging pipeline
  • McpLogNotificationTests.cs - Unit tests (8 tests)

Modified files:

  • Program.cs - Registers McpNotificationWriter and McpLoggerProvider for MCP mode
  • McpStdioServer.cs - Enables notifications when logging/setLevel is called

How was this tested?

  • Unit tests: 6 tests covering level mapping, enable/disable, JSON format
  • Manual testing with MCP Inspector: verified notifications appear when logging/setLevel is sent

Note

This PR targets dev/anushakolan/set-log-level (PR #3419) as it depends on the logging/setLevel implementation.

Comment thread src/Service/Program.cs Outdated
Comment thread src/Azure.DataApiBuilder.Mcp/Telemetry/McpLogNotificationWriter.cs Outdated
Comment thread src/Azure.DataApiBuilder.Mcp/Telemetry/McpLogNotificationWriter.cs Outdated
Comment thread src/Service.Tests/UnitTests/McpLogNotificationTests.cs Outdated
Aniruddh25
Aniruddh25 previously approved these changes May 1, 2026
Copy link
Copy Markdown
Collaborator

@Aniruddh25 Aniruddh25 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some suggestions for code reuse.

Base automatically changed from dev/anushakolan/set-log-level to main May 2, 2026 00:28
@anushakolan anushakolan dismissed Aniruddh25’s stale review May 2, 2026 00:28

The base branch was changed.

Copilot AI review requested due to automatic review settings May 4, 2026 20:01
@anushakolan anushakolan force-pushed the dev/anushakolan/mcp-notifications branch from 3b658ef to e2a1ee8 Compare May 4, 2026 20:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 McpLogLevelConverter and wires it into dynamic log level handling.
  • Updates MCP stdio server to enable/disable log notifications based on logging/setLevel and 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.

Comment thread src/Azure.DataApiBuilder.Mcp/Telemetry/McpLogNotificationWriter.cs
Comment thread src/Azure.DataApiBuilder.Mcp/Telemetry/McpLogNotificationWriter.cs
Comment thread src/Service.Tests/UnitTests/McpLogNotificationTests.cs
Comment thread src/Service.Tests/UnitTests/McpLogNotificationTests.cs
Comment thread src/Service/Program.cs Outdated
Comment thread src/Azure.DataApiBuilder.Mcp/Telemetry/McpLogger.cs
Comment thread src/Azure.DataApiBuilder.Mcp/Model/McpStdioJsonRpcErrorCodes.cs
@anushakolan anushakolan force-pushed the dev/anushakolan/mcp-notifications branch from bc9426d to 350a4b5 Compare May 5, 2026 06:45
private void InitializeWriter()
{
// Use the same approach as McpStdioServer - get raw stdout
Stream stdout = Console.OpenStandardOutput();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor

@aaronburtle aaronburtle May 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider supporting external scopes ie: implement ISupportExternalScope as a future task.

}

/// <inheritdoc />
public ILogger CreateLogger(string categoryName)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Contributor

@aaronburtle aaronburtle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good once comments are resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants