
调整客户端调用
为方便后续调试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 实例。