目录

前言

📺 视频演示

为什么要做这个项目

技术背景

解决方案设计

从这个项目能学到什么

核心技术栈

架构亮点

核心设计理念

领域模型设计

1. XiaozhiMcpEndpoint(小智连接端点)

2. McpServiceConfig(MCP服务配置)

WebSocket会话管理

问题1:如何聚合多个MCP服务的工具?

问题2:小智请求工具列表怎么响应?

问题3:小智调用工具怎么路由到对应的MCP服务?

MCP服务认证支持

分布式WebSocket管理

Blazor前端开发体验

部署和上线

Docker单镜像部署

配置说明

总结与展望

📺 推荐观看视频教程


前言

最近小智AI对话机器人在ESP32社区实在是太火了,看过之前文章的小伙伴应该都知道之前有给桌面机器人开发过一个.NET客户端,所以对小智也算是比较熟悉。小智虽然支持MCP(Model Context Protocol)协议来扩展功能,但是小智的MCP端点是一个特殊的WebSocket服务,如果想要为小智开发MCP功能,就需要针对这个特殊的端点进行开发。

于是就想着能不能做一个转接平台,让开发者可以专注于标准MCP服务的开发,而不用关心小智特殊的WebSocket协议细节。这个平台可以将标准的MCP服务聚合后通过WebSocket提供给小智,同时支持多租户,每个用户都可以配置自己专属的MCP服务。

项目已经上线并开源了,大家可以直接访问 https://xiaozhi.verdure-hiro.cn 体验,也可以在GitHub上找到完整源码:verdure-mcp-for-xiaozhi

img

📺 视频演示

想快速了解项目功能和使用方法?观看我的B站视频教程:

B站视频内容

内容简介

适合人群

小智 MCP 转接服务上线与开源

平台介绍、功能演示、在线使用教程

小智商家和小智爱好者

私有化部署与米家智能家居控制

Docker部署教程、米家MCP服务接入实战

需要私有部署和智能家居控制的用户

为什么要做这个项目

技术背景

小智AI的MCP端点采用的是WebSocket协议,这是一个特殊的实现方式,与标准的MCP协议(基于HTTP/SSE)有所不同。如果想要为小智开发MCP功能,开发者需要:

  1. 了解WebSocket协议细节

    :需要处理连接管理、心跳检测、重连机制等

  2. 实现MCP协议转换

    :将标准MCP的HTTP/SSE请求转换为WebSocket消息

  3. 处理工具聚合

    :如果要使用多个MCP服务,需要自己实现工具列表的聚合和路由

解决方案设计

基于这些技术需求,设计了一个MCP服务转接平台来简化开发:

  • 协议转换

    :自动将标准MCP服务(HTTP/SSE)转换为小智的WebSocket协议

  • 多租户架构

    :每个用户都有独立的配置空间,互不干扰

  • 可视化管理

    :通过Web界面管理MCP服务配置,无需手动编辑配置文件

  • 服务聚合

    :平台作为中间层,将多个MCP服务的工具聚合后提供给小智

  • 分布式支持

    :支持多实例部署,通过Redis实现分布式协调

从这个项目能学到什么

核心技术栈

  • .NET 9

     - 新的.NET框架,性能和开发体验都很棒(准备过段时间升级.NET 10)

  • Blazor WebAssembly

     - 纯C#开发前端,无需学习JavaScript

  • 领域驱动设计(DDD)

     - 规范的分层架构和领域模型

  • 仓储模式

     - 数据访问层的最佳实践

  • Keycloak认证

     - OpenID Connect标准的身份认证

  • WebSocket编程

     - 实时双向通信的实现

  • 分布式协调

     - 基于Redis的分布式锁和状态管理

架构亮点

这个项目展示了企业级.NET应用的完整架构:

verdure-mcp-for-xiaozhi/
├── Domain/           # 领域层:聚合根、实体、仓储接口
├── Application/      # 应用服务层:业务逻辑编排
├── Infrastructure/   # 基础设施层:数据访问、外部服务
├── Api/             # API层:RESTful接口、WebSocket服务
└── Web/             # Blazor前端:组件化UI开发

