mirror of https://github.com/Wox-launcher/Wox
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:
parent
bc54323d5a
commit
4f67fa1c98
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue