Skip to content

Commit 05961be

Browse files
committed
feat: Enhance MCP server management with shortcut functionality
- Added `showShortcut` property to MCP server models and DTOs. - Implemented UI controls for toggling MCP shortcuts in chat input. - Introduced `McpShortcutControl` component for managing MCP shortcuts. - Updated `McpController` to handle shortcut visibility for user assignments. - Enhanced `AssignUsersModal` to include shortcut settings for assigned users. - Added server instructions field to MCP server management and modal. - Updated translations for new features and UI elements.
1 parent 617b52c commit 05961be

17 files changed

Lines changed: 588 additions & 104 deletions

src/BE/web/Chats.BE.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<PackageReference Include="Microsoft.Bcl.Memory" Version="10.0.9" />
2626
<PackageReference Include="Microsoft.ML.Tokenizers.Data.O200kBase" Version="2.0.0" />
2727
<PackageReference Include="MiniExcel" Version="1.45.0" />
28-
<PackageReference Include="ModelContextProtocol.Core" Version="1.4.1" />
28+
<PackageReference Include="ModelContextProtocol.Core" Version="2.0.0" />
2929
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
3030
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
3131
<PackageReference Include="System.Runtime.Caching" Version="10.0.9" />

src/BE/web/Controllers/Chats/Chats/ChatController.cs

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
using Chats.DB.Enums;
2929
using Chats.BE.DB.Extensions;
3030
using Chats.BE.Services.CodeInterpreter;
31+
using Chats.BE.Services.Mcp;
3132
using Chats.BE.Services.Options;
3233
using Chats.BE.Services.RequestTracing;
3334
using Chats.BE.Services.TitleSummary;
@@ -518,14 +519,20 @@ private async Task ProcessChatSpan(
518519
currentRoundSteps: dbUserMessage?.Steps ?? [],
519520
codeExecutionEnabled: codeExecutionEnabled,
520521
contextPrefix: ciPrefix);
522+
NeutralSystemMessage? systemMessage = chatSpan.ChatConfig.CodeExecutionEnabled
523+
? codeInterpreter.BuildSystemMessage(chatSpan.ChatConfig.SystemPrompt)
524+
: null;
525+
systemMessage = McpServerInstructionsBuilder.MergeSystemMessage(
526+
systemMessage,
527+
chatSpan.ChatConfig.SystemPrompt,
528+
chatSpan.ChatConfig.ChatConfigMcps.Select(x => x.McpServer));
529+
521530
ChatRequest csr = new()
522531
{
523532
EndUserId = $"{chat.Id}-{chatSpan.SpanId}",
524533
Messages = neutralMessages,
525534
ChatConfig = chatSpan.ChatConfig,
526-
System = chatSpan.ChatConfig.CodeExecutionEnabled
527-
? codeInterpreter.BuildSystemMessage(chatSpan.ChatConfig.SystemPrompt)
528-
: null,
535+
System = systemMessage,
529536
Tools = [],
530537
Source = UsageSource.WebChat,
531538
};
@@ -710,16 +717,25 @@ .. artifactStepContents
710717
logger.LogInformation("Using MCP Server {mcpServer.Label} ({mcpServer.Url}) for tool call {call.Name} with headers: {headers}",
711718
mcpServer.Label, mcpServer.Url, call.Name, headers);
712719
Stopwatch sw = Stopwatch.StartNew();
713-
McpClient mcpClient = await McpClient.CreateAsync(new HttpClientTransport(new HttpClientTransportOptions
720+
bool isSuccess;
721+
string toolResult;
722+
await using (HttpClientTransport transport = new(
723+
new HttpClientTransportOptions
724+
{
725+
Endpoint = new Uri(mcpServer.Url),
726+
AdditionalHeaders = headers,
727+
},
728+
httpClientFactory.CreateClient(HttpClientNames.ChatControllerMcp),
729+
loggerFactory,
730+
ownsHttpClient: false))
731+
await using (McpClient mcpClient = await McpClient.CreateAsync(transport, cancellationToken: cancellationToken))
714732
{
715-
Endpoint = new Uri(mcpServer.Url),
716-
AdditionalHeaders = headers,
717-
}, httpClientFactory.CreateClient(HttpClientNames.ChatControllerMcp), loggerFactory, ownsHttpClient: true), cancellationToken: cancellationToken);
733+
logger.LogInformation("{mcpServer.Label} connected, elapsed={elapsed}ms, Calling tool: {toolName}, parameters: {call.Parameters}",
734+
mcpServer.Label, sw.ElapsedMilliseconds, toolName, call.Parameters);
718735