平台界面展示

核心设计理念

领域模型设计

项目采用DDD设计,核心有两个聚合根:

1. XiaozhiMcpEndpoint(小智连接端点)

代表用户配置的小智WebSocket连接地址,是整个系统的核心实体。

public class XiaozhiMcpEndpoint : Entity, IAggregateRoot
{
public string Name { get; privateset; }
public string Address { get; privateset; }  // WebSocket地址
public string UserId { get; privateset; }
public string? Description { get; privateset; }
public bool IsEnabled { get; privateset; }  // 是否启用连接
public bool IsConnected { get; privateset; } // 实时连接状态

// 时间戳追踪
public DateTime CreatedAt { get; privateset; }
public DateTime? UpdatedAt { get; privateset; }
public DateTime? LastConnectedAt { get; privateset; }
public DateTime? LastDisconnectedAt { get; privateset; }

// 服务绑定集合 - 私有字段,只读暴露
private readonly List<McpServiceBinding> _serviceBindings = new();
public IReadOnlyCollection<McpServiceBinding> ServiceBindings => _serviceBindings.AsReadOnly();

publicXiaozhiMcpEndpoint(string name, string address, string userId, string? description = null)
    {
        GenerateId(); // 使用Guid Version 7生成ID
        Name = name;
        Address = address;
        UserId = userId;
        Description = description;
        IsEnabled = false; // 默认禁用,需用户主动启用
        IsConnected = false;
        CreatedAt = DateTime.UtcNow;
    }

// 核心业务方法:启用端点
publicvoidEnable()
    {
        IsEnabled = true;
        UpdatedAt = DateTime.UtcNow;
    }

// 核心业务方法:禁用端点并断开连接
publicvoidDisable()
    {
        IsEnabled = false;
        IsConnected = false;
        UpdatedAt = DateTime.UtcNow;
    }

// ...其他方法: SetConnected(), SetDisconnected(), UpdateInfo()等已省略
}

设计要点:

  • 使用私有setter保护数据完整性

  • 通过方法(Enable/Disable)而非直接修改属性来改变状态

  • IsEnabled和IsConnected分离:IsEnabled是用户意图,IsConnected是实际状态

  • ServiceBindings集合封装:私有List配合只读接口暴露,防止外部直接修改

2. McpServiceConfig(MCP服务配置)

代表一个可用的MCP服务节点及其认证配置。

public class McpServiceConfig : Entity, IAggregateRoot
{
public string Name { get; privateset; }
public string Endpoint { get; privateset; }
public string UserId { get; privateset; }
public string? Description { get; privateset; }
public bool IsPublic { get; privateset; }

// 认证配置支持4种类型: bearer, basic, apikey, oauth2
public string? AuthenticationType { get; privateset; }
public string? AuthenticationConfig { get; privateset; } // JSON格式配置

public string? Protocol { get; privateset; } // stdio/http/sse

// 时间戳
public DateTime CreatedAt { get; privateset; }
public DateTime? UpdatedAt { get; privateset; }
public DateTime? LastSyncedAt { get; privateset; }

// 工具集合 - 私有字段,只读暴露
private readonly List<McpTool> _tools = new();
public IReadOnlyCollection<McpTool> Tools => _tools.AsReadOnly();

public McpServiceConfig(
string name, 
string endpoint, 
string userId, 
string? description = null,
string? authenticationType = null,
string? authenticationConfig = null,
string? protocol = null)
    {
        GenerateId();
        Name = name;
        Endpoint = endpoint;
        UserId = userId;
        Description = description;
        IsPublic = false; // 默认私有
        AuthenticationType = authenticationType;
        AuthenticationConfig = authenticationConfig;
        Protocol = protocol ?? "stdio"; // 默认stdio协议
        CreatedAt = DateTime.UtcNow;
    }

// 更新服务配置信息
public void UpdateInfo(
string name, 
string endpoint, 
string? description = null,
string? authenticationType = null,
string? authenticationConfig = null,
string? protocol = null)
    {
        Name = name;
        Endpoint = endpoint;
        Description = description;
        AuthenticationType = authenticationType;
        AuthenticationConfig = authenticationConfig;
        Protocol = protocol;
        UpdatedAt = DateTime.UtcNow;
    }

publicvoidSetPublic()
    {
        IsPublic = true;
        UpdatedAt = DateTime.UtcNow;
    }

publicvoidSetPrivate()
    {
        IsPublic = false;
        UpdatedAt = DateTime.UtcNow;
    }

// ...其他方法: UpdateAuthenticationConfig()等已省略
}

