mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-14 08:26:07 +00:00
fix mcp sidebar status sync (#2312)
Co-authored-by: Hyu <chenhyu@proton.me>
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user