Skip to content

Commit 3523b45

Browse files
PureWeenCopilot
andauthored
fix: load MCP servers on lazy-resume so tools work after restart (#561)
## Summary Two fixes for MCP server availability: ### 1. MCP servers missing on lazy-resumed sessions When sessions are lazily resumed after app restart, the `ResumeSessionConfig` was missing `McpServers` and `SkillDirectories`. This meant MCP tools (e.g., WorkIQ, Maestro) were unavailable until the user manually ran `/mcp reload`. **Fix:** Load MCP servers and skill directories in `EnsureSessionConnectedAsync` before creating the resume config. Also applied to the fresh-session fallback path. ### 2. `/mcp reload` crashes on mobile The command tried to access the local SDK session which doesn't exist in remote mode, throwing `InvalidOperationException`. The guard existed in `CopilotService` but the UI called through without checking first. **Fix:** Guard in `Dashboard.razor` before calling `ReloadMcpServersAsync`, showing a friendly message. ## Test plan - [x] `LazyResumePath_IncludesMcpServersAndSkills` — structural test verifying MCP servers in both resume and fallback configs - [x] Full suite: 3320 tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 592fc14 commit 3523b45

4 files changed

Lines changed: 63 additions & 1 deletion

File tree

PolyPilot.Tests/ChatExperienceSafetyTests.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,6 +1115,50 @@ public void ReconnectPath_IncludesMcpServersAndSkills()
11151115
Assert.Contains("SkillDirectories", helperBlock);
11161116
}
11171117