设计要点:

  • 支持4种认证方式(Bearer/Basic/API Key/OAuth2),认证配置以JSON存储保持灵活性

  • 通过SetPublic/SetPrivate方法控制服务可见性,支持公共服务市场

  • Tools集合封装:私有List配合只读接口暴露,防止外部直接修改

  • 使用Guid Version 7作为ID,提供更好的数据库索引性能和时序特性

WebSocket会话管理

这是整个平台最核心的部分,需要处理几个关键问题:

问题1:如何聚合多个MCP服务的工具?

解决方案:McpSessionService维护多个McpClient实例

public class McpSessionService : IAsyncDisposable
{
private readonly ILogger<McpSessionService> _logger;
private readonly IMcpClientService _mcpClientService;
private readonly McpSessionConfiguration _config;
private readonly ReconnectionSettings _reconnectionSettings;

// Session state
private ClientWebSocket? _webSocket;
private readonly List<McpClient> _mcpClients = new();
// 🔧 追踪每个客户端的服务配置,用于会话恢复
privatereadonly Dictionary<int, McpServiceEndpoint> _clientIndexToServiceConfig = new();
// 🔧 追踪失败的服务,用于定期重试
private readonly Dictionary<string, (McpServiceEndpoint Config, DateTime LastAttempt)> _failedServices = new();

// Ping timeout monitoring
private DateTime _lastPingReceivedTime = DateTime.UtcNow;
private readonly TimeSpan _pingTimeout = TimeSpan.FromSeconds(120);

// Connection status events
public event Func<Task>? OnConnected;
public event Func<string, Task>? OnConnectionFailed;
public event Func<Task>? OnDisconnected;

privateasync Task ConnectAsync(CancellationToken cancellationToken)
    {
// ⚠️ 关键:先连接MCP服务,再连接WebSocket
// 这确保所有后端服务就绪后才告知小智我们在线

        _logger.LogInformation("Server {ServerId}: Connecting to {Count} MCP service(s)...",
            ServerId, _config.McpServices.Count);

var failedServiceNames = new List<string>();

// 1. 先连接所有MCP服务(支持多种认证方式)
foreach (var service in _config.McpServices)
        {
try
            {
// 创建MCP客户端,传递认证配置
var mcpClient = await _mcpClientService.CreateMcpClientAsync(
$"McpService_{service.ServiceName}",
                    service.NodeAddress,
                    service.Protocol ?? "stdio",
                    service.AuthenticationType,  // bearer/basic/apikey/oauth2
                    service.AuthenticationConfig, // JSON格式认证配置
                    cancellationToken);

var clientIndex = _mcpClients.Count;
                _mcpClients.Add(mcpClient);
                _clientIndexToServiceConfig[clientIndex] = service; // 追踪配置用于会话恢复

                _logger.LogInformation("Server {ServerId}: Connected to MCP service {ServiceName}",
                    ServerId, service.ServiceName);
            }
catch (Exception ex)
            {
// 记录失败的服务,供后续重试
                failedServiceNames.Add(service.ServiceName);
                _failedServices[service.ServiceName] = (service, DateTime.UtcNow);
                _logger.LogWarning("Server {ServerId}: Skipping MCP service {ServiceName} - {Error}",
                    ServerId, service.ServiceName, ex.Message);
            }
        }

// 检查是否至少有一个MCP客户端连接成功
if (_mcpClients.Count == 0)
        {
thrownew InvalidOperationException(
$"No MCP clients connected successfully (0/{_config.McpServices.Count})");
        }

        _logger.LogInformation("Server {ServerId}: {SuccessCount}/{TotalCount} MCP services connected",
            ServerId, _mcpClients.Count, _config.McpServices.Count);

// 2. 所有MCP服务就绪后,连接小智WebSocket
        _webSocket = new ClientWebSocket();
await _webSocket.ConnectAsync(new Uri(_config.WebSocketEndpoint), cancellationToken);

        _lastPingReceivedTime = DateTime.UtcNow; // 初始化ping监控

await (OnConnected?.Invoke() ?? Task.CompletedTask); // 触发连接成功回调

// 3. 启动双向通信 + ping超时监控
// ...消息管道和监控逻辑已省略
    }
}

