mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-25 05:46:13 +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
|
// Maximum number of entity sub-items visible before "More" toggle
|
||||||
const MAX_VISIBLE_ITEMS = 5;
|
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
|
// Sort entity items by updatedAt descending (most recent first), items without updatedAt go last
|
||||||
function sortByRecent(items: SidebarEntityItem[]): SidebarEntityItem[] {
|
function sortByRecent(items: SidebarEntityItem[]): SidebarEntityItem[] {
|
||||||
@@ -352,11 +394,19 @@ function NavItems({
|
|||||||
if (extRefreshing) return;
|
if (extRefreshing) return;
|
||||||
setExtRefreshing(true);
|
setExtRefreshing(true);
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
const results = await Promise.allSettled([
|
||||||
sidebarData.refreshPlugins(),
|
sidebarData.refreshPlugins(),
|
||||||
sidebarData.refreshMCPServers(),
|
|
||||||
sidebarData.refreshSkills(),
|
sidebarData.refreshSkills(),
|
||||||
|
refreshEnabledMCPConnections(),
|
||||||
]);
|
]);
|
||||||
|
const mcpRefreshResult = results[2];
|
||||||
|
if (mcpRefreshResult.status === 'rejected') {
|
||||||
|
console.error(
|
||||||
|
'Failed to refresh MCP connections:',
|
||||||
|
mcpRefreshResult.reason,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await sidebarData.refreshMCPServers();
|
||||||
} finally {
|
} finally {
|
||||||
setExtRefreshing(false);
|
setExtRefreshing(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,6 +157,10 @@ export default function MCPDetailContent({ id }: { id: string }) {
|
|||||||
navigate(`/home/mcp?id=${encodeURIComponent(serverName)}`);
|
navigate(`/home/mcp?id=${encodeURIComponent(serverName)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handlePersistedTestComplete = useCallback(async () => {
|
||||||
|
await refreshMCPServers();
|
||||||
|
}, [refreshMCPServers]);
|
||||||
|
|
||||||
function confirmDelete() {
|
function confirmDelete() {
|
||||||
httpClient
|
httpClient
|
||||||
.deleteMCPServer(id)
|
.deleteMCPServer(id)
|
||||||
@@ -364,6 +368,7 @@ export default function MCPDetailContent({ id }: { id: string }) {
|
|||||||
onRuntimeInfoChange={(runtimeInfo) =>
|
onRuntimeInfoChange={(runtimeInfo) =>
|
||||||
setDetailRuntimeStatus(runtimeInfo?.status ?? null)
|
setDetailRuntimeStatus(runtimeInfo?.status ?? null)
|
||||||
}
|
}
|
||||||
|
onPersistedTestComplete={handlePersistedTestComplete}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -488,6 +488,7 @@ interface MCPFormProps {
|
|||||||
onDirtyChange?: (dirty: boolean) => void;
|
onDirtyChange?: (dirty: boolean) => void;
|
||||||
onTestingChange?: (testing: boolean) => void;
|
onTestingChange?: (testing: boolean) => void;
|
||||||
onRuntimeInfoChange?: (runtimeInfo: MCPServerRuntimeInfo | null) => void;
|
onRuntimeInfoChange?: (runtimeInfo: MCPServerRuntimeInfo | null) => void;
|
||||||
|
onPersistedTestComplete?: (serverName: string) => void | Promise<void>;
|
||||||
/** Reported when the form cannot be saved because the current mode is
|
/** Reported when the form cannot be saved because the current mode is
|
||||||
* ``stdio`` and the Box sandbox is disabled/unavailable. Parents that
|
* ``stdio`` and the Box sandbox is disabled/unavailable. Parents that
|
||||||
* render the Save button outside this component should disable it. */
|
* render the Save button outside this component should disable it. */
|
||||||
@@ -512,6 +513,7 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
|||||||
onDirtyChange,
|
onDirtyChange,
|
||||||
onTestingChange,
|
onTestingChange,
|
||||||
onRuntimeInfoChange,
|
onRuntimeInfoChange,
|
||||||
|
onPersistedTestComplete,
|
||||||
onSaveBlockedChange,
|
onSaveBlockedChange,
|
||||||
layout = 'stacked',
|
layout = 'stacked',
|
||||||
sideHeader,
|
sideHeader,
|
||||||
@@ -823,6 +825,8 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
|||||||
// are always current.
|
// are always current.
|
||||||
const serverName =
|
const serverName =
|
||||||
isEditMode && initServerName ? initServerName : form.getValues('name');
|
isEditMode && initServerName ? initServerName : form.getValues('name');
|
||||||
|
const shouldTestPersistedServer =
|
||||||
|
isEditMode && !!initServerName && !form.formState.isDirty;
|
||||||
const formExtraArgs = form.getValues('extra_args') ?? [];
|
const formExtraArgs = form.getValues('extra_args') ?? [];
|
||||||
const formStdioArgs = form.getValues('args') ?? [];
|
const formStdioArgs = form.getValues('args') ?? [];
|
||||||
let extraArgsData: MCPServerExtraArgsRemote | MCPServerExtraArgsStdio;
|
let extraArgsData: MCPServerExtraArgsRemote | MCPServerExtraArgsStdio;
|
||||||
@@ -845,12 +849,20 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { task_id } = await httpClient.testMCPServer('_', {
|
const testTarget = shouldTestPersistedServer ? serverName : '_';
|
||||||
name: serverName,
|
const testPayload = shouldTestPersistedServer
|
||||||
mode,
|
? {}
|
||||||
enable: true,
|
: ({
|
||||||
extra_args: extraArgsData,
|
name: serverName,
|
||||||
} as MCPServer);
|
mode,
|
||||||
|
enable: true,
|
||||||
|
extra_args: extraArgsData,
|
||||||
|
} as MCPServer);
|
||||||
|
|
||||||
|
const { task_id } = await httpClient.testMCPServer(
|
||||||
|
testTarget,
|
||||||
|
testPayload,
|
||||||
|
);
|
||||||
|
|
||||||
if (!task_id) {
|
if (!task_id) {
|
||||||
throw new Error(t('mcp.noTaskId'));
|
throw new Error(t('mcp.noTaskId'));
|
||||||
@@ -876,14 +888,18 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
|||||||
resource_count: 0,
|
resource_count: 0,
|
||||||
resources: [],
|
resources: [],
|
||||||
});
|
});
|
||||||
|
if (shouldTestPersistedServer) {
|
||||||
|
await onPersistedTestComplete?.(serverName);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isEditMode) {
|
if (shouldTestPersistedServer) {
|
||||||
await loadServerForEdit(serverName);
|
await loadServerForEdit(serverName);
|
||||||
|
await onPersistedTestComplete?.(serverName);
|
||||||
} else {
|
} 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 +
|
// The backend stashes the discovered runtime info (status +
|
||||||
// tools) in the test task's metadata before tearing the
|
// tools) in the task metadata before tearing the transient
|
||||||
// transient session down — surface it so a successful test
|
// session down — surface it so a successful test
|
||||||
// shows the tool list instead of "no tools found".
|
// shows the tool list instead of "no tools found".
|
||||||
const runtimeInfoFromTest = taskResp.task_context?.metadata
|
const runtimeInfoFromTest = taskResp.task_context?.metadata
|
||||||
?.runtime_info as MCPServerRuntimeInfo | undefined;
|
?.runtime_info as MCPServerRuntimeInfo | undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user