fix mcp sidebar status sync (#2312)

Co-authored-by: Hyu <chenhyu@proton.me>
This commit is contained in:
RockChinQ
2026-07-03 20:23:12 +08:00
committed by GitHub
parent ffabb91bfe
commit f3b5fcfb7c
3 changed files with 83 additions and 12 deletions
@@ -264,6 +264,48 @@ function saveListExpansionState(state: SidebarListExpansionState) {
// Maximum number of entity sub-items visible before "More" toggle
const MAX_VISIBLE_ITEMS = 5;
const MCP_REFRESH_POLL_INTERVAL_MS = 1000;
const MCP_REFRESH_TIMEOUT_MS = 60000;
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function waitForMCPRefreshTask(taskId: number) {
const deadline = Date.now() + MCP_REFRESH_TIMEOUT_MS;
while (Date.now() < deadline) {
const task = await httpClient.getAsyncTask(taskId);
if (task.runtime.done) return task;
await sleep(MCP_REFRESH_POLL_INTERVAL_MS);
}
throw new Error(`Timed out waiting for MCP refresh task ${taskId}`);
}
async function refreshEnabledMCPConnections() {
const resp = await httpClient.getMCPServers();
const enabledServers = resp.servers.filter((server) => server.enable);
if (enabledServers.length === 0) return;
const taskResults = await Promise.allSettled(
enabledServers.map((server) => httpClient.testMCPServer(server.name, {})),
);
const taskIds: number[] = [];
for (const result of taskResults) {
if (
result.status === 'fulfilled' &&
typeof result.value.task_id === 'number'
) {
taskIds.push(result.value.task_id);
} else if (result.status === 'rejected') {
console.error('Failed to start MCP refresh task:', result.reason);
}
}
await Promise.allSettled(taskIds.map(waitForMCPRefreshTask));
}
// Sort entity items by updatedAt descending (most recent first), items without updatedAt go last
function sortByRecent(items: SidebarEntityItem[]): SidebarEntityItem[] {
@@ -352,11 +394,19 @@ function NavItems({
if (extRefreshing) return;
setExtRefreshing(true);
try {
await Promise.all([
const results = await Promise.allSettled([
sidebarData.refreshPlugins(),
sidebarData.refreshMCPServers(),
sidebarData.refreshSkills(),
refreshEnabledMCPConnections(),
]);
const mcpRefreshResult = results[2];
if (mcpRefreshResult.status === 'rejected') {
console.error(
'Failed to refresh MCP connections:',
mcpRefreshResult.reason,
);
}
await sidebarData.refreshMCPServers();
} finally {
setExtRefreshing(false);
}
@@ -157,6 +157,10 @@ export default function MCPDetailContent({ id }: { id: string }) {
navigate(`/home/mcp?id=${encodeURIComponent(serverName)}`);
}
const handlePersistedTestComplete = useCallback(async () => {
await refreshMCPServers();
}, [refreshMCPServers]);
function confirmDelete() {
httpClient
.deleteMCPServer(id)
@@ -364,6 +368,7 @@ export default function MCPDetailContent({ id }: { id: string }) {
onRuntimeInfoChange={(runtimeInfo) =>
setDetailRuntimeStatus(runtimeInfo?.status ?? null)
}
onPersistedTestComplete={handlePersistedTestComplete}
/>
</div>
</div>
@@ -488,6 +488,7 @@ interface MCPFormProps {
onDirtyChange?: (dirty: boolean) => void;
onTestingChange?: (testing: boolean) => void;
onRuntimeInfoChange?: (runtimeInfo: MCPServerRuntimeInfo | null) => void;
onPersistedTestComplete?: (serverName: string) => void | Promise<void>;
/** Reported when the form cannot be saved because the current mode is
* ``stdio`` and the Box sandbox is disabled/unavailable. Parents that
* render the Save button outside this component should disable it. */
@@ -512,6 +513,7 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
onDirtyChange,
onTestingChange,
onRuntimeInfoChange,
onPersistedTestComplete,
onSaveBlockedChange,
layout = 'stacked',
sideHeader,
@@ -823,6 +825,8 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
// are always current.
const serverName =
isEditMode && initServerName ? initServerName : form.getValues('name');
const shouldTestPersistedServer =
isEditMode && !!initServerName && !form.formState.isDirty;
const formExtraArgs = form.getValues('extra_args') ?? [];
const formStdioArgs = form.getValues('args') ?? [];
let extraArgsData: MCPServerExtraArgsRemote | MCPServerExtraArgsStdio;
@@ -845,12 +849,20 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
};
}
const { task_id } = await httpClient.testMCPServer('_', {
name: serverName,
mode,
enable: true,
extra_args: extraArgsData,
} as MCPServer);
const testTarget = shouldTestPersistedServer ? serverName : '_';
const testPayload = shouldTestPersistedServer
? {}
: ({
name: serverName,
mode,
enable: true,
extra_args: extraArgsData,
} as MCPServer);
const { task_id } = await httpClient.testMCPServer(
testTarget,
testPayload,
);
if (!task_id) {
throw new Error(t('mcp.noTaskId'));
@@ -876,14 +888,18 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
resource_count: 0,
resources: [],
});
if (shouldTestPersistedServer) {
await onPersistedTestComplete?.(serverName);
}
} else {
if (isEditMode) {
if (shouldTestPersistedServer) {
await loadServerForEdit(serverName);
await onPersistedTestComplete?.(serverName);
} else {
// Create mode has no persisted server to reload tools from.
// Transient tests have no persisted server to reload tools from.
// The backend stashes the discovered runtime info (status +
// tools) in the test task's metadata before tearing the
// transient session down — surface it so a successful test
// tools) in the task metadata before tearing the transient
// session down — surface it so a successful test
// shows the tool list instead of "no tools found".
const runtimeInfoFromTest = taskResp.task_context?.metadata
?.runtime_info as MCPServerRuntimeInfo | undefined;