listToolsWithTimeout enumerates a toolset's tools under a bounded deadline. The (potentially blocking) Tools call runs in a goroutine and we select on either its completion or the timeout, so a toolset whose Tools() ignores context cancellation — e.g. a wedged MCP stdio subprocess — cannot block sta
(ctx context.Context, toolset tools.ToolSet, timeout time.Duration)
| 1678 | // sends into a buffered channel and exits if the call ever returns, so it does |
| 1679 | // not leak past the eventual (or never) return of Tools(). |
| 1680 | func listToolsWithTimeout(ctx context.Context, toolset tools.ToolSet, timeout time.Duration) ([]tools.Tool, error) { |
| 1681 | // Defend against a zero/negative timeout (e.g. a directly-constructed |
| 1682 | // LocalRuntime that bypassed NewLocalRuntime) so we never collapse to an |
| 1683 | // already-expired context that skips every toolset. |
| 1684 | if timeout <= 0 { |
| 1685 | timeout = defaultToolListTimeout |
| 1686 | } |
| 1687 | toolCtx, cancel := context.WithTimeout(ctx, timeout) |
| 1688 | defer cancel() |
| 1689 | |
| 1690 | type listResult struct { |
| 1691 | tools []tools.Tool |
| 1692 | err error |
| 1693 | } |
| 1694 | done := make(chan listResult, 1) // buffered so a late send never blocks |
| 1695 | go func() { |
| 1696 | ts, err := toolset.Tools(toolCtx) |
| 1697 | done <- listResult{tools: ts, err: err} |
| 1698 | }() |
| 1699 | |
| 1700 | select { |
| 1701 | case <-toolCtx.Done(): |
| 1702 | return nil, toolCtx.Err() |
| 1703 | case res := <-done: |
| 1704 | return res.tools, res.err |
| 1705 | } |
| 1706 | } |
| 1707 | |
| 1708 | // startToolsetWithTimeout starts a toolset under a bounded deadline, mirroring |
| 1709 | // listToolsWithTimeout. The deadline must be enforced by racing the call |
no test coverage detected