feat(ai): add timeout protection for MCP tool listing

* Implemented a timeout mechanism for the `MCPListTools` function to prevent long-running operations.
* Utilized goroutines and channels to handle tool listing asynchronously.
* Enhanced error handling to manage potential panics during tool listing.
* Updated documentation to reflect the new timeout protection feature.
This commit is contained in:
qianlifeng 2025-06-03 21:14:28 +08:00
parent bc54323d5a
commit 4f67fa1c98
No known key found for this signature in database
6 changed files with 67 additions and 40 deletions

View File

@ -66,7 +66,7 @@ func getMCPClient(ctx context.Context, config common.AIChatMCPServerConfig) (c c
return mcpClient, nil
}
// MCPListTools lists the tools for a given MCP server config
// MCPListTools lists the tools for a given MCP server config with timeout protection
func MCPListTools(ctx context.Context, config common.AIChatMCPServerConfig) ([]common.MCPTool, error) {
if tools, ok := mcpTools.Load(config.Name); ok {
util.GetLogger().Debug(ctx, fmt.Sprintf("Listing tools for MCP server from cache: %s", config.Name))
@ -74,17 +74,68 @@ func MCPListTools(ctx context.Context, config common.AIChatMCPServerConfig) ([]c
}
util.GetLogger().Debug(ctx, fmt.Sprintf("Listing tools for MCP server: %s", config.Name))
client, err := getMCPClient(ctx, config)
if err != nil {
return nil, err
// Use channel and goroutine to implement timeout protection
type listToolsResult struct {
tools []common.MCPTool
err error
}
// List Tools
tools, err := client.ListTools(ctx, mcp.ListToolsRequest{})
if err != nil {
return nil, err
}
resultChan := make(chan listToolsResult, 1)
// Start the actual tool listing in a separate goroutine
go func() {
defer func() {
if r := recover(); r != nil {
util.GetLogger().Error(ctx, fmt.Sprintf("Panic in MCPListTools for server %s: %v", config.Name, r))
resultChan <- listToolsResult{
tools: nil,
err: fmt.Errorf("panic occurred while listing tools: %v", r),
}
}
}()
// Create timeout context for this operation (30 seconds)
timeoutCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
client, err := getMCPClient(timeoutCtx, config)
if err != nil {
resultChan <- listToolsResult{tools: nil, err: err}
return
}
// List Tools
tools, err := client.ListTools(timeoutCtx, mcp.ListToolsRequest{})
if err != nil {
resultChan <- listToolsResult{tools: nil, err: err}
return
}
// Process tools and send result
processedTools, processErr := processToolsResponse(timeoutCtx, tools, config, client)
resultChan <- listToolsResult{tools: processedTools, err: processErr}
}()
// Wait for result or timeout
select {
case result := <-resultChan:
if result.err != nil {
return nil, result.err
}
util.GetLogger().Debug(ctx, fmt.Sprintf("Found %d tools", len(result.tools)))
mcpTools.Store(config.Name, result.tools)
return result.tools, nil
case <-time.After(35 * time.Second): // Slightly longer than the context timeout
util.GetLogger().Error(ctx, fmt.Sprintf("Timeout listing tools for MCP server: %s", config.Name))
return nil, fmt.Errorf("timeout after 35 seconds listing tools for server: %s", config.Name)
}
}
// processToolsResponse processes the tools response and converts to MCPTool format
func processToolsResponse(ctx context.Context, tools *mcp.ListToolsResult, config common.AIChatMCPServerConfig, client client.MCPClient) ([]common.MCPTool, error) {
var toolsList []common.MCPTool
for _, tool := range tools.Tools {

View File

@ -1400,6 +1400,7 @@ func (m *Manager) ExecuteRefresh(ctx context.Context, refreshableResultWithId Re
return RefreshableResultWithResultId{}, fmt.Errorf("failed to copy refreshable result: %w", copyErr)
}
// maybe user has changed the query, which may flush the result cache
resultCache, found := m.resultCache.Load(refreshableResultWithId.ResultId)
if !found {
return refreshableResultWithId, fmt.Errorf("result cache not found for result id (execute refresh): %s", refreshableResultWithId.ResultId)

View File

@ -399,7 +399,6 @@ func (r *AIChatPlugin) reloadMCPServers(ctx context.Context) {
r.mcpToolsMap = mcpTools
plugin.GetPluginManager().GetUI().ReloadChatResources(ctx, "tools")
}
func (r *AIChatPlugin) loadMCPServers(ctx context.Context) ([]common.AIChatMCPServerConfig, error) {

View File

@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "ead455963c12b453cdb2358cad34969c76daf180"
revision: "be698c48a6750c8cb8e61c740ca9991bb947aba2"
channel: "stable"
project_type: app
@ -13,26 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: ead455963c12b453cdb2358cad34969c76daf180
base_revision: ead455963c12b453cdb2358cad34969c76daf180
- platform: android
create_revision: ead455963c12b453cdb2358cad34969c76daf180
base_revision: ead455963c12b453cdb2358cad34969c76daf180
- platform: ios
create_revision: ead455963c12b453cdb2358cad34969c76daf180
base_revision: ead455963c12b453cdb2358cad34969c76daf180
- platform: linux
create_revision: ead455963c12b453cdb2358cad34969c76daf180
base_revision: ead455963c12b453cdb2358cad34969c76daf180
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
- platform: macos
create_revision: ead455963c12b453cdb2358cad34969c76daf180
base_revision: ead455963c12b453cdb2358cad34969c76daf180
- platform: web
create_revision: ead455963c12b453cdb2358cad34969c76daf180
base_revision: ead455963c12b453cdb2358cad34969c76daf180
- platform: windows
create_revision: ead455963c12b453cdb2358cad34969c76daf180
base_revision: ead455963c12b453cdb2358cad34969c76daf180
create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2
# User provided section

View File

@ -280,7 +280,7 @@ class _WoxPreviewViewState extends State<WoxPreviewView> {
// If hasPendingAutoFocusToChatInput is true, focus to chat input after the UI has been built
if (launcherController.hasPendingAutoFocusToChatInput) {
chatController.focusToChatInput(const UuidV4().toString());
chatController.focusToChatInput(const UuidV4().generate());
launcherController.hasPendingAutoFocusToChatInput = false;
}

View File

@ -42,15 +42,6 @@ class WoxWebsocketMsgUtil {
var msg = WoxWebsocketMsg.fromJson(jsonDecode(event));
if (msg.success == false) {
Logger.instance.error(msg.traceId, "Received error websocket message: ${msg.toJson()}");
Get.find<WoxLauncherController>().showToolbarMsg(
msg.traceId,
ToolbarMsg(
icon: WoxImage(
imageType: WoxImageTypeEnum.WOX_IMAGE_TYPE_SVG.code,
imageData:
'<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#f21818" d="M12 17q.425 0 .713-.288T13 16t-.288-.712T12 15t-.712.288T11 16t.288.713T12 17m-1-4h2V7h-2zm1 9q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22"/></svg>'),
text: msg.data,
));
return;
}