discoverWorkspaceMCPTools resolves the chat's workspace agent and lists the workspace MCP tools advertised by that agent. Results are cached per chat keyed on the agent ID so subsequent calls hit the cache. Returns nil (and never an error) on every failure mode so the caller can continue without MCP
( ctx context.Context, logger slog.Logger, chatID uuid.UUID, workspaceCtx *turnWorkspaceContext, )
| 545 | // mid-turn PrepareTools path triggered after create_workspace / |
| 546 | // start_workspace bind a workspace to a chat that started without one. |
| 547 | func (p *Server) discoverWorkspaceMCPTools( |
| 548 | ctx context.Context, |
| 549 | logger slog.Logger, |
| 550 | chatID uuid.UUID, |
| 551 | workspaceCtx *turnWorkspaceContext, |
| 552 | ) []fantasy.AgentTool { |
| 553 | // Fast path: check cache using the in-memory cached agent |
| 554 | // (ensureWorkspaceAgent is free when already loaded). This |
| 555 | // avoids a per-turn latest-build DB query on the common |
| 556 | // subsequent-turn path. |
| 557 | if agent, agentErr := workspaceCtx.getWorkspaceAgent(ctx); agentErr == nil { |
| 558 | if tools := p.loadCachedWorkspaceContext( |
| 559 | chatID, agent, workspaceCtx.getWorkspaceConn, |
| 560 | ); tools != nil { |
| 561 | return tools |
| 562 | } |
| 563 | } // Cache miss, agent changed, or no cache: validate |
| 564 | // that the workspace still has a live agent before |
| 565 | // attempting a dial. |
| 566 | _, _, agentErr := workspaceCtx.workspaceAgentIDForConn(ctx) |
| 567 | if agentErr != nil { |
| 568 | if xerrors.Is(agentErr, errChatHasNoWorkspaceAgent) { |
| 569 | p.workspaceMCPToolsCache.Delete(chatID) |
| 570 | return nil |
| 571 | } |
| 572 | logger.Warn(ctx, "failed to resolve workspace agent for MCP tools", |
| 573 | slog.Error(agentErr)) |
| 574 | return nil |
| 575 | } |
| 576 | |
| 577 | // List workspace MCP tools via the agent conn. |
| 578 | conn, connErr := workspaceCtx.getWorkspaceConn(ctx) |
| 579 | if connErr != nil { |
| 580 | logger.Warn(ctx, "failed to get workspace conn for MCP tools", |
| 581 | slog.Error(connErr)) |
| 582 | return nil |
| 583 | } |
| 584 | listCtx, cancel := context.WithTimeout(ctx, workspaceMCPDiscoveryTimeout) |
| 585 | defer cancel() |
| 586 | toolsResp, listErr := conn.ListMCPTools(listCtx) |
| 587 | if listErr != nil { |
| 588 | logger.Warn(ctx, "failed to list workspace MCP tools", |
| 589 | slog.Error(listErr)) |
| 590 | return nil |
| 591 | } |
| 592 | // Cache the result for subsequent turns. Skip caching when |
| 593 | // the list is empty because the agent's MCP Connect may not |
| 594 | // have finished yet; caching an empty list would hide tools |
| 595 | // permanently. |
| 596 | if len(toolsResp.Tools) > 0 { |
| 597 | if agent, agentErr := workspaceCtx.getWorkspaceAgent(ctx); agentErr == nil { |
| 598 | p.workspaceMCPToolsCache.Store(chatID, &cachedWorkspaceMCPTools{ |
| 599 | agentID: agent.ID, |
| 600 | tools: toolsResp.Tools, |
| 601 | }) |
| 602 | } |
| 603 | } |
| 604 |
no test coverage detected