前言
最近小智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和创客教程