设计要点:

  • List而非Dictionary

    :_mcpClients使用List存储,通过索引映射到配置

  • 失败服务跟踪

    :_failedServices记录失败的服务供后续重试

  • Ping超时监控

    :120秒未收到ping则认为连接断开

  • 连接顺序关键

    :先MCP服务,再WebSocket,确保后端就绪

  • 事件驱动

    :通过OnConnected/OnConnectionFailed/OnDisconnected通知上层

问题2:小智请求工具列表怎么响应?

解决方案:从配置数据直接获取工具信息,不依赖MCP客户端连接状态

private async Task HandleToolsListAsync(int? id, CancellationToken cancellationToken)
{
// ⚡ 性能优化: 直接从配置读取工具列表,不依赖MCP客户端连接状态
// 即使部分MCP服务连接失败,也能返回已配置的完整工具列表

if (_config.McpServices.Count == 0)
    {
await SendErrorResponseAsync(id, -32603, "No MCP services configured",
"No MCP service bindings configured for this endpoint", cancellationToken);
return;
    }

var allTools = new List<object>();

// 遍历所有配置的服务,聚合SelectedTools
foreach (var serviceConfig in _config.McpServices)
    {
foreach (var tool in serviceConfig.SelectedTools)
        {
// 解析存储的InputSchema JSON(完整的工具Schema已在工具同步时保存)
var properties = new Dictionary<string, object>();
var required = Array.Empty<string>();

if (!string.IsNullOrEmpty(tool.InputSchema))
            {
var schemaDoc = JsonDocument.Parse(tool.InputSchema);
if (schemaDoc.RootElement.TryGetProperty("properties", outvar propsElement))
                {
                    properties = JsonElementToObject(propsElement) as Dictionary<string, object> ?? new();
                }
if (schemaDoc.RootElement.TryGetProperty("required", outvar reqElement))
                {
                    required = reqElement.EnumerateArray()
                        .Select(x => x.GetString() ?? "")
                        .ToArray();
                }
            }

// 构建符合MCP协议的工具定义
            allTools.Add(new
            {
                name = tool.Name,
                description = tool.Description,
                inputSchema = new
                {
                    type = "object",
                    properties = properties,
                    required = required,
                    title = $"{tool.Name}Arguments"
                }
            });
        }
    }

// 返回JSON-RPC格式响应
var response = new
    {
        jsonrpc = "2.0",
        id = id,
        result = new { tools = allTools.ToArray() }
    };

await SendWebSocketResponseAsync(response, cancellationToken);
}

关键优化:

  • 直接从配置读取工具数据,即使MCP客户端连接失败也能返回工具列表

  • 完整解析InputSchema的properties和required字段

  • 符合MCP协议的工具schema格式要求

问题3:小智调用工具怎么路由到对应的MCP服务?

解决方案:根据工具名称查找对应的McpClient

private async Task HandleToolsCallAsync(int? id, JsonDocument request, CancellationToken cancellationToken)
{
var toolName = request.RootElement.GetProperty("params")
        .GetProperty("name").GetString();
var arguments = request.RootElement.GetProperty("params")
        .GetProperty("arguments");

// 🔍 遍历所有MCP客户端,查找包含该工具的服务
for (int i = 0; i < _mcpClients.Count; i++)
    {
var mcpClient = _mcpClients[i];
var serviceConfig = _clientIndexToServiceConfig[i];

// 如果配置了SelectedTools,则只在选中的工具中查找
var selectedTools = serviceConfig.SelectedTools;
if (selectedTools.Any())
        {
var isToolSelected = selectedTools.Any(t => t.Name == toolName);
if (!isToolSelected) continue; // 工具未被选中,跳过此服务
        }

// 检查此MCP客户端是否提供该工具
var hasTool = mcpClient.Tools?.Any(t => t.Name == toolName) ?? false;
if (!hasTool) continue;

// 找到目标服务,调用工具
try
        {
var result = await mcpClient.CallToolAsync(
                toolName,
                JsonSerializer.Deserialize<Dictionary<string, object>>(arguments.GetRawText())!,
                cancellationToken: cancellationToken);

// 返回工具调用结果
await SendWebSocketResponseAsync(new
            {
                jsonrpc = "2.0",
                id = id,
                result = result
            }, cancellationToken);

return; // 成功调用,结束
        }
catch (Exception ex)
        {
            _logger.LogError(ex, "Tool call failed for {ToolName} on service {ServiceName}",
                toolName, serviceConfig.ServiceName);
// 继续尝试下一个服务
        }
    }

// 未找到提供该工具的服务
await SendErrorResponseAsync(id, -32601, "Tool not found",
$"No MCP service provides tool '{toolName}'", cancellationToken);
}

MCP服务认证支持

为了支持各种需要认证的MCP服务,我抽象出了统一的认证助手类:

///<summary>
/// MCP认证配置助手类
/// 被McpClientService(工具同步)和McpSessionService(WebSocket连接)共享使用
///</summary>
public static class McpAuthenticationHelper
{
///<summary>
/// 为Bearer、Basic和API Key认证构建HTTP请求头
///</summary>
public static Dictionary<string, string> BuildAuthenticationHeaders(
string authenticationType,
string authenticationConfig,
        ILogger? logger = null)
    {
if (string.IsNullOrEmpty(authenticationType) || string.IsNullOrEmpty(authenticationConfig))
        {
thrownew ArgumentException("Authentication type and config cannot be null or empty");
        }

try
        {
var authType = authenticationType.ToLowerInvariant();

return authType switch
            {
"bearer" => BuildBearerTokenHeaders(authenticationConfig, logger),
"basic" => BuildBasicAuthHeaders(authenticationConfig, logger),
"apikey" => BuildApiKeyHeaders(authenticationConfig, logger),
                _ => thrownew InvalidOperationException(
$"Unsupported authentication type: {authenticationType}. Use BuildOAuth2Options for OAuth 2.0.")
            };
        }
catch (Exception ex)
        {
            logger?.LogError(ex, "Failed to build authentication headers for type {AuthType}", authenticationType);
thrownew InvalidOperationException($"Failed to configure authentication: {ex.Message}", ex);
        }
    }

///<summary>
/// 为OAuth 2.0构建SDK的ClientOAuthOptions配置
///</summary>
public static ClientOAuthOptions BuildOAuth2Options(
string authenticationConfig,
        ILogger? logger = null)
    {
var authConfig = JsonSerializer.Deserialize<OAuth2AuthConfig>(authenticationConfig);

if (string.IsNullOrEmpty(authConfig?.ClientId) || string.IsNullOrEmpty(authConfig.RedirectUri))
        {
thrownew InvalidOperationException("OAuth 2.0 Client ID and Redirect URI are required");
        }

        logger?.LogDebug("Configuring OAuth 2.0 with Client ID: {ClientId}", authConfig.ClientId);

var oauthOptions = new ClientOAuthOptions
        {
            RedirectUri = new Uri(authConfig.RedirectUri),
            ClientId = authConfig.ClientId,
            ClientSecret = authConfig.ClientSecret
        };

if (!string.IsNullOrEmpty(authConfig.Scope))
        {
            oauthOptions.Scopes = authConfig.Scope.Split(' ', StringSplitOptions.RemoveEmptyEntries);
        }

return oauthOptions;
    }

// 私有辅助方法: BuildBearerTokenHeaders, BuildBasicAuthHeaders, BuildApiKeyHeaders
// ...实现细节已省略(解析JSON配置,构建对应的HTTP请求头)
}

设计要点:

  • DRY原则

    :工具同步和WebSocket连接都复用这个助手类,消除了150+行重复代码

  • 双方法设计

    :

    • BuildAuthenticationHeaders

      :处理Bearer/Basic/API Key(通过HTTP Header传递)

    • BuildOAuth2Options

      :处理OAuth 2.0(使用SDK的ClientOAuthOptions)

  • 健壮的错误处理

    :参数验证、异常捕获、详细错误信息

  • 可选日志

    :通过ILogger参数支持调试,不强制依赖

  • 私有辅助方法

    :每种认证类型的具体实现封装在私有方法中,保持代码清晰

分布式WebSocket管理

为了支持多实例部署,我使用Redis实现了分布式协调:

public class McpSessionManager : IAsyncDisposable
{
private readonly IDistributedLockService _lockService; // RedLock实现
private readonly IConnectionStateService _connectionStateService; // Redis状态管理
private readonly Dictionary<string, McpSessionService> _sessions = new();

public async Task<bool> StartSessionAsync(string serverId, CancellationToken cancellationToken = default)
    {
// 1. 本地检查: 避免不必要的锁竞争
if (_sessions.ContainsKey(serverId))
        {
            _logger.LogInformation("Server {ServerId} session already exists locally", serverId);
returnfalse;
        }

// 2. Redis检查: 可能其他实例已连接
var connectionState = await _connectionStateService.GetConnectionStateAsync(serverId);
if (connectionState?.Status == ConnectionStatus.Connected)
        {
            _logger.LogInformation("Server {ServerId} is already connected on instance {InstanceId}",
                serverId, connectionState.InstanceId);
returnfalse;
        }

// 3. 获取分布式锁 (RedLock算法)
var lockKey = $"mcp:session:lock:{serverId}";
var acquired = await _lockService.AcquireLockAsync(
            lockKey,
            expiryTime: TimeSpan.FromMinutes(5),
            waitTime: TimeSpan.FromSeconds(10),
            retryTime: TimeSpan.FromSeconds(1));

if (!acquired)
        {
            _logger.LogWarning("Failed to acquire lock for server {ServerId}", serverId);
returnfalse;
        }

try
        {
// 4. Double-check: 再次检查Redis状态(持有锁后)
            connectionState = await _connectionStateService.GetConnectionStateAsync(serverId);
if (connectionState?.Status == ConnectionStatus.Connected)
            {
                _logger.LogInformation("Server {ServerId} connected by another instance during lock wait",
                    serverId);
returnfalse;
            }

// 5. 构建会话配置(从数据库加载服务绑定、工具等)
var config = await BuildSessionConfigurationAsync(serverId, cancellationToken);

// 6. 创建会话并订阅事件
var session = new McpSessionService(config, _mcpClientService, _loggerFactory);

            session.OnConnected += async () =>
            {
await _connectionStateService.RegisterConnectionAsync(serverId, InstanceId);
await UpdateEndpointStatusAsync(serverId, isConnected: true);
            };

            session.OnConnectionFailed += async (error) =>
            {
await _connectionStateService.UpdateStatusAsync(serverId, ConnectionStatus.Failed);
await UpdateEndpointStatusAsync(serverId, isConnected: false);
            };

            session.OnDisconnected += async () =>
            {
await _connectionStateService.UnregisterConnectionAsync(serverId);
await UpdateEndpointStatusAsync(serverId, isConnected: false);
                _sessions.Remove(serverId); // 清理本地会话
            };

            _sessions[serverId] = session;

// 7. 在后台启动会话(不阻塞)
            _ = Task.Run(async () =>
            {
try
                {
await session.ConnectAsync(cancellationToken);
                }
catch (Exception ex)
                {
                    _logger.LogError(ex, "Session connection failed for {ServerId}", serverId);
                }
            }, cancellationToken);

returntrue;
        }
finally
        {
await _lockService.ReleaseLockAsync(lockKey); // 释放分布式锁
        }
    }

// ...其他方法: StopSessionAsync, BuildSessionConfigurationAsync等已省略
}

分布式设计要点:

  • 三层检查机制

    : 本地字典 → Redis状态 → 分布式锁,最小化锁竞争开销

  • RedLock算法

    : 使用RedLock.net库实现分布式锁,支持多Redis实例容错

  • Double-Check模式

    : 获取锁后再次检查Redis状态,防止竞态条件下的重复连接

  • 事件驱动状态同步

    : 通过OnConnected/OnDisconnected事件自动更新Redis和数据库

  • 非阻塞启动

    : 会话连接在后台Task中执行,StartSessionAsync立即返回

  • 故障转移支持

    : 实例下线时,监控服务可检测到Redis状态变化并在其他实例重启会话

Blazor前端开发体验

Blazor WebAssembly实现了纯C#全栈开发,前后端统一技术栈:

@page "/connections"
@inject IXiaozhiMcpEndpointClientService ServerService
@inject ISnackbar Snackbar

<MudDataGridItems="@_servers"Filterable="true">
<Columns>
<PropertyColumnProperty="x => x.Name"Title="名称" />
<PropertyColumnProperty="x => x.IsConnected"Title="状态">
<CellTemplate>
@if (context.Item.IsConnected)
{
<MudChipColor="Color.Success"Icon="@Icons.Material.Filled.CheckCircle">已连接</MudChip>
}
elseif (context.Item.IsEnabled)
                {
<MudChipColor="Color.Warning">未连接</MudChip>
}
else
{
<MudChip>已禁用</MudChip>
}
</CellTemplate>
</PropertyColumn>
<TemplateColumnTitle="操作">
<CellTemplate>
@if (context.Item.IsEnabled)
{
<MudIconButtonIcon="@Icons.Material.Filled.PowerOff"
Color="Color.Error"
OnClick="@(() => DisableServerAsync(context.Item.Id!))" />
}
else
{
<MudIconButtonIcon="@Icons.Material.Filled.PlayArrow"
Color="Color.Success"
OnClick="@(() => EnableServerAsync(context.Item.Id!))" />
}
</CellTemplate>
</TemplateColumn>
</Columns>
</MudDataGrid>

@code {
private IEnumerable<XiaozhiMcpEndpointDto> _servers = Array.Empty<XiaozhiMcpEndpointDto>();

protectedoverrideasync Task OnInitializedAsync()
    {
await LoadServersAsync();
    }

privateasync Task EnableServerAsync(string serverId)
    {
await ServerService.EnableServerAsync(serverId);
        Snackbar.Add("WebSocket连接已启动", Severity.Success);
await LoadServersAsync();
    }
}

使用MudBlazor组件库,界面开发效率很高,而且组件都是Material Design风格,很美观。

部署和上线

Docker单镜像部署

项目配置了完整的Docker支持,前后端打包到一个镜像中:

# 基础运行时镜像 - Alpine Linux最小化体积
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS base
WORKDIR /app
EXPOSE80808081
RUN apk add --no-cache curl icu-libs tzdata

# 构建镜像
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src

# 复制项目文件并还原依赖
COPY ["Verdure.McpPlatform.sln", "."]
COPY ["src/Verdure.McpPlatform.Api/Verdure.McpPlatform.Api.csproj", "src/Verdure.McpPlatform.Api/"]
COPY ["src/Verdure.McpPlatform.Web/Verdure.McpPlatform.Web.csproj", "src/Verdure.McpPlatform.Web/"]
# ...其他项目文件已省略

RUN dotnet restore "src/Verdure.McpPlatform.Api/Verdure.McpPlatform.Api.csproj"

# 复制源代码并构建
COPY . .
WORKDIR"/src/src/Verdure.McpPlatform.Api"
RUN dotnet publish "Verdure.McpPlatform.Api.csproj" \
    -c $BUILD_CONFIGURATION \
    -o /app/publish \
    /p:UseAppHost=false

# 最终运行时镜像
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .

ENV ASPNETCORE_URLS=http://+:8080
ENV ASPNETCORE_ENVIRONMENT=Production
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false

HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
    CMD curl -f http://localhost:8080/api/health || exit 1

ENTRYPOINT ["dotnet", "Verdure.McpPlatform.Api.dll"]

配置说明

主要需要配置以下环境变量:

{
    "ConnectionStrings":{
        "mcpdb":"Host=localhost;Database=verdure_mcp;Username=postgres;Password=***",
        "identitydb":"Host=localhost;Database=verdure_identity;Username=postgres;Password=***",
        "Redis":"localhost:6379"
    },
    "Database":{
        "Provider":"PostgreSQL",// 或 "SQLite"
        "TablePrefix":"verdure_"// 数据库表名前缀
    },
    "Identity":{
    "Url":"https://auth.verdure-hiro.cn/realms/maker-community",
    "Realm":"maker-community",
    "ClientId":"verdure-mcp",
    "Audience":"verdure-mcp-api",
    "RequireHttpsMetadata":true,
    "ClockSkewMinutes":5
    },
    "ConnectionMonitor":{
        "CheckIntervalSeconds":30,// 监控检查间隔
        "HeartbeatTimeoutSeconds":90,// 心跳超时时间
        "ReconnectCooldownSeconds":60// 重连冷却时间
    },
    "Logging":{
        "LogLevel":{
            "Default":"Information",
            "Verdure.McpPlatform":"Debug"
        }
    }
}

关键配置说明:

  1. 数据库配置:

    • mcpdb

      : 业务数据库连接字符串(MCP服务配置、连接端点等)

    • identitydb

      : 身份认证数据库连接字符串(用户、角色等)

    • Provider

      : 支持PostgreSQL和SQLite两种数据库

    • TablePrefix

      : 统一的表名前缀,用于多租户部署

  2. Redis配置:

    • 用于分布式锁和连接状态管理

    • 生产环境建议配置密码和SSL: "redis:6379,password=***,ssl=true,abortConnect=false"

  3. 身份认证配置:

    • Url

      : Keycloak服务地址,包含realm路径

    • Realm

      : Keycloak realm名称

    • ClientId

      : 客户端ID

    • Audience

      : API受众标识,用于JWT验证

    • ClockSkewMinutes

      : 时钟偏移容忍度,处理服务器时间差异

  4. 连接监控配置:

    • CheckIntervalSeconds

      : WebSocket连接健康检查间隔

    • HeartbeatTimeoutSeconds

      : 心跳超时判定时间

    • ReconnectCooldownSeconds

      : 断开后重连冷却时间

    • 开发环境可以设置更短的间隔(15秒)以快速检测问题

环境变量方式配置:

# 使用环境变量覆盖配置
ConnectionStrings__mcpdb="Host=prod-db;Database=verdure_mcp;..."
ConnectionStrings__Redis="redis:6379,password=***"
Identity__Url="https://auth.example.com/realms/prod"
ConnectionMonitor__CheckIntervalSeconds=60

总结与展望

通过这个项目的开发实践,可以看到.NET生态在全栈开发上的优势:

  • 统一的技术栈

    :从后端API到前端UI都是C#,降低了学习成本

  • 成熟的框架支持

    :EF Core、ASP.NET Core、Blazor等都很完善

  • 企业级特性

    :DDD、仓储模式、分布式协调等都有现成的最佳实践可以参考

目前平台已经上线并开源,大家可以访问在线服务体验,也欢迎在GitHub上贡献代码。后续计划:

  • 添加更多MCP服务的预置模板

  • 实现工具调用的监控和统计功能

  • 开发MCP服务的市场和分享机制

这个项目展示了.NET在现代全栈开发中的应用,希望能给想学习.NET技术的小伙伴提供一个实战参考,同时也为小智社区的生态建设贡献一份力量。

📺 推荐观看视频教程

如果你想更直观地了解项目的使用方法和部署流程,强烈推荐观看我的B站视频内容:

  • 小智 MCP 转接服务上线与开源 - 快速上手指南: https://www.bilibili.com/video/BV1LYkyBoEh1/

  • 私有化部署与米家智能家居控制 - 深度实战教程:https://www.bilibili.com/video/BV1eJUTBYEsi/

  • B站主页 @绿荫阿广 - 获取更多AI和创客教程:https://space.bilibili.com/25228512

引入地址

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