
调整客户端调用
为方便后续调试MCP Client 代码,将之前文章中的客户端进行改造,代码逻辑如下:
            
            
              cs
              
              
            
          
          internal class Program
{
    static async Task Main(string[] args)
    {
        // 创建日志工厂用于McpClient日志输出
        using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
        IClientTransport clientTransport = new HttpClientTransport(new() {
            // sse
            Endpoint = new Uri("http://localhost:5000/sse"),
            TransportMode = HttpTransportMode.Sse,
        });
        // 配置客户端选项
        McpClientOptions clientOptions = new()
        {
            // 设置客户端信息
            ClientInfo = new ModelContextProtocol.Protocol.Implementation
            {
                Name = "EchoMcpClient",
                Version = "1.0.0",
                Title = "这是一个EchoMcp客户端示例程序"
            },
            // 添加能力
            Capabilities = new ClientCapabilities() { 
                Sampling = new SamplingCapability(),
                Roots = new RootsCapability(),
                Elicitation = new ElicitationCapability()
            },
            // 客户端处理程序
            Handlers = new McpClientHandlers
            {
                ElicitationHandler = ElicitationHandlerMethod,
                SamplingHandler = SamplingHandlerMethod
            }
        };
        // 构建McpClient客户端实例
        await using var mcpClient = await McpClient.CreateAsync(clientTransport,
        clientOptions: clientOptions
        ,loggerFactory: loggerFactory);
		// 设置日志监听等级
        await mcpClient.SetLoggingLevel(LoggingLevel.Debug);
        // 注册通知
mcpClient.RegisterNotificationHandler(NotificationMethods.LoggingMessageNotification, NotificationHandler);
        // 列出所有注册的工具与资源
        Console.WriteLine("Tool:");
        IList<McpClientTool> tools = await mcpClient.ListToolsAsync();
        foreach (var tool in tools)
        {
             Console.WriteLine($"- {tool.Name}: {tool.Description}");
        }
        Console.WriteLine("Resource:");
        foreach (var resource in await mcpClient.ListResourcesAsync())
        {
            Console.WriteLine($"- {resource.Name}: {resource.MimeType} - {resource.Uri}");
        }
        string message = string.Empty;
        while(true)
        {
            Console.WriteLine("请输入需要调用的工具名称");
            Console.Write(">");
            message = Console.ReadLine() ?? string.Empty;
            if (string.IsNullOrEmpty(message) && message == "bye")
            {
                return;
            }
            // 判定工具是否存在
            McpClientTool current = tools.FirstOrDefault(x => x.Name == message);
            if (current != null)
            {
                // 获取工具参数信息
                var input = current.ProtocolTool.InputSchema;
                Console.WriteLine("输入参数要求",input);
                Dictionary<string, string> propdics = new Dictionary<string, string>();
                // 简化函数读取参数
                StringBuilder stringBuilder = new StringBuilder();
                // 列出参数信息
                GetRequestParams(input, propdics, stringBuilder);
                if (stringBuilder.Length > 0)
                {
                    Console.WriteLine(stringBuilder.ToString());
                }
                Dictionary<string, object> argsDic = new Dictionary<string, object>();
                // 获取必需参数
                if (input.TryGetProperty("required",out JsonElement require))
                    {
                        if (require.ValueKind == JsonValueKind.Array)
                        {
                            // 构建传入参数键值对
                            foreach (var item in require.EnumerateArray())
                            {
                                Console.WriteLine($"请输入必需参数: {item}");
                                object argValue = string.Empty;
                                Console.Write(">");
                                string inputstr = Console.ReadLine() ?? string.Empty;
                                if (propdics.TryGetValue(item.ToString(),out string type))
                                {
                                    if (type == "object")
                                    {
                                        argValue = JsonDocument.Parse(inputstr); // 涉及到object需要转换类型
                                    }
                                    else
                                    {
                                        argValue = inputstr;
                                    }
                                }
                                argsDic.Add(item.GetString() ?? string.Empty, argValue);
                            }
                        }
                    }
                Console.WriteLine(JsonSerializer.Serialize(argsDic));
                // 调用工具方法
                var result = await mcpClient.CallToolAsync(message, argsDic); // pass argument to the call method
                // 输出调用结果
                foreach (var block in result.Content)
                {
                    string resultmsg = result.IsError != null && result.IsError.Value ? "失败" : "成功";
                    Console.WriteLine($"响应结果:调用{resultmsg} 返回结果如下:");
                    if (block is TextContentBlock textBlock)
                    {
                        Console.Write(textBlock.Text);
                    }
                    else
                    {
                        Console.Write($"Received unexpected result content of type {block.GetType()}");
                    }
                    Console.WriteLine();
                }
                continue;
            }
            else
                {
                    Console.WriteLine($"工具 {message} 不存在,请重新输入");
            }
        }
        消息通知回调
消息通知LoggingMessageNotification回调函数。
            
            
              cs
              
              
            
          
          private static async ValueTask NotificationHandler(JsonRpcNotification notification, CancellationToken token)
{
    if (JsonSerializer.Deserialize<LoggingMessageNotificationParams>(notification.Params) is { } ln)
    {
        Console.WriteLine($"[{ln.Level}] {ln.Logger} {ln.Data}");
    }
    else
    {
        Console.WriteLine($"Received unexpected logging notification: {notification.Params}");
    }
}
        工具传入结果解析
工具请求参数解析函数,通过递归方式进行解析。
            
            
              cs
              
              
            
          
          private static void GetRequestParams(JsonElement input, Dictionary<string, string> propdics,StringBuilder stringBuilder,string flag="")
{
    if (input.TryGetProperty("properties", out JsonElement properties))
    {
        // 列出所有参数信息
        foreach (var prop in properties.EnumerateObject())
        {
            propdics.Add(prop.Name, prop.Value.ToString() ?? string.Empty);
            stringBuilder.Append($"{flag}参数名称: {prop.Name} ");
            var propDetails = prop.Value;
            if (propDetails.TryGetProperty("type", out JsonElement type))
            {
                propdics[prop.Name] = type.ToString();
                stringBuilder.Append($"参数类型: {type} ");
                
                if (propDetails.TryGetProperty("description", out var description))
                {
                    stringBuilder.Append($"参数描述: {description}");
                }
                
                stringBuilder.AppendLine();
                if (type.ToString() == "object")
                {
                    string newflag = flag + ">";
                    GetRequestParams(propDetails, propdics,stringBuilder,newflag);
                }
            }
        }
    }
}
        运行解析效果
运行时,调用指定工具,输入如下:

调整服务端
修改HttpTransport配置
服务端与客户端进行交互时,可以通过配置WithHttpTransport() 实现委托处理。
            
            
              cs
              
              
            
          
          .WithHttpTransport(options =>
    {
        options.IdleTimeout = Timeout.InfiniteTimeSpan; // 永不超时
        options.ConfigureSessionOptions = ConfigureSessionOptions;// 会话处理
        options.RunSessionHandler = RunSessionHandler;//会话运行处理
    }
)
        会话处理ConfigureSessionOptions,主要配置在McpServerOptions ,可对配置进行读取和配置,HttpContext 实例,包含在服务注册和构建过程中添加的对应服务。
            
            
              cs
              
              
            
          
          private static Task ConfigureSessionOptions(HttpContext context, McpServerOptions options, CancellationToken token)
{
    return Task.CompletedTask;
}
        会话运行时处理RunSessionHandler,执行顺序会高于ConfigureSessionOptions。
            
            
              cs
              
              
            
          
          private static async Task RunSessionHandler(HttpContext context, McpServer server, CancellationToken token)
{
    // 运行服务端并处理客户端请求
    await server.RunAsync(token);
    return;
}
        处理配置HttpServerTransportOptions
其他的处理执行,可自行查看HttpServerTransportOptions属性。
            
            
              cs
              
              
            
          
          public class HttpServerTransportOptions
{
    public Func<HttpContext, McpServerOptions, CancellationToken, Task>? ConfigureSessionOptions { get; set; }
    public Func<HttpContext, McpServer, CancellationToken, Task>? RunSessionHandler { get; set; }
    public bool Stateless { get; set; }
    public bool PerSessionExecutionContext { get; set; }
    public TimeSpan IdleTimeout { get; set; } = TimeSpan.FromHours(2);
    public int MaxIdleSessionCount { get; set; } = 10_000;
    public TimeProvider TimeProvider { get; set; } = TimeProvider.System;
}
        配置服务端开放能力
设置提供给客户端访问的服务端能力。
            
            
              cs
              
              
            
          
          .AddMcpServer(options => {
    options.Capabilities = new ServerCapabilities
    {
        Logging = new LoggingCapability() { },// 日志能力
        Resources = new ResourcesCapability(),// 资源能力
        Tools = new ToolsCapability(),// 工具能力
        Prompts = new PromptsCapability(),// 提示词能力
    };
}) 
        处理日志等级变化
当客户端设置修改需要监听的日志等级,服务端可以进行监听处理。
            
            
              cs
              
              
            
          
          builder.Services.AddMcpServer(options => {
   // 省略
}) 
.WithHttpTransport(options =>
   // 省略
)
.WithSetLoggingLevelHandler(SetLoggingLevelHandler) // 添加设置日志级别处理器
        设置日志监听等级SetLoggingLevelHandler,其中request.Server 对应着McpServer实例,发送通知消息实际是通过McpServer调用SendNotificationAsync()。
            
            
              cs
              
              
            
          
          private static ValueTask<EmptyResult> SetLoggingLevelHandler(RequestContext<SetLevelRequestParams> request, CancellationToken cancellationToken)
{
    Console.WriteLine($"客户端设置日志过滤等级为:{request.Params.Level}");
    var message = new
    {
        Level = request.Params.Level.ToString().ToLower(),
        Logger = nameof(McpServer),
        Data = new { Message="HHHHHHHH" },
    };
    // 发送测试消息到客户端
    request.Server.SendNotificationAsync(NotificationMethods.LoggingMessageNotification, message,cancellationToken:cancellationToken);
    return new ValueTask<EmptyResult>(new EmptyResult());
}
        工具函数添加日志
对应工具函数通过方法注入的方式注入McpServer 实例,以工具Echo 为例。
            
            
              cs
              
              
            
          
          /// <summary>
/// 通用命令工具
/// </summary>
[McpServerToolType]
internal class CommonTools
{
    /// <summary>
    /// Echo 输出工具
    /// </summary>
    /// <param name="message">客户端发送信息</param>
    /// <returns></returns>
    [McpServerTool, Description("Echo 输出工具,将客户端发送进行返回输出")]
    public string Echo(McpServer server, string message) {
        var provider = server.AsClientLoggerProvider();// 获取日志提供器
        ILogger logger = provider.CreateLogger(nameof(CommonTools));// 创建日志实例
        logger.LogInformation("工具Echo调用成功");// 输出日志信息
       return $"你好 {message}";
    }
}
        日志消息通知效果
客户端连接后,调用Echo运行效果如下,其中输出[Info] CommonTools 工具Echo调用成功,实际表明信息回调成功。
            
            
              bash
              
              
            
          
          [Debug] McpServer {"message":"HHHHHHHH"}
请输入需要调用的工具名称
>echo
输入参数要求
参数名称: message 参数类型: string
请输入必需参数: message
>HHHH
{"message":"HHHH"}
[Info] CommonTools 工具Echo调用成功
响应结果:调用成功 返回结果如下:
你好 HHHH
请输入需要调用的工具名称
>
        退出服务,运行AppHost,查看Aspire面板,在运行客户端,可以看到对应的日志输出信息。

反思与源码排查
这个时候,有读者可能会疑问,为什么不通过ILogger<> 方式进行方法注入。
实际笔者也想过,也尝试过,当前ModelContextProtocol 即之前提到过的MCP SDK版本为0.4.0-preview.2。
            
            
              xml
              
              
            
          
          <PackageReference Include="ModelContextProtocol" Version="0.4.0-preview.2" />
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="0.4.0-preview.2" />
        解析日志发送逻辑
解析logger.LogInformation("工具Echo调用成功");结合之前的SetLoggingLevelHandler,可以找到对应日志实例代码ClientLogger,同样是通过McpServer 进行NotificationMethods.LoggingMessageNotification 日志推送,关键还在McpServer,而McpServer 有关联着ClientLoggerProvider 构建,要想通过ILogger<> 进行注入,就需要能够先创建McpServer 实例。
            
            
              cs
              
              
            
          
          private sealed class ClientLogger(McpServer server, string categoryName) : ILogger
{
	// [重点]McpServer实例
    private readonly McpServer _server = server;
    private readonly string _categoryName = categoryName;
    // 省略
    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
    {
		// 省略
        LogInternal(logLevel, formatter(state, exception));
        void LogInternal(LogLevel level, string message)
        {
	        // [重点]发送通知到日志消息
            _ = _server.SendNotificationAsync(NotificationMethods.LoggingMessageNotification, new LoggingMessageNotificationParams
            {
                Level = McpServerImpl.ToLoggingLevel(level),
                Data = JsonSerializer.SerializeToElement(message, McpJsonUtilities.JsonContext.Default.String),
                Logger = _categoryName,
            });
        }
    }
}
        客户端连接方式排查
通过查阅代码,使用http/sse 方式作为server 服务时,实际构建实例代码在MapMcp() 中。
            
            
              cs
              
              
            
          
          public static IEndpointConventionBuilder MapMcp(this IEndpointRouteBuilder endpoints, [StringSyntax("Route")] string pattern = "")
{
		// 省略
        var sseHandler = endpoints.ServiceProvider.GetRequiredService<SseHandler>();
        var sseGroup = mcpGroup.MapGroup("")
            .WithDisplayName(b => $"MCP HTTP with SSE | {b.DisplayName}");
		// [重点]连接sse 实际调用的sseHandler HandleSseRequestAsync 
        sseGroup.MapGet("/sse", sseHandler.HandleSseRequestAsync)
            .WithMetadata(new ProducesResponseTypeMetadata(StatusCodes.Status200OK, contentTypes: ["text/event-stream"]));
        sseGroup.MapPost("/message", sseHandler.HandleMessageRequestAsync)
            .WithMetadata(new AcceptsMetadata(["application/json"]))
            .WithMetadata(new ProducesResponseTypeMetadata(StatusCodes.Status202Accepted));
	    // 省略
}
        查看一下HandleSseRequestAsync 实现代码。
            
            
              cs
              
              
            
          
          public async Task HandleSseRequestAsync(HttpContext context)
{
	// 省略
    try
    {
		// 省略
        try
        {
		    // 创建mcpServer
            await using var mcpServer = McpServer.Create(transport, mcpServerOptions, loggerFactory, context.RequestServices);
            context.Features.Set(mcpServer);
			// 运行会话委托
            var runSessionAsync = httpMcpServerOptions.Value.RunSessionHandler ?? StreamableHttpHandler.RunSessionAsync;
            await runSessionAsync(context, mcpServer, cancellationToken);
        }
        finally
        {
            await transport.DisposeAsync();
            await transportTask;
        }
    }
    catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
    {
		// 省略
    }
    finally
    {
	    // 会话退出时,移除会话
        _sessions.TryRemove(sessionId, out _);
    }
}
        想法验证结论
可能还是不太理解,简单说就是通过http/sse 连接服务端,mcpServer 的创建并未包含在依赖注入的管理容器中,故目前无法通过函数注入的方式注入McpServer 理想状态下通过类似如下注册日志提供器。
            
            
              cs
              
              
            
          
          builder.Services.AddSingleton<ILoggerProvider>(provider =>
{
    var mcpserver = provider.GetService<McpServer>();// 目前无法实现
    return mcpserver.AsClientLoggerProvider();
});
        就看之后官方是否能够有其他解决方案,就目前的实现逻辑来看,日志的注入看起来不太优雅,注入McpServer 需要手动构建Logger 实例。