博客 / 詳情

返回

使用.NET開發並上線一個小智AI對話機器人的MCP服務轉接平台

前言

最近小智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; private set; }
    public string Address { get; private set; }  // WebSocket地址
    public string UserId { get; private set; }
    public string? Description { get; private set; }
    public bool IsEnabled { get; private set; }  // 是否啓用連接
    public bool IsConnected { get; private set; } // 實時連接狀態
    
    // 時間戳追蹤
    public DateTime CreatedAt { get; private set; }
    public DateTime? UpdatedAt { get; private set; }
    public DateTime? LastConnectedAt { get; private set; }
    public DateTime? LastDisconnectedAt { get; private set; }
    
    // 服務綁定集合 - 私有字段,只讀暴露
    private readonly List<McpServiceBinding> _serviceBindings = new();
    public IReadOnlyCollection<McpServiceBinding> ServiceBindings => _serviceBindings.AsReadOnly();
    
    public XiaozhiMcpEndpoint(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;
    }
    
    // 核心業務方法:啓用端點
    public void Enable()
    {
        IsEnabled = true;
        UpdatedAt = DateTime.UtcNow;
    }
    
    // 核心業務方法:禁用端點並斷開連接
    public void Disable()
    {
        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; private set; }
    public string Endpoint { get; private set; }
    public string UserId { get; private set; }
    public string? Description { get; private set; }
    public bool IsPublic { get; private set; }
    
    // 認證配置支持4種類型: bearer, basic, apikey, oauth2
    public string? AuthenticationType { get; private set; }
    public string? AuthenticationConfig { get; private set; } // JSON格式配置
    
    public string? Protocol { get; private set; } // stdio/http/sse
    
    // 時間戳
    public DateTime CreatedAt { get; private set; }
    public DateTime? UpdatedAt { get; private set; }
    public DateTime? LastSyncedAt { get; private set; }
    
    // 工具集合 - 私有字段,只讀暴露
    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;
    }
    
    public void SetPublic()
    {
        IsPublic = true;
        UpdatedAt = DateTime.UtcNow;
    }
    
    public void SetPrivate()
    {
        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();
    // 🔧 追蹤每個客户端的服務配置,用於會話恢復
    private readonly 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;
    
    private async 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)
        {
            throw new 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", out var propsElement))
                {
                    properties = JsonElementToObject(propsElement) as Dictionary<string, object> ?? new();
                }
                if (schemaDoc.RootElement.TryGetProperty("required", out var 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))
        {
            throw new 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),
                _ => throw new 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);
            throw new 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))
        {
            throw new 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);
            return false;
        }
        
        // 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);
            return false;
        }
        
        // 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);
            return false;
        }
        
        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);
                return false;
            }
            
            // 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);
            
            return true;
        }
        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

<MudDataGrid Items="@_servers" Filterable="true">
    <Columns>
        <PropertyColumn Property="x => x.Name" Title="名稱" />
        <PropertyColumn Property="x => x.IsConnected" Title="狀態">
            <CellTemplate>
                @if (context.Item.IsConnected)
                {
                    <MudChip Color="Color.Success" Icon="@Icons.Material.Filled.CheckCircle">已連接</MudChip>
                }
                else if (context.Item.IsEnabled)
                {
                    <MudChip Color="Color.Warning">未連接</MudChip>
                }
                else
                {
                    <MudChip>已禁用</MudChip>
                }
            </CellTemplate>
        </PropertyColumn>
        <TemplateColumn Title="操作">
            <CellTemplate>
                @if (context.Item.IsEnabled)
                {
                    <MudIconButton Icon="@Icons.Material.Filled.PowerOff" 
                                   Color="Color.Error" 
                                   OnClick="@(() => DisableServerAsync(context.Item.Id!))" />
                }
                else
                {
                    <MudIconButton Icon="@Icons.Material.Filled.PlayArrow" 
                                   Color="Color.Success" 
                                   OnClick="@(() => EnableServerAsync(context.Item.Id!))" />
                }
            </CellTemplate>
        </TemplateColumn>
    </Columns>
</MudDataGrid>

@code {
    private IEnumerable<XiaozhiMcpEndpointDto> _servers = Array.Empty<XiaozhiMcpEndpointDto>();
    
    protected override async Task OnInitializedAsync()
    {
        await LoadServersAsync();
    }
    
    private async 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
EXPOSE 8080 8081
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 轉接服務上線與開源 - 快速上手指南
  • 私有化部署與米家智能家居控制 - 深度實戰教程
  • B站主頁 @綠蔭阿廣 - 獲取更多AI和創客教程

參考推薦

  • 創客社區地址
  • 項目開源倉庫
  • 在線體驗地址
  • 小智AI官網
  • MCP協議規範
  • 小智MCP例子
  • .NET官方文檔
  • Blazor官方文檔
  • MudBlazor組件庫
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.