前言
最近小智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
📺 視頻演示
想快速瞭解項目功能和使用方法?觀看我的B站視頻教程:
| B站視頻內容 | 內容簡介 | 適合人羣 |
|---|---|---|
| 小智 MCP 轉接服務上線與開源 | 平台介紹、功能演示、在線使用教程 | 小智商家和小智愛好者 |
| 私有化部署與米家智能家居控制 | Docker部署教程、米家MCP服務接入實戰 | 需要私有部署和智能家居控制的用户 |
為什麼要做這個項目
技術背景
小智AI的MCP端點採用的是WebSocket協議,這是一個特殊的實現方式,與標準的MCP協議(基於HTTP/SSE)有所不同。如果想要為小智開發MCP功能,開發者需要:
- 瞭解WebSocket協議細節:需要處理連接管理、心跳檢測、重連機制等
- 實現MCP協議轉換:將標準MCP的HTTP/SSE請求轉換為WebSocket消息
- 處理工具聚合:如果要使用多個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"
}
}
}
關鍵配置説明:
-
數據庫配置:
mcpdb: 業務數據庫連接字符串(MCP服務配置、連接端點等)identitydb: 身份認證數據庫連接字符串(用户、角色等)Provider: 支持PostgreSQL和SQLite兩種數據庫TablePrefix: 統一的表名前綴,用於多租户部署
-
Redis配置:
- 用於分佈式鎖和連接狀態管理
- 生產環境建議配置密碼和SSL:
"redis:6379,password=***,ssl=true,abortConnect=false"
-
身份認證配置:
Url: Keycloak服務地址,包含realm路徑Realm: Keycloak realm名稱ClientId: 客户端IDAudience: API受眾標識,用於JWT驗證ClockSkewMinutes: 時鐘偏移容忍度,處理服務器時間差異
-
連接監控配置:
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組件庫