1118+
/// <summary>
1119+
/// Lazy-resume and its fresh-session fallback must include McpServers and SkillDirectories.
1120+
/// Without these, resumed sessions start without MCP tools (e.g., WorkIQ) until manual /mcp reload.
1121+
/// </summary>
1122+
[Fact]
1123+
public void LazyResumePath_IncludesMcpServersAndSkills()
1124+
{
1125+
var source = File.ReadAllText(
1126+
Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.Persistence.cs"));
1127+
1128+
// Find the EnsureSessionConnectedAsync method containing the lazy-resume logic
1129+
var methodIdx = source.IndexOf("EnsureSessionConnectedAsync", StringComparison.Ordinal);
1130+
Assert.True(methodIdx > 0, "EnsureSessionConnectedAsync not found");
1131+
var block = source.Substring(methodIdx, Math.Min(3000, source.Length - methodIdx));
1132+
1133+
// MCP servers must be loaded before creating the resume config
1134+
Assert.Contains("LoadMcpServers", block);
1135+
Assert.Contains("LoadSkillDirectories", block);
1136+
1137+
// Both the ResumeSessionConfig and the fallback SessionConfig must include MCP
1138+
Assert.Contains("McpServers = mcpServers", block);
1139+
Assert.Contains("SkillDirectories = skillDirs", block);
1140+
}
1141+
1142+
/// <summary>
1143+
/// The public ResumeSessionAsync (sidebar resume, bridge resume) must also include
1144+
/// McpServers and SkillDirectories so MCP tools work after explicit resume.
1145+
/// </summary>
1146+
[Fact]
1147+
public void SidebarResumePath_IncludesMcpServersAndSkills()
1148+
{
1149+
var source = File.ReadAllText(
1150+
Path.Combine(GetRepoRoot(), "PolyPilot", "Services", "CopilotService.cs"));
1151+
1152+
var methodIdx = source.IndexOf("public async Task<AgentSessionInfo> ResumeSessionAsync(", StringComparison.Ordinal);
1153+
Assert.True(methodIdx > 0, "ResumeSessionAsync not found");
1154+
var block = source.Substring(methodIdx, Math.Min(6000, source.Length - methodIdx));
1155+
1156+
Assert.Contains("LoadMcpServers", block);
1157+
Assert.Contains("LoadSkillDirectories", block);
1158+
Assert.Contains("McpServers = resumeMcpServers", block);
1159+
Assert.Contains("SkillDirectories = resumeSkillDirs", block);
1160+
}
1161+
11181162
// =========================================================================
11191163
// F. Race Condition & Edge Case Tests
11201164
// =========================================================================

PolyPilot/Components/Pages/Dashboard.razor

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2344,6 +2344,11 @@
23442344
break;
23452345

23462346
case "reload":
2347+
if (CopilotService.IsRemoteMode)
2348+
{
2349+
session.History.Add(ChatMessage.SystemMessage("⚠️ `/mcp reload` is not available in Remote mode. Use the desktop app to reload MCP servers."));
2350+
break;
2351+
}
23472352
session.History.Add(ChatMessage.SystemMessage("🔄 Reloading MCP servers — replacing SDK session in-place (history preserved)..."));
23482353
_ = ReloadMcpServersAsync(session);
23492354
break;

PolyPilot/Services/CopilotService.Persistence.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,10 +414,18 @@ private async Task EnsureSessionConnectedAsync(string sessionName, SessionState
414414
var resumeModel = state.Info.Model ?? DefaultModel;
415415
var resumeWorkDir = state.Info.WorkingDirectory;
416416

417+
// Load MCP servers and skill directories so resumed sessions have full tool access.
418+
// Without this, lazily-resumed sessions start without MCP tools until manual /mcp reload.
419+
var settings = ConnectionSettings.Load();
420+
var mcpServers = LoadMcpServers(settings.DisabledMcpServers, settings.DisabledPlugins);
421+
var skillDirs = LoadSkillDirectories(settings.DisabledPlugins);
422+
417423
var resumeConfig = new ResumeSessionConfig
418424
{
419425
Model = resumeModel,
420426
WorkingDirectory = resumeWorkDir,
427+
McpServers = mcpServers,
428+
SkillDirectories = skillDirs,
421429
Tools = new List<Microsoft.Extensions.AI.AIFunction> { ShowImageTool.CreateFunction() },
422430
OnPermissionRequest = AutoApprovePermissions,
423431
InfiniteSessions = new InfiniteSessionConfig { Enabled = true },
@@ -441,6 +449,8 @@ private async Task EnsureSessionConnectedAsync(string sessionName, SessionState
441449
{
442450
Model = resumeModel,
443451
WorkingDirectory = resumeWorkDir,
452+
McpServers = mcpServers,
453+
SkillDirectories = skillDirs,
444454
Tools = new List<Microsoft.Extensions.AI.AIFunction> { ShowImageTool.CreateFunction() },
445455
OnPermissionRequest = AutoApprovePermissions,
446456
InfiniteSessions = new InfiniteSessionConfig { Enabled = true },

PolyPilot/Services/CopilotService.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2472,7 +2472,10 @@ public async Task<AgentSessionInfo> ResumeSessionAsync(string sessionId, string
24722472
var resumeModel = Models.ModelHelper.NormalizeToSlug(GetSessionModelFromDisk(sessionId) ?? model ?? DefaultModel);
24732473
if (string.IsNullOrEmpty(resumeModel)) resumeModel = DefaultModel;
24742474
Debug($"Resuming session '{displayName}' with model: '{resumeModel}', cwd: '{resumeWorkingDirectory}'");
2475-
var resumeConfig = new ResumeSessionConfig { Model = resumeModel, WorkingDirectory = resumeWorkingDirectory, Tools = new List<Microsoft.Extensions.AI.AIFunction> { ShowImageTool.CreateFunction() }, OnPermissionRequest = AutoApprovePermissions, InfiniteSessions = new InfiniteSessionConfig { Enabled = true } };
2475+
var resumeSettings = ConnectionSettings.Load();
2476+
var resumeMcpServers = LoadMcpServers(resumeSettings.DisabledMcpServers, resumeSettings.DisabledPlugins);
2477+
var resumeSkillDirs = LoadSkillDirectories(resumeSettings.DisabledPlugins);
2478+
var resumeConfig = new ResumeSessionConfig { Model = resumeModel, WorkingDirectory = resumeWorkingDirectory, McpServers = resumeMcpServers, SkillDirectories = resumeSkillDirs, Tools = new List<Microsoft.Extensions.AI.AIFunction> { ShowImageTool.CreateFunction() }, OnPermissionRequest = AutoApprovePermissions, InfiniteSessions = new InfiniteSessionConfig { Enabled = true } };
24762479
var copilotSession = await GetClientForGroup(groupId).ResumeSessionAsync(sessionId, resumeConfig, cancellationToken);
24772480

24782481
// Detect session ID mismatch: the persistent server may return a different

0 commit comments

Comments
 (0)