719-
logger.LogInformation("{mcpServer.Label} connected, elapsed={elapsed}ms, Calling tool: {toolName}, parameters: {call.Parameters}",
720-
mcpServer.Label, sw.ElapsedMilliseconds, toolName, call.Parameters);
736+
(isSuccess, toolResult) = await CallMcp(mcpClient, cancellationToken);
737+
}
721738

722-
(bool isSuccess, string toolResult) = await CallMcp(cancellationToken);
723739
logger.LogInformation("Tool {call.Name} completed, success: {success}, result: {result}", call.Name, isSuccess, toolResult);
724740
writer.TryWrite(new ToolCompletedLine(chatSpan.SpanId, true, call.ToolCallId!, toolResult));
725741
WriteStep(new Step()
@@ -744,7 +760,7 @@ .. artifactStepContents
744760
],
745761
});
746762

747-
async Task<(bool success, string result)> CallMcp(CancellationToken cancellationToken)
763+
async Task<(bool success, string result)> CallMcp(McpClient mcpClient, CancellationToken cancellationToken)
748764
{
749765
try
750766
{
Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1-
namespace Chats.BE.Controllers.Users.Mcps.Dtos;
1+
using System.Text.Json.Serialization;
22

3-
public record FetchToolsRequest(string ServerUrl, string? Headers);
3+
namespace Chats.BE.Controllers.Users.Mcps.Dtos;
4+
5+
public record FetchToolsRequest(string ServerUrl, string? Headers);
6+
7+
public record FetchToolsResponse
8+
{
9+
[JsonPropertyName("tools")] public required List<McpToolBasicInfo> Tools { get; init; }
10+
[JsonPropertyName("serverInstructions")] public string? ServerInstructions { get; init; }
11+
}

src/BE/web/Controllers/Users/Mcps/Dtos/McpServerDetailsDto.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ namespace Chats.BE.Controllers.Users.Mcps.Dtos;
55
public record McpServerDetailsDto : ManagementMcpServerDto
66
{
77
[JsonPropertyName("headers")] public string? Headers { get; init; }
8+
[JsonPropertyName("serverInstructions")] public string? ServerInstructions { get; init; }
89
[JsonPropertyName("tools")] public required List<McpToolBasicInfo> Tools { get; init; }
910
}

src/BE/web/Controllers/Users/Mcps/Dtos/McpServerListItemDto.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ public record McpServerListItemDto
66
{
77
[JsonPropertyName("id")] public required int Id { get; init; }
88
[JsonPropertyName("label")] public required string Label { get; init; }
9+
[JsonPropertyName("showShortcut")] public bool ShowShortcut { get; init; }
910
}
1011

1112
public record ManagementMcpServerDto : McpServerListItemDto
@@ -17,4 +18,6 @@ public record ManagementMcpServerDto : McpServerListItemDto
1718
[JsonPropertyName("owner")] public required string Owner { get; init; }
1819
[JsonPropertyName("editable")] public required bool Editable { get; init; }
1920
[JsonPropertyName("assignedUserCount")] public required int AssignedUserCount { get; init; }
21+
/// <summary>Whether the current user is assigned this MCP (can toggle own shortcut).</summary>
22+
[JsonPropertyName("assignedToMe")] public bool AssignedToMe { get; init; }
2023
}

src/BE/web/Controllers/Users/Mcps/Dtos/UpdateMcpServerRequest.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ public record UpdateMcpServerRequest
77
[JsonPropertyName("label")] public required string Label { get; init; }
88
[JsonPropertyName("url")] public required string Url { get; init; }
99
[JsonPropertyName("headers")] public string? Headers { get; init; }
10+
[JsonPropertyName("serverInstructions")] public string? ServerInstructions { get; init; }
1011
[JsonPropertyName("tools")] public required List<McpToolBasicInfo> Tools { get; init; }
1112

1213
public bool ValidateToolNameUnique()

src/BE/web/Controllers/Users/Mcps/Dtos/UserAssignmentDtos.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public record AssignedUserInfo
1515
{
1616
[JsonPropertyName("id")] public required int Id { get; init; }
1717
[JsonPropertyName("customHeaders")] public string? CustomHeaders { get; init; }
18+
[JsonPropertyName("showShortcut")] public bool? ShowShortcut { get; init; }
1819
}
1920

2021
// 未分配用户信息
@@ -30,6 +31,13 @@ public record AssignedUserDetailsDto
3031
[JsonPropertyName("id")] public required int Id { get; init; }
3132
[JsonPropertyName("userName")] public required string UserName { get; init; }
3233
[JsonPropertyName("customHeaders")] public string? CustomHeaders { get; init; }
34+
[JsonPropertyName("showShortcut")] public required bool ShowShortcut { get; init; }
35+
}
36+
37+
// 当前用户更新自己的 MCP 赋权偏好
38+
public record UpdateMyMcpAssignmentRequest
39+
{
40+
[JsonPropertyName("showShortcut")] public required bool ShowShortcut { get; init; }
3341
}
3442

3543
// 用于快速获取用户名的DTO

src/BE/web/Controllers/Users/Mcps/McpController.cs

Lines changed: 85 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,21 @@ private static bool IsNullOrWhiteSpaceOrJsonObject(string? text)
2828
return false;
2929
}
3030
}
31+
32+
private static string? NormalizeOptionalText(string? text)
33+
=> string.IsNullOrWhiteSpace(text) ? null : text.Trim();
34+
3135
[HttpGet]
3236
public async Task<ActionResult<McpServerListItemDto[]>> ListAllMcpServers(CancellationToken cancellationToken)
3337
{
34-
IQueryable<McpServer> query = db.McpServers.Where(x => x.UserMcps.Any(um => um.UserId == currentUser.Id));
35-
36-
McpServerListItemDto[] data = await query
37-
.OrderByDescending(x => x.Id)
38-
.Select(x => new McpServerListItemDto
38+
McpServerListItemDto[] data = await db.UserMcps
39+
.Where(um => um.UserId == currentUser.Id)
40+
.OrderByDescending(um => um.McpServerId)
41+
.Select(um => new McpServerListItemDto
3942
{
40-
Id = x.Id,
41-
Label = x.Label,
43+
Id = um.McpServer.Id,
44+
Label = um.McpServer.Label,
45+
ShowShortcut = um.ShowShortcut,
4246
})
4347
.ToArrayAsync(cancellationToken);
4448
return Ok(data);
@@ -64,6 +68,11 @@ public async Task<ActionResult<ManagementMcpServerDto[]>> ListAllMcpServersForMa
6468
Editable = currentUser.IsAdmin || x.OwnerUserId == currentUser.Id, // admin or owner can edit
6569
Owner = x.OwnerUser.DisplayName,
6670
AssignedUserCount = x.UserMcps.Count,
71+
AssignedToMe = x.UserMcps.Any(um => um.UserId == currentUser.Id),
72+
ShowShortcut = x.UserMcps
73+
.Where(um => um.UserId == currentUser.Id)
74+
.Select(um => (bool?)um.ShowShortcut)
75+
.FirstOrDefault() ?? false,
6776
})
6877
.ToArrayAsync(cancellationToken);
6978
return Ok(data);
@@ -92,12 +101,18 @@ public async Task<ActionResult<McpServerDetailsDto>> GetMcpServerDetails(int mcp
92101
Label = x.Label,
93102
Url = x.Url,
94103
Headers = x.Headers,
104+
ServerInstructions = x.ServerInstructions,
95105
Owner = x.OwnerUser.DisplayName,
96106
CreatedAt = x.CreatedAt,
97107
UpdatedAt = x.UpdatedAt,
98108
ToolsCount = x.McpTools.Count,
99109
Editable = currentUser.IsAdmin || x.OwnerUserId == currentUser.Id,
100110
AssignedUserCount = x.UserMcps.Count,
111+
AssignedToMe = x.UserMcps.Any(um => um.UserId == currentUser.Id),
112+
ShowShortcut = x.UserMcps
113+
.Where(um => um.UserId == currentUser.Id)
114+
.Select(um => (bool?)um.ShowShortcut)
115+
.FirstOrDefault() ?? false,
101116
Tools = x.McpTools
102117
.OrderBy(t => t.Id)
103118
.Select(t => new McpToolBasicInfo
@@ -167,7 +182,8 @@ public async Task<ActionResult<McpServerDetailsDto>> CreateMcpServer([FromBody]
167182
{
168183
Label = request.Label,
169184
Url = request.Url,
170-
Headers = string.IsNullOrWhiteSpace(request.Headers) ? null : request.Headers,
185+
Headers = NormalizeOptionalText(request.Headers),
186+
ServerInstructions = NormalizeOptionalText(request.ServerInstructions),
171187
OwnerUserId = currentUser.Id,
172188
CreatedAt = DateTime.UtcNow,
173189
UpdatedAt = DateTime.UtcNow,
@@ -176,6 +192,7 @@ public async Task<ActionResult<McpServerDetailsDto>> CreateMcpServer([FromBody]
176192
new UserMcp
177193
{
178194
UserId = currentUser.Id, // auto-assign to creator
195+
ShowShortcut = false,
179196
}
180197
]
181198
};
@@ -256,12 +273,14 @@ public async Task<ActionResult<McpServerDetailsDto>> UpdateMcpServer(int mcpId,
256273

257274
server.Label = request.Label;
258275
server.Url = request.Url;
259-
server.Headers = string.IsNullOrWhiteSpace(request.Headers) ? null : request.Headers;
276+
server.Headers = NormalizeOptionalText(request.Headers);
277+
server.ServerInstructions = NormalizeOptionalText(request.ServerInstructions);
260278
if (!server.UserMcps.Any(um => um.UserId == currentUser.Id))
261279
{
262280
server.UserMcps.Add(new UserMcp
263281
{
264282
UserId = currentUser.Id,
283+
ShowShortcut = false,
265284
});
266285
}
267286

@@ -328,8 +347,8 @@ public async Task<ActionResult> DeleteMcpServer(int mcpId, CancellationToken can
328347
}
329348

330349
[HttpPost("fetch-tools")]
331-
public async Task<ActionResult<List<McpToolBasicInfo>>> FetchMcpTools(
332-
[FromBody] FetchToolsRequest req,
350+
public async Task<ActionResult<FetchToolsResponse>> FetchMcpTools(
351+
[FromBody] FetchToolsRequest req,
333352
[FromServices] ILogger<McpController> logger,
334353
[FromServices] IHttpClientFactory httpClientFactory,
335354
[FromServices] ILoggerFactory loggerFactory,
@@ -364,7 +383,12 @@ public async Task<ActionResult<List<McpToolBasicInfo>>> FetchMcpTools(
364383

365384
try
366385
{
367-
McpClient client = await McpClient.CreateAsync(new HttpClientTransport(options, httpClientFactory.CreateClient(HttpClientNames.McpController), loggerFactory), cancellationToken: cancellationToken);
386+
await using HttpClientTransport transport = new(
387+
options,
388+
httpClientFactory.CreateClient(HttpClientNames.McpController),
389+
loggerFactory,
390+
ownsHttpClient: false);
391+
await using McpClient client = await McpClient.CreateAsync(transport, cancellationToken: cancellationToken);
368392
List<McpToolBasicInfo> tools = [];
369393
ListToolsResult mcpToolsResp = await client.ListToolsAsync(new ListToolsRequestParams(), cancellationToken);
370394
foreach (Tool tool in mcpToolsResp.Tools)
@@ -376,9 +400,13 @@ public async Task<ActionResult<List<McpToolBasicInfo>>> FetchMcpTools(
376400
Parameters = JSON.Serialize(tool.InputSchema),
377401
});
378402
}
379-
return Ok(tools);
403+
return Ok(new FetchToolsResponse
404+
{
405+
Tools = tools,
406+
ServerInstructions = NormalizeOptionalText(client.ServerInstructions),
407+
});
380408
}
381-
catch (HttpRequestException ex)
409+
catch (Exception ex) when (ex is HttpRequestException or TimeoutException or JsonException or IOException or InvalidOperationException)
382410
{
383411
logger.LogWarning(ex, "Failed to fetch MCP tools from {Url}", req.ServerUrl);
384412
return BadRequest(ex.Message);
@@ -404,7 +432,7 @@ public async Task<ActionResult> AssignUsersToMcp(int mcpId, [FromBody] AssignUse
404432
McpServer? server = await db.McpServers
405433
.Include(x => x.UserMcps)
406434
.FirstOrDefaultAsync(x => x.Id == mcpId, cancellationToken);
407-
435+
408436
if (server == null)
409437
{
410438
return NotFound();
@@ -421,13 +449,17 @@ public async Task<ActionResult> AssignUsersToMcp(int mcpId, [FromBody] AssignUse
421449
.Where(u => allUserIds.Contains(u.Id))
422450
.Select(u => u.Id)
423451
.ToListAsync(cancellationToken);
424-
452+
425453
if (existingUserIds.Count != allUserIds.Count)
426454
{
427455
List<int> missingUserIds = [.. allUserIds.Except(existingUserIds)];
428456
return BadRequest($"User IDs not found: {string.Join(", ", missingUserIds)}");
429457
}
430458

459+
bool assignerShowShortcut = server.UserMcps
460+
.FirstOrDefault(um => um.UserId == currentUser.Id)
461+
?.ShowShortcut ?? false;
462+
431463
// 处理新分配的用户
432464
foreach (AssignedUserInfo userInfo in request.ToAssignedUsers)
433465
{
@@ -447,6 +479,7 @@ public async Task<ActionResult> AssignUsersToMcp(int mcpId, [FromBody] AssignUse
447479
{
448480
UserId = userInfo.Id,
449481
CustomHeaders = userInfo.CustomHeaders,
482+
ShowShortcut = userInfo.ShowShortcut ?? assignerShowShortcut,
450483
McpServerId = mcpId
451484
});
452485
}
@@ -466,6 +499,10 @@ public async Task<ActionResult> AssignUsersToMcp(int mcpId, [FromBody] AssignUse
466499
}
467500

468501
existingAssignment.CustomHeaders = userInfo.CustomHeaders;
502+
if (userInfo.ShowShortcut.HasValue)
503+
{
504+
existingAssignment.ShowShortcut = userInfo.ShowShortcut.Value;
505+
}
469506
}
470507

471508
// 处理删除的用户
@@ -491,9 +528,9 @@ public async Task<ActionResult> AssignUsersToMcp(int mcpId, [FromBody] AssignUse
491528

492529
[HttpGet("{mcpId:int}/get-unassigned-users")]
493530
public async Task<ActionResult<UnassignedUserDto[]>> GetUnassignedUsers(
494-
int mcpId,
495-
[FromQuery] string? search = null,
496-
[FromQuery] int limit = 10,
531+
int mcpId,
532+
[FromQuery] string? search = null,
533+
[FromQuery] int limit = 10,
497534
CancellationToken cancellationToken = default)
498535
{
499536
// 只有管理员可以调用此API
@@ -558,13 +595,41 @@ public async Task<ActionResult<AssignedUserDetailsDto[]>> GetAssignedUserDetails
558595
{
559596
Id = um.UserId,
560597
UserName = um.User.DisplayName,
561-
CustomHeaders = um.CustomHeaders
598+
CustomHeaders = um.CustomHeaders,
599+
ShowShortcut = um.ShowShortcut,
562600
})
563601
.ToArrayAsync(cancellationToken);
564602

565603
return Ok(assignedUsers);
566604
}
567605

606+
[HttpPut("{mcpId:int}/my-assignment")]
607+
public async Task<ActionResult> UpdateMyMcpAssignment(
608+
int mcpId,
609+
[FromBody] UpdateMyMcpAssignmentRequest request,
610+
CancellationToken cancellationToken)
611+
{
612+
if (!ModelState.IsValid)
613+
{
614+
return BadRequest(ModelState);
615+
}
616+
617+
UserMcp? assignment = await db.UserMcps
618+
.FirstOrDefaultAsync(um => um.McpServerId == mcpId && um.UserId == currentUser.Id, cancellationToken);
619+
if (assignment is null)
620+
{
621+
return NotFound();
622+
}
623+
624+
if (assignment.ShowShortcut != request.ShowShortcut)
625+
{
626+
assignment.ShowShortcut = request.ShowShortcut;
627+
await db.SaveChangesAsync(cancellationToken);
628+
}
629+
630+
return Ok();
631+
}
632+
568633
[HttpGet("{mcpId:int}/assigned-user-names")]
569634
public async Task<ActionResult<AssignedUserNameDto[]>> GetAssignedUserNames(int mcpId, CancellationToken cancellationToken)
570635
{

0 commit comments

Comments
 (0)