Skip to content

feat: add new fetch tool and related components#121

Merged
ssdeanx merged 1 commit intomainfrom
develop
Mar 6, 2026
Merged

feat: add new fetch tool and related components#121
ssdeanx merged 1 commit intomainfrom
develop

Conversation

@ssdeanx
Copy link
Copy Markdown
Owner

@ssdeanx ssdeanx commented Mar 6, 2026

  • Introduced a new fetch tool in src/mastra/tools/fetch.tool.ts for web content fetching and markdown conversion.
  • Implemented various search functionalities including DuckDuckGo, Google, and Bing search.
  • Added support for Google News RSS fetching.
  • Included URL validation and sanitization to ensure safe fetching.
  • Created a new React component for MCP A2A page in app/chat/mcp-a2a/page.tsx to manage MCP servers and tools.
  • Developed a workspace management page in app/chat/workspaces/page.tsx to handle file browsing and skill display.
  • Updated exports in src/mastra/tools/index.ts to include the new fetch tool.

Summary by Sourcery

Introduce a new fetch/search tool with markdown output, expand Mastra frontend hooks for workspaces, sandbox, MCP, and A2A, and add new UI pages and navigation for managing workspaces and MCP/A2A integrations.

New Features:

  • Add a production-ready fetch tool for URL fetching and multi-provider web/news search with markdown conversion and RE2-based URL filtering.
  • Expose workspace and sandbox filesystem, search, and skills operations through new React Query hooks for frontend consumption.
  • Introduce a Workspaces page for browsing sandbox files, viewing file contents, and listing workspace skills.
  • Add an MCP / A2A page to inspect MCP servers/tools and view A2A agent cards from the UI.

Bug Fixes:

  • Fix prior compile/type issues in use-mastra-query by fully wiring the new workspace, sandbox, MCP, and A2A hooks and removing stray helper hooks.

Enhancements:

  • Update research agent tooling guidance to prefer the new fetch tool for web access and register it as an available tool.
  • Extend mastra tool exports and UI tool typings to include the new fetch tool.
  • Enrich mastra query key definitions with granular workspace and sandbox cache keys and wire MCP/A2A hooks into the shared hook object.
  • Enhance the main sidebar with navigation entries for the new Workspaces and MCP / A2A sections and minor agent card rendering tweaks.

Documentation:

  • Document the new workspace/sandbox hooks and query keys in AGENTS.md and capture related progress/context in the memory bank files.

- Introduced a new fetch tool in `src/mastra/tools/fetch.tool.ts` for web content fetching and markdown conversion.
- Implemented various search functionalities including DuckDuckGo, Google, and Bing search.
- Added support for Google News RSS fetching.
- Included URL validation and sanitization to ensure safe fetching.
- Created a new React component for MCP A2A page in `app/chat/mcp-a2a/page.tsx` to manage MCP servers and tools.
- Developed a workspace management page in `app/chat/workspaces/page.tsx` to handle file browsing and skill display.
- Updated exports in `src/mastra/tools/index.ts` to include the new fetch tool.
Copilot AI review requested due to automatic review settings March 6, 2026 15:35
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agent-stack Building Building Preview, Comment Mar 6, 2026 3:35pm

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Mar 6, 2026

Reviewer's Guide

Adds a production-oriented fetch/search tool with markdown output and RE2-based URL filtering, expands Mastra query hooks for workspaces/sandbox/MCP/A2A, and introduces new UI pages to explore workspaces, MCP servers/tools, and A2A agents while wiring everything into side navigation and documentation/memory.

Sequence diagram for the new WorkspacesPage workspace/sandbox interactions

sequenceDiagram
    actor User
    participant WorkspacesPage
    participant MastraHooks as useMastraQuery
    participant ReactQuery as ReactQueryHooks
    participant MastraClient as mastraClient
    participant WorkspaceAPI as Workspace

    User->>WorkspacesPage: Navigate to /chat/workspaces
    WorkspacesPage->>MastraHooks: useMastraQuery()
    MastraHooks-->>WorkspacesPage: { useWorkspaces, useSandboxFiles, useSandboxReadFile, useWorkspaceSkills }

    Note over WorkspacesPage: Fetch workspace list
    WorkspacesPage->>ReactQuery: useWorkspaces()
    ReactQuery->>MastraClient: getWorkspaces()
    MastraClient-->>ReactQuery: { workspaces }
    ReactQuery-->>WorkspacesPage: workspaces data

    Note over WorkspacesPage: Derive activeWorkspaceId and file tree
    WorkspacesPage->>ReactQuery: useSandboxFiles(activeWorkspaceId, "/", true)
    ReactQuery->>MastraClient: getWorkspace(activeWorkspaceId)
    MastraClient->>WorkspaceAPI: listFiles(path="/", recursive=true)
    WorkspaceAPI-->>MastraClient: WorkspaceFsListResponse
    MastraClient-->>ReactQuery: files
    ReactQuery-->>WorkspacesPage: files data

    User->>WorkspacesPage: Select file in FileTree
    WorkspacesPage->>ReactQuery: useSandboxReadFile(activeWorkspaceId, selectedFilePath, "utf-8")
    ReactQuery->>MastraClient: getWorkspace(activeWorkspaceId)
    MastraClient->>WorkspaceAPI: readFile(path, encoding)
    WorkspaceAPI-->>MastraClient: WorkspaceFsReadResponse
    MastraClient-->>ReactQuery: file content
    ReactQuery-->>WorkspacesPage: content

    Note over WorkspacesPage: Fetch workspace skills
    WorkspacesPage->>ReactQuery: useWorkspaceSkills(activeWorkspaceId)
    ReactQuery->>MastraClient: getWorkspace(activeWorkspaceId)
    MastraClient->>WorkspaceAPI: listSkills()
    WorkspaceAPI-->>MastraClient: ListSkillsResponse
    MastraClient-->>ReactQuery: skills data
    ReactQuery-->>WorkspacesPage: skills

    WorkspacesPage-->>User: Render file tree, code viewer, skills list
Loading

Sequence diagram for the new McpA2APage MCP server and A2A agent interactions

sequenceDiagram
    actor User
    participant McpA2APage
    participant MastraHooks as useMastraQuery
    participant ReactQuery as ReactQueryHooks
    participant MastraClient as mastraClient
    participant MCPAPI as MCP
    participant AgentsAPI as Agents
    participant A2AAPI as A2A

    User->>McpA2APage: Navigate to /chat/mcp-a2a
    McpA2APage->>MastraHooks: useMastraQuery()
    MastraHooks-->>McpA2APage: { useMcpServers, useMcpServerTools, useAgents, useA2ACard }

    Note over McpA2APage: Load MCP servers
    McpA2APage->>ReactQuery: useMcpServers({ page:0, perPage:50 })
    ReactQuery->>MastraClient: getMcpServers(params)
    MastraClient->>MCPAPI: listServers(params)
    MCPAPI-->>MastraClient: McpServerListResponse
    MastraClient-->>ReactQuery: servers
    ReactQuery-->>McpA2APage: servers data

    Note over McpA2APage: Load tools for active server
    McpA2APage->>ReactQuery: useMcpServerTools(activeServerId)
    ReactQuery->>MastraClient: getMcpServerTools(serverId)
    MastraClient->>MCPAPI: listTools(serverId)
    MCPAPI-->>MastraClient: McpServerToolListResponse
    MastraClient-->>ReactQuery: tools
    ReactQuery-->>McpA2APage: serverTools

    Note over McpA2APage: Load agents list
    McpA2APage->>ReactQuery: useAgents()
    ReactQuery->>MastraClient: getAgents()
    MastraClient->>AgentsAPI: listAgents()
    AgentsAPI-->>MastraClient: agents[]
    MastraClient-->>ReactQuery: agents
    ReactQuery-->>McpA2APage: agents data

    Note over McpA2APage: Load A2A card for active agent
    McpA2APage->>ReactQuery: useA2ACard(activeAgentId)
    ReactQuery->>MastraClient: getA2A(agentId)
    MastraClient->>A2AAPI: getCard()
    A2AAPI-->>MastraClient: AgentCard
    MastraClient-->>ReactQuery: card
    ReactQuery-->>McpA2APage: a2aCard

    McpA2APage-->>User: Render MCP tools and A2A agent card
Loading

Updated class diagram for the fetchTool module

classDiagram
    class FetchToolContext {
      <<interface>>
      +string userAgent
      +number timeout
      +string userId
      +string workspaceId
    }

    class FetchToolError {
      +string code
      +number statusCode
      +string url
      +constructor(message, code, statusCode, url)
    }

    class ValidationUtils {
      <<static>>
      +boolean validateUrl(url)
    }

    class FetchTool {
      <<tool>>
      +execute(inputData, context)
      +onInputStart()
      +onInputDelta()
      +onInputAvailable()
      +onOutput()
    }

    class FetchToolInputSchema {
      <<zodSchema>>
      +url
      +query
      +searchProvider
      +searchVertical
      +maxResults
      +includeContent
      +timeout
      +userAgent
      +contentContext
      +includeUrlPatterns
      +excludeUrlPatterns
    }

    class FetchToolOutputSchema {
      <<zodSchema>>
      +mode
      +query
      +url
      +markdown
      +results
      +metadata
    }

    class SearchResult {
      +string title
      +string url
      +string snippet
    }

    class HttpFetch {
      <<function>>
      +httpFetch(url, options)
    }

    class FetchHelpers {
      <<module>>
      +buildRequestHeaders(userAgent)
      +sanitizeHtml(html)
      +htmlToMarkdown(html)
      +compileRe2Patterns(patterns)
      +passesRe2Filters(value, include, exclude)
      +dedupeResults(results)
      +normalizeUrl(rawUrl)
      +isNewsQuery(query)
      +applyContentWindow(markdown, window)
      +resolveContentWindow(input)
      +extractDuckDuckGoResults(html)
      +extractGoogleResults(html)
      +extractBingResults(html)
      +searchDuckDuckGo(options)
      +searchGoogle(options)
      +searchBing(options)
      +searchGoogleNewsRss(options)
      +fetchPageAsMarkdown(options)
    }

    class ObservabilitySpan {
      <<from @mastra/core/observability>>
      +update(data)
      +end()
      +error(options)
    }

    class TracingContext {
      <<from @mastra/core/observability>>
    }

    class RequestContext {
      <<from @mastra/core/request-context>>
    }

    class Writer {
      <<toolWriter>>
      +custom(event)
    }

    class Error

    FetchToolError --|> Error
    FetchToolContext --|> RequestContext

    FetchTool ..> FetchToolInputSchema : uses
    FetchTool ..> FetchToolOutputSchema : uses
    FetchTool ..> FetchHelpers : calls
    FetchTool ..> FetchToolError : throws
    FetchTool ..> FetchToolContext : cast requestContext
    FetchTool ..> ObservabilitySpan : tracing span
    FetchTool ..> TracingContext : tracingContext
    FetchTool ..> Writer : progress events
    FetchTool ..> HttpFetch : network calls

    FetchHelpers ..> SearchResult : returns
    FetchHelpers ..> HttpFetch : uses
    FetchHelpers ..> ValidationUtils : uses
Loading

File-Level Changes

Change Details Files
Implement a new robust fetch/search tool that fetches web content, performs multi-provider search, converts results to markdown, and exposes it through the Mastra tool system.
  • Create fetch tool with zod-validated inputs/outputs, tracing hooks, and progress events using Mastra tool APIs
  • Implement HTML sanitization and HTML→markdown conversion using JSDOM and cheerio, with configurable content windowing and truncation modes
  • Support DuckDuckGo, Google, Bing, and Google News RSS search with per-provider parsers and RE2-based URL include/exclude filters plus tracking-parameter stripping
  • Introduce FetchToolContext, error handling types, and export fetchTool and FetchUITool via existing tools index and UI tool typing
src/mastra/tools/fetch.tool.ts
src/mastra/tools/index.ts
src/components/ai-elements/tools/types.ts
src/mastra/agents/researchAgent.ts
Extend Mastra React query hooks to fully support workspace and sandbox filesystem/search/skills, MCP servers/tools, and A2A agent interactions.
  • Add workspace hooks for info, file listing/reading/stat, search, skills, and skills search, plus write/delete/mkdir/index mutations with proper cache invalidation
  • Create parallel sandbox hooks for filesystem/search with separate query keys and invalidation, decoupled from workspace hooks
  • Add MCP hooks for listing servers, fetching server/tool details, and executing tools, as well as A2A hooks for cards, messaging, task retrieval, and cancellation
  • Expand mastraQueryKeys with granular keys for workspaces, sandbox, mcp, and a2a, and wire all new hooks into the MastraQueryHooks interface and returned hook object
lib/hooks/use-mastra-query.ts
Add new UI pages to inspect workspaces (files + skills) and MCP/A2A entities, and expose them from the main sidebar.
  • Create a Workspaces page that selects an active workspace, lists sandbox files via FileTree, reads file contents into a syntax-highlighted CodeBlock, and lists workspace skills
  • Create an MCP/A2A page that selects MCP servers, displays their tools, selects agents, and shows their A2A cards (description and skills) using Mastra hooks
  • Add Workspaces and MCP / A2A navigation entries with icons to the main sidebar, and perform minor sidebar conditional rendering cleanups
app/chat/workspaces/page.tsx
app/chat/mcp-a2a/page.tsx
app/chat/components/main-sidebar.tsx
Document and track the new hooks and behaviors in project memory and contributor docs.
  • Update memory bank with progress and active context notes about workspace/sandbox hooks, MCP/A2A hook wiring, and error cleanup in use-mastra-query
  • Extend AGENTS.md to describe the new Mastra query workspace hooks and granular cache keys
  • Refine Copilot instructions to enforce targeted error checking around edited pages and recommend web research tools for framework/API questions
memory-bank/progress.md
memory-bank/activeContext.md
.github/copilot-instructions.md
lib/AGENTS.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 6, 2026

🤖 Hi @ssdeanx, I've received your request, and I'm working on it now! You can track my progress in the logs for more details.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 6, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a32e8f89-f048-4c03-963f-32ea9a7ec7ad

📥 Commits

Reviewing files that changed from the base of the PR and between a63f0d3 and ad53978.

📒 Files selected for processing (12)
  • .github/copilot-instructions.md
  • app/chat/components/main-sidebar.tsx
  • app/chat/mcp-a2a/page.tsx
  • app/chat/workspaces/page.tsx
  • lib/AGENTS.md
  • lib/hooks/use-mastra-query.ts
  • memory-bank/activeContext.md
  • memory-bank/progress.md
  • src/components/ai-elements/tools/types.ts
  • src/mastra/agents/researchAgent.ts
  • src/mastra/tools/fetch.tool.ts
  • src/mastra/tools/index.ts

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Workspaces page: browse workspace files with syntax highlighting and access workspace skills.
    • Added MCP/A2A page: interact with MCP servers and their tools, manage A2A agents.
    • Updated sidebar navigation with Workspaces and MCP/A2A links.
    • Enhanced web fetch and search capabilities for the research agent.
  • Documentation

    • Updated project documentation with workspace, sandbox, and MCP/A2A integration details.

Walkthrough

This PR introduces workspace and MCP/A2A management UI pages with integrated query hooks, adds a production-grade web fetch tool with multi-provider search integration to the research agent, and expands the Mastra query hook system to support workspace, sandbox, MCP, and A2A operations alongside associated cache keys and mutations.

Changes

Cohort / File(s) Summary
Navigation & Sidebar
.github/copilot-instructions.md, app/chat/components/main-sidebar.tsx
Added Copilot page-edit workflow instructions with VS Code error checking guidance; introduced Workspaces and MCP/A2A navigation items in sidebar with icons.
Workspace & MCP UI Pages
app/chat/workspaces/page.tsx, app/chat/mcp-a2a/page.tsx
Added two new client-side pages: Workspaces page with file tree, code viewer, and skills panel; MCP/A2A page with server/tool selector and agent card display. Both leverage new query hooks.
Fetch Tool Implementation
src/mastra/tools/fetch.tool.ts, src/mastra/tools/index.ts, src/mastra/agents/researchAgent.ts, src/components/ai-elements/tools/types.ts
Introduced comprehensive web fetch/search tool with multi-provider support (DuckDuckGo, Google, Bing, Google News RSS), HTML sanitization, markdown conversion, result deduplication, tracing integration, and error handling. Integrated fetchTool into research agent and exported FetchUITool type.
Query Hooks Expansion
lib/hooks/use-mastra-query.ts
Expanded MastraQueryHooks interface with 30\+ new hooks and mutations for workspace, sandbox, MCP, and A2A operations including useWorkspaceFiles, useWorkspaceReadFile, useMcpServers, useA2ACard with corresponding mutations. Extended mastraQueryKeys with granular cache keys for precise invalidation.
Documentation & Progress
lib/AGENTS.md, memory-bank/activeContext.md, memory-bank/progress.md
Added documentation entries detailing workspace hook expansion, hook error cleanup, and new cache key structure for frontend cache invalidation.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant WorkspacePage as Workspace UI
    participant MastraQuery as useMastraQuery Hooks
    participant MastraClient as Mastra Client
    participant Backend as Backend API

    User->>WorkspacePage: Select workspace
    WorkspacePage->>MastraQuery: useWorkspaces()
    MastraQuery->>MastraClient: fetch workspaces
    MastraClient->>Backend: GET /workspaces
    Backend-->>MastraClient: workspace list
    MastraClient-->>MastraQuery: return data
    MastraQuery-->>WorkspacePage: workspaces state updated

    User->>WorkspacePage: Select file path
    WorkspacePage->>MastraQuery: useWorkspaceFiles(workspaceId)
    MastraQuery->>MastraClient: fetch files
    MastraClient->>Backend: GET /workspaces/{id}/files
    Backend-->>MastraClient: file tree
    MastraClient-->>MastraQuery: return data
    MastraQuery-->>WorkspacePage: file content displayed
Loading
sequenceDiagram
    participant Agent
    participant FetchTool as fetchTool
    participant Providers as Search Providers
    participant HTML as HTML Processor
    participant Result as Result Formatter

    Agent->>FetchTool: Execute(query/url)
    FetchTool->>Providers: Query DuckDuckGo, Google, Bing
    Providers-->>FetchTool: Raw results
    FetchTool->>HTML: Sanitize & convert to markdown
    HTML-->>FetchTool: Cleaned markdown
    FetchTool->>Result: Deduplicate, aggregate
    Result-->>FetchTool: Final result set
    FetchTool-->>Agent: Return structured output
Loading
sequenceDiagram
    participant User
    participant McpPage as MCP/A2A UI
    participant MastraQuery as useMcpServers/Agents Hooks
    participant MastraClient as Mastra Client
    participant McpServers as MCP Servers

    User->>McpPage: Load page
    McpPage->>MastraQuery: useMcpServers()
    MastraQuery->>MastraClient: fetch servers
    MastraClient->>McpServers: Query MCP server registry
    McpServers-->>MastraClient: server list
    MastraClient-->>MastraQuery: return data
    MastraQuery-->>McpPage: servers populated

    User->>McpPage: Select server
    McpPage->>MastraQuery: useMcpServerTools(serverId)
    MastraQuery->>MastraClient: fetch tools
    MastraClient->>McpServers: GET /servers/{id}/tools
    McpServers-->>MastraClient: tool list
    MastraClient-->>MastraQuery: return data
    MastraQuery-->>McpPage: tools & agent card displayed
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Poem

🐰 Workspaces bloom, MCP servers call,
Fetch tool weaves the web through it all,
Query hooks spring forth in nested array,
Cache keys dance in systematic way,
Frontend dreams now wake to code so bright!

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ssdeanx ssdeanx merged commit b636aa9 into main Mar 6, 2026
14 of 20 checks passed
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a powerful new web fetching and search tool, significantly enhancing the application's ability to acquire and process external data. Complementing this, new user interfaces have been developed for managing Mastra Control Plane servers, Agent-to-Agent interactions, and workspace files, providing users with improved visibility and control over their agent environments and associated data.

Highlights

  • New Fetch Tool: Introduced a new fetch tool (src/mastra/tools/fetch.tool.ts) for robust web content fetching, search across multiple providers (DuckDuckGo, Google, Bing, Google News RSS), HTML sanitization, and conversion to Markdown, including content windowing and URL filtering.
  • New UI Pages: Created two new React pages: app/chat/mcp-a2a/page.tsx for managing Mastra Control Plane (MCP) servers and Agent-to-Agent (A2A) agent cards, and app/chat/workspaces/page.tsx for workspace file browsing, content viewing, and skill display.
  • Expanded useMastraQuery Hook: Significantly extended the useMastraQuery hook (lib/hooks/use-mastra-query.ts) with a comprehensive set of new queries and mutations for workspaces, sandboxes, MCP servers/tools, and A2A agents, along with granular query keys for improved caching.
  • UI Navigation Updates: Updated the main sidebar (app/chat/components/main-sidebar.tsx) to include new navigation links for the 'Workspaces' and 'MCP / A2A' pages.
  • Copilot Instructions Update: Revised Copilot instructions (.github/copilot-instructions.md) to include new guidelines for page/component editing, error checking, and internet research best practices.
Changelog
  • .github/copilot-instructions.md
    • Added new guidelines for editing pages/components, including VS Code interaction error checks.
    • Included instructions for internal error-tool enable flow.
    • Provided guidance on using internet research tools for UI page edits.
    • Specified not to run project-wide type checks/lint commands by default for page edits.
  • app/chat/components/main-sidebar.tsx
    • Imported FolderTreeIcon and NetworkIcon from lucide-react.
    • Added new SidebarMenuItem components for 'Workspaces' and 'MCP / A2A' with corresponding navigation.
    • Updated conditional rendering logic for agent provider and model ID display.
    • Modified agent description rendering to remove redundant italic class.
  • app/chat/mcp-a2a/page.tsx
    • Added a new React page to display and manage MCP servers, their tools, and A2A agent cards.
    • Implemented state management for selecting active MCP servers and A2A agents.
    • Utilized useMastraQuery hooks to fetch MCP server lists, server tools, agents, and A2A agent card details.
  • app/chat/workspaces/page.tsx
    • Added a new React page for workspace management, featuring a file tree, code block for file content, and a list of workspace skills.
    • Implemented state management for selecting active workspaces and files.
    • Utilized useMastraQuery hooks to fetch workspaces, sandbox files, read file content, and list workspace skills.
    • Included utility functions for determining file language and splitting file nodes into folders and plain files.
  • lib/AGENTS.md
    • Documented a recent update (2026-03-05) detailing the expansion of hooks/use-mastra-query.ts with workspace/sandbox UI hooks.
    • Listed new queries and mutations related to workspace APIs.
    • Noted the addition of granular workspace query keys for frontend cache invalidation.
  • lib/hooks/use-mastra-query.ts
    • Imported numerous new types for ListSkillsResponse, McpServerListResponse, McpServerToolListResponse, SearchSkillsParams, SearchSkillsResponse, WorkspaceFs*Response, WorkspaceIndexParams, WorkspaceIndexResponse, WorkspaceInfoResponse, WorkspaceSearchParams, WorkspaceSearchResponse, AgentCard, GetTaskResponse, MessageSendParams, SendMessageResponse, Task, TaskQueryParams, and ServerDetailInfo.
    • Added a comprehensive set of new query hooks: useWorkspaces, useWorkspace, useWorkspaceInfo, useWorkspaceFiles, useWorkspaceReadFile, useWorkspaceStat, useWorkspaceSearch, useWorkspaceSkills, useWorkspaceSearchSkills, useSandboxInfo, useSandboxFiles, useSandboxReadFile, useSandboxStat, useSandboxSearch, useMcpServers, useMcpServerDetails, useMcpServerTools, useMcpToolDetails, useA2ACard, useA2AGetTask.
    • Added a comprehensive set of new mutation hooks: useWorkspaceWriteFileMutation, useWorkspaceDeleteMutation, useWorkspaceMkdirMutation, useWorkspaceIndexMutation, useSandboxWriteFileMutation, useSandboxDeleteMutation, useSandboxMkdirMutation, useSandboxIndexMutation, useMcpToolExecuteMutation, useA2ASendMessageMutation, useA2ACancelTaskMutation.
    • Extended mastraQueryKeys with new keys for workspaces, sandbox, mcp, and a2a to enable granular cache invalidation.
    • Included the newly added query and mutation hooks in the returned useMastraQuery object.
  • memory-bank/activeContext.md
    • Added an "Active Context Update" section (2026-03-05) detailing the expansion of use-mastra-query.ts with workspace/sandbox hooks and granular query keys.
    • Added another "Active Context Update" section (2026-03-05) describing the cleanup of use-mastra-query.ts errors, the separation of sandbox hooks, and the inclusion of MCP/A2A hooks.
  • memory-bank/progress.md
    • Added a "2026-03-05 use-mastra-query error cleanup" section detailing fixes, removal of useWorkspaceSandboxReady, addition of separate sandbox hooks, and wiring of MCP/A2A hooks.
    • Added a "2026-03-05 Workspace/Sandbox Hooks for Frontend UI" section outlining the review of Mastra Workspace docs, extension of use-mastra-query.ts with workspace filesystem/search/skills hooks, and new mastraQueryKeys.
  • src/components/ai-elements/tools/types.ts
    • Imported fetchTool and exported its FetchUITool type.
  • src/mastra/agents/researchAgent.ts
    • Imported fetchTool.
    • Updated the "Tool Selection Guide" comment to prefer fetchTool for reliable URL fetching and search to markdown.
    • Added fetchTool to the list of available tools.
  • src/mastra/tools/fetch.tool.ts
    • Added a new fetchTool for web content fetching and search.
    • Implemented URL validation, HTML sanitization, and HTML to Markdown conversion.
    • Included search functionalities for DuckDuckGo, Google, Bing, and Google News RSS.
    • Provided options for content windowing (head, tail, head-tail truncation) and URL pattern filtering using RE2 regex.
    • Defined input and output schemas using zod and integrated observability tracing.
  • src/mastra/tools/index.ts
    • Exported the newly created fetch.tool.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 6, 2026

🤖 I'm sorry @ssdeanx, but I was unable to process your request. Please see the logs for more details.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The workspace vs sandbox hooks in use-mastra-query.ts are almost identical; consider extracting a shared factory/helper to avoid duplicated query/mutation logic and keep future changes in sync.
  • Both app/chat/workspaces/page.tsx and app/chat/mcp-a2a/page.tsx assume non-empty server/workspace/agent lists; it would be safer to gate the selects and dependent hooks on loading/error/empty states instead of defaulting to the first element or an empty string.
  • In WorkspacesPage, the filesystem payload is treated as unknown and then heuristically mapped via entries/items/files; wiring this to the concrete WorkspaceFsListResponse type (or a dedicated transformer) would make the file-tree mapping more robust to backend shape changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The workspace vs sandbox hooks in `use-mastra-query.ts` are almost identical; consider extracting a shared factory/helper to avoid duplicated query/mutation logic and keep future changes in sync.
- Both `app/chat/workspaces/page.tsx` and `app/chat/mcp-a2a/page.tsx` assume non-empty server/workspace/agent lists; it would be safer to gate the selects and dependent hooks on loading/error/empty states instead of defaulting to the first element or an empty string.
- In `WorkspacesPage`, the filesystem payload is treated as `unknown` and then heuristically mapped via `entries/items/files`; wiring this to the concrete `WorkspaceFsListResponse` type (or a dedicated transformer) would make the file-tree mapping more robust to backend shape changes.

## Individual Comments

### Comment 1
<location path="app/chat/components/main-sidebar.tsx" line_range="309-314" />
<code_context>
+                                                                        {!(!(agent.provider ?? agent.modelId)) && (
</code_context>
<issue_to_address>
**suggestion:** Simplify boolean checks for provider/modelId to improve readability.

`!(!(agent.provider ?? agent.modelId))` and `Boolean(agent.provider)` are much less readable than `!!(agent.provider || agent.modelId)` and `agent.provider && ...`. Please revert to the more idiomatic forms to keep the logic easy to scan and maintain.

Suggested implementation:

```typescript
                                                                        {!!(agent.provider || agent.modelId) && (

```

```typescript
                                                                                        {agent.provider && `${agent.provider} • `}

```
</issue_to_address>

### Comment 2
<location path="src/mastra/tools/fetch.tool.ts" line_range="179-188" />
<code_context>
+    mode: 'head-tail',
+}
+
+function compileRe2Patterns(patterns?: string[]) {
+    const compiled: Array<InstanceType<typeof RE2Ctor>> = []
+    for (const pattern of patterns ?? []) {
+        try {
+            if (typeof pattern === 'string' && pattern.trim().length > 0) {
+                compiled.push(new RE2Ctor(pattern))
+            }
+        } catch (error) {
+            log.warn('Invalid RE2 pattern ignored', {
+                pattern,
+                error: error instanceof Error ? error.message : String(error),
+            })
+        }
+    }
+    return compiled
+}
+
</code_context>
<issue_to_address>
**suggestion:** Surface invalid RE2 patterns back into tool output or tracing to aid debugging.

Currently invalid RE2 patterns are only logged with `log.warn` and then ignored, which makes it hard for callers to understand why their include/exclude filters are not taking effect. Please also surface invalid pattern details in span metadata or in `providerDiagnostics`/output metadata so consumers and monitoring can detect misconfiguration without relying solely on logs.

Suggested implementation:

```typescript
type Re2PatternDiagnostic = {
    pattern: string
    error: string
}

function compileRe2Patterns(
    patterns?: string[]
): {
    compiled: Array<InstanceType<typeof RE2Ctor>>
    invalidPatterns: Re2PatternDiagnostic[]
} {
    const compiled: Array<InstanceType<typeof RE2Ctor>> = []
    const invalidPatterns: Re2PatternDiagnostic[] = []

    for (const pattern of patterns ?? []) {
        try {
            if (typeof pattern === 'string' && pattern.trim().length > 0) {
                compiled.push(new RE2Ctor(pattern))
            }
        } catch (error) {
            const message = error instanceof Error ? error.message : String(error)

            log.warn('Invalid RE2 pattern ignored', {
                pattern,
                error: message,
            })

            invalidPatterns.push({
                pattern,
                error: message,
            })
        }
    }

    return { compiled, invalidPatterns }
}

```

To fully surface invalid RE2 patterns as requested, you should also:

1. Update all call sites of `compileRe2Patterns` to handle the new return type:
   - Replace usages like:
     - `const includePatterns = compileRe2Patterns(config.includePatterns)`
     with:
     - `const { compiled: includePatterns, invalidPatterns: includePatternErrors } = compileRe2Patterns(config.includePatterns)`
2. Propagate `invalidPatterns` into:
   - Span metadata (e.g., `span.setAttribute('fetch.invalidRe2Patterns', invalidPatterns)` or similar, depending on your tracing abstraction).
   - Tool/provider diagnostics or output metadata (e.g., append to a `providerDiagnostics` array or include in a `debug`/`meta` field returned by the tool).
3. If you have a shared diagnostics type, align `Re2PatternDiagnostic` with it or remove the local type and reuse the shared one.
4. Consider aggregating invalid patterns from multiple call sites into a single diagnostics payload associated with the tool invocation, so consumers can easily see all misconfigurations for a given request.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +309 to +314
{!(!(agent.provider ?? agent.modelId)) && (
<div className="mt-1.5 flex items-center gap-2">
<div className="flex h-5 items-center rounded-md border border-primary/20 bg-primary/10 px-1.5 text-[9px] font-bold uppercase tracking-wider text-primary">
<CpuIcon className="mr-1 size-3" />
<span>
{agent.provider && `${agent.provider} • `}
{(Boolean(agent.provider)) && `${agent.provider} • `}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Simplify boolean checks for provider/modelId to improve readability.

!(!(agent.provider ?? agent.modelId)) and Boolean(agent.provider) are much less readable than !!(agent.provider || agent.modelId) and agent.provider && .... Please revert to the more idiomatic forms to keep the logic easy to scan and maintain.

Suggested implementation:

                                                                        {!!(agent.provider || agent.modelId) && (
                                                                                        {agent.provider && `${agent.provider} • `}

Comment on lines +179 to +188
function compileRe2Patterns(patterns?: string[]) {
const compiled: Array<InstanceType<typeof RE2Ctor>> = []
for (const pattern of patterns ?? []) {
try {
if (typeof pattern === 'string' && pattern.trim().length > 0) {
compiled.push(new RE2Ctor(pattern))
}
} catch (error) {
log.warn('Invalid RE2 pattern ignored', {
pattern,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Surface invalid RE2 patterns back into tool output or tracing to aid debugging.

Currently invalid RE2 patterns are only logged with log.warn and then ignored, which makes it hard for callers to understand why their include/exclude filters are not taking effect. Please also surface invalid pattern details in span metadata or in providerDiagnostics/output metadata so consumers and monitoring can detect misconfiguration without relying solely on logs.

Suggested implementation:

type Re2PatternDiagnostic = {
    pattern: string
    error: string
}

function compileRe2Patterns(
    patterns?: string[]
): {
    compiled: Array<InstanceType<typeof RE2Ctor>>
    invalidPatterns: Re2PatternDiagnostic[]
} {
    const compiled: Array<InstanceType<typeof RE2Ctor>> = []
    const invalidPatterns: Re2PatternDiagnostic[] = []

    for (const pattern of patterns ?? []) {
        try {
            if (typeof pattern === 'string' && pattern.trim().length > 0) {
                compiled.push(new RE2Ctor(pattern))
            }
        } catch (error) {
            const message = error instanceof Error ? error.message : String(error)

            log.warn('Invalid RE2 pattern ignored', {
                pattern,
                error: message,
            })

            invalidPatterns.push({
                pattern,
                error: message,
            })
        }
    }

    return { compiled, invalidPatterns }
}

To fully surface invalid RE2 patterns as requested, you should also:

  1. Update all call sites of compileRe2Patterns to handle the new return type:
    • Replace usages like:
      • const includePatterns = compileRe2Patterns(config.includePatterns)
        with:
      • const { compiled: includePatterns, invalidPatterns: includePatternErrors } = compileRe2Patterns(config.includePatterns)
  2. Propagate invalidPatterns into:
    • Span metadata (e.g., span.setAttribute('fetch.invalidRe2Patterns', invalidPatterns) or similar, depending on your tracing abstraction).
    • Tool/provider diagnostics or output metadata (e.g., append to a providerDiagnostics array or include in a debug/meta field returned by the tool).
  3. If you have a shared diagnostics type, align Re2PatternDiagnostic with it or remove the local type and reuse the shared one.
  4. Consider aggregating invalid patterns from multiple call sites into a single diagnostics payload associated with the tool invocation, so consumers can easily see all misconfigurations for a given request.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a versatile fetch/search tool and accompanying UI pages for workspace and MCP/A2A management, featuring a new fetch.tool.ts with multi-provider search capabilities and new pages in app/chat/. While the new functionality is impressive, the fetch tool lacks critical security controls, making it vulnerable to Server-Side Request Forgery (SSRF) and Denial of Service (DoS) attacks due to unvalidated URL inputs and unrestricted response sizes. These security issues must be addressed before production deployment. Furthermore, improvements are needed regarding code duplication in the new React Query hooks and the fragility of the web scraping implementation in the fetch tool to enhance long-term maintainability and robustness.

Comment on lines +817 to +822
const page = await fetchPageAsMarkdown({
url: inputData.url,
timeout,
userAgent,
contentWindow,
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The fetch tool accepts a user-provided URL and fetches its content without validating the destination. This allows an attacker to perform Server-Side Request Forgery (SSRF) by pointing the tool to internal services, loopback addresses, or cloud metadata endpoints (e.g., http://169.254.169.254/latest/meta-data/).

To remediate this, implement a validation step that blocks internal IP ranges and sensitive hostnames. You should also ensure that only http and https protocols are allowed.

Comment on lines +895 to +1103
const useWorkspaceInfo = (id: string) =>
useQuery<WorkspaceInfoResponse, Error>({
queryKey: mastraQueryKeys.workspaces.info(id),
queryFn: () => mastraClient.getWorkspace(id).info(),
enabled: !!id,
})

const useWorkspaceFiles = (
workspaceId: string,
path = '/',
recursive = false
) =>
useQuery<WorkspaceFsListResponse, Error>({
queryKey: mastraQueryKeys.workspaces.files(workspaceId, path, recursive),
queryFn: () => mastraClient.getWorkspace(workspaceId).listFiles(path, recursive),
enabled: !!workspaceId,
})

const useWorkspaceReadFile = (
workspaceId: string,
path: string,
encoding = 'utf-8'
) =>
useQuery<WorkspaceFsReadResponse, Error>({
queryKey: mastraQueryKeys.workspaces.file(workspaceId, path, encoding),
queryFn: () => mastraClient.getWorkspace(workspaceId).readFile(path, encoding),
enabled: !!workspaceId && !!path,
})

const useWorkspaceStat = (workspaceId: string, path: string) =>
useQuery<WorkspaceFsStatResponse, Error>({
queryKey: mastraQueryKeys.workspaces.stat(workspaceId, path),
queryFn: () => mastraClient.getWorkspace(workspaceId).stat(path),
enabled: !!workspaceId && !!path,
})

const useWorkspaceSearch = (workspaceId: string, params: WorkspaceSearchParams) =>
useQuery<WorkspaceSearchResponse, Error>({
queryKey: mastraQueryKeys.workspaces.search(workspaceId, params),
queryFn: () => mastraClient.getWorkspace(workspaceId).search(params),
enabled: !!workspaceId && !!params?.query,
})

const useWorkspaceSkills = (workspaceId: string) =>
useQuery<ListSkillsResponse, Error>({
queryKey: mastraQueryKeys.workspaces.skills(workspaceId),
queryFn: () => mastraClient.getWorkspace(workspaceId).listSkills(),
enabled: !!workspaceId,
})

const useWorkspaceSearchSkills = (
workspaceId: string,
params: SearchSkillsParams
) =>
useQuery<SearchSkillsResponse, Error>({
queryKey: mastraQueryKeys.workspaces.searchSkills(workspaceId, params),
queryFn: () => mastraClient.getWorkspace(workspaceId).searchSkills(params),
enabled: !!workspaceId,
})

// Workspace Mutations
const useWorkspaceWriteFileMutation = (workspaceId: string) =>
useMutation({
mutationFn: (params: {
path: string
content: string
options?: { encoding?: 'utf-8' | 'base64'; recursive?: boolean }
}) =>
mastraClient
.getWorkspace(workspaceId)
.writeFile(params.path, params.content, params.options),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: mastraQueryKeys.workspaces.all,
})
},
})

const useWorkspaceDeleteMutation = (workspaceId: string) =>
useMutation({
mutationFn: (params: {
path: string
options?: { recursive?: boolean; force?: boolean }
}) =>
mastraClient
.getWorkspace(workspaceId)
.delete(params.path, params.options),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: mastraQueryKeys.workspaces.all,
})
},
})

const useWorkspaceMkdirMutation = (workspaceId: string) =>
useMutation({
mutationFn: (params: { path: string; recursive?: boolean }) =>
mastraClient
.getWorkspace(workspaceId)
.mkdir(params.path, params.recursive),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: mastraQueryKeys.workspaces.all,
})
},
})

const useWorkspaceIndexMutation = (workspaceId: string) =>
useMutation({
mutationFn: (params: WorkspaceIndexParams) =>
mastraClient.getWorkspace(workspaceId).index(params),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: mastraQueryKeys.workspaces.all,
})
},
})

// --- SANDBOX (separate frontend hooks) ---

const useSandboxInfo = (workspaceId: string) =>
useQuery<WorkspaceInfoResponse, Error>({
queryKey: mastraQueryKeys.sandbox.info(workspaceId),
queryFn: () => mastraClient.getWorkspace(workspaceId).info(),
enabled: !!workspaceId,
})

const useSandboxFiles = (
workspaceId: string,
path = '/',
recursive = false
) =>
useQuery<WorkspaceFsListResponse, Error>({
queryKey: mastraQueryKeys.sandbox.files(workspaceId, path, recursive),
queryFn: () => mastraClient.getWorkspace(workspaceId).listFiles(path, recursive),
enabled: !!workspaceId,
})

const useSandboxReadFile = (
workspaceId: string,
path: string,
encoding = 'utf-8'
) =>
useQuery<WorkspaceFsReadResponse, Error>({
queryKey: mastraQueryKeys.sandbox.file(workspaceId, path, encoding),
queryFn: () => mastraClient.getWorkspace(workspaceId).readFile(path, encoding),
enabled: !!workspaceId && !!path,
})

const useSandboxStat = (workspaceId: string, path: string) =>
useQuery<WorkspaceFsStatResponse, Error>({
queryKey: mastraQueryKeys.sandbox.stat(workspaceId, path),
queryFn: () => mastraClient.getWorkspace(workspaceId).stat(path),
enabled: !!workspaceId && !!path,
})

const useSandboxSearch = (workspaceId: string, params: WorkspaceSearchParams) =>
useQuery<WorkspaceSearchResponse, Error>({
queryKey: mastraQueryKeys.sandbox.search(workspaceId, params),
queryFn: () => mastraClient.getWorkspace(workspaceId).search(params),
enabled: !!workspaceId && !!params?.query,
})

const useSandboxWriteFileMutation = (workspaceId: string) =>
useMutation({
mutationFn: (params: {
path: string
content: string
options?: { encoding?: 'utf-8' | 'base64'; recursive?: boolean }
}) =>
mastraClient
.getWorkspace(workspaceId)
.writeFile(params.path, params.content, params.options),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: mastraQueryKeys.sandbox.all })
},
})

const useSandboxDeleteMutation = (workspaceId: string) =>
useMutation({
mutationFn: (params: {
path: string
options?: { recursive?: boolean; force?: boolean }
}) =>
mastraClient
.getWorkspace(workspaceId)
.delete(params.path, params.options),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: mastraQueryKeys.sandbox.all })
},
})

const useSandboxMkdirMutation = (workspaceId: string) =>
useMutation({
mutationFn: (params: { path: string; recursive?: boolean }) =>
mastraClient.getWorkspace(workspaceId).mkdir(params.path, params.recursive),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: mastraQueryKeys.sandbox.all })
},
})

const useSandboxIndexMutation = (workspaceId: string) =>
useMutation({
mutationFn: (params: WorkspaceIndexParams) =>
mastraClient.getWorkspace(workspaceId).index(params),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: mastraQueryKeys.sandbox.all })
},
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There's significant code duplication between the useWorkspace... hooks and the useSandbox... hooks. The implementations are identical, only differing in the query key used (mastraQueryKeys.workspaces vs mastraQueryKeys.sandbox). This makes the code harder to maintain, as any change needs to be applied in two places.

Consider refactoring this by creating a factory function that generates these hooks, taking the query key group as an argument. This would eliminate the duplication and improve maintainability.

Example:

const createWorkspaceHooks = (keyGroup: typeof mastraQueryKeys.workspaces | typeof mastraQueryKeys.sandbox) => {
  const useInfo = (id: string) =>
    useQuery<WorkspaceInfoResponse, Error>({
      queryKey: keyGroup.info(id),
      queryFn: () => mastraClient.getWorkspace(id).info(),
      enabled: !!id,
    });

  // ... other hooks for files, readFile, etc.

  return { useInfo, ... };
}

// Then you could use it like this:
const { useWorkspaceInfo, ... } = createWorkspaceHooks(mastraQueryKeys.workspaces);
const { useSandboxInfo, ... } = createWorkspaceHooks(mastraQueryKeys.sandbox);

Comment on lines +316 to +416
function extractDuckDuckGoResults(html: string): SearchResult[] {
const $ = cheerio.load(html)
const out: SearchResult[] = []

$('a.result__a').each((_i, el) => {
const anchor = $(el)
const title = anchor.text().trim()
const href = anchor.attr('href') ?? ''
if (href.trim().length === 0) {
return
}

let resolvedUrl = href
try {
const urlObj = new URL(href, 'https://duckduckgo.com')
const uddg = urlObj.searchParams.get('uddg')
resolvedUrl =
typeof uddg === 'string' && uddg.trim().length > 0
? decodeURIComponent(uddg)
: urlObj.href
} catch {
// Keep original href
}

const snippet =
anchor.closest('.result').find('.result__snippet').text().trim() ||
undefined

if (ValidationUtils.validateUrl(resolvedUrl)) {
out.push({ title, url: resolvedUrl, snippet })
}
})

return out
}

function extractGoogleResults(html: string): SearchResult[] {
const $ = cheerio.load(html)
const out: SearchResult[] = []

$('a[href^="/url?q="]').each((_i, el) => {
const anchor = $(el)
const href = anchor.attr('href') ?? ''
const title = anchor.text().trim()
if (href.trim().length === 0) {
return
}

try {
const parsed = new URL(`https://www.google.com${href}`)
const target = parsed.searchParams.get('q') ?? ''
if (!ValidationUtils.validateUrl(target)) {
return
}

const snippet =
anchor
.closest('div')
.parent()
.find('span,div')
.first()
.text()
.trim() || undefined

out.push({
title: title.length > 0 ? title : target,
url: target,
snippet,
})
} catch {
// Skip malformed results
}
})

return out
}

function extractBingResults(html: string): SearchResult[] {
const $ = cheerio.load(html)
const out: SearchResult[] = []

$('li.b_algo').each((_i, el) => {
const node = $(el)
const linkEl = node.find('h2 a').first()
const url = linkEl.attr('href') ?? ''
if (!ValidationUtils.validateUrl(url)) {
return
}

const title = linkEl.text().trim()
const snippet = node.find('p').first().text().trim() || undefined

out.push({
title: title.length > 0 ? title : url,
url,
snippet,
})
})

return out
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The functions for extracting search results (extractDuckDuckGoResults, extractGoogleResults, extractBingResults) rely on scraping HTML content using cheerio selectors. This approach is very brittle and prone to breaking whenever the search engines update their website's HTML structure, which can happen frequently and without notice. This could cause the tool to fail or return incorrect data.

For a more robust solution, consider using official search APIs if available. If scraping is the only option, it would be good to add prominent comments warning about the fragility of this implementation and potentially add more robust error handling or monitoring to detect when scraping fails.

Comment on lines +602 to +618
const response = await httpFetch(options.url, {
method: 'GET',
timeout: options.timeout,
responseType: 'text',
headers,
})

if (!response.ok) {
throw new FetchToolError(
`HTTP ${response.status}: ${response.statusText}`,
'HTTP_ERROR',
response.status,
options.url
)
}

const html = await response.text()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The tool does not limit the size of the HTTP response it fetches. An attacker can provide a URL that returns a very large response, causing the server to exhaust its memory when reading the body into a string and parsing it with JSDOM. This can lead to a Denial of Service (DoS) by crashing the server process.

To remediate this, set a maximum response size limit (e.g., 5MB) in the httpFetch call using the maxContentLength option or by checking the Content-Length header before processing the body.

- 2) Run `get_errors` on the exact files being edited (not project-wide).
- 3) Fix reported issues.
- 4) Run `get_errors` again on those same files to verify clean state.
- 🌐 When unsure about framework/API behavior while editing UI pages, use internet research tools first (`#web`, `#websearch`, or `fetch_webpage`) and then apply fixes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The instruction mentions fetch_webpage, but the new tool added in this PR is fetchTool. To avoid confusion and ensure the instructions are accurate, it would be better to use the actual tool ID.

Suggested change
- 🌐 When unsure about framework/API behavior while editing UI pages, use internet research tools first (`#web`, `#websearch`, or `fetch_webpage`) and then apply fixes.
- 🌐 When unsure about framework/API behavior while editing UI pages, use internet research tools first (`#web`, `#websearch`, or `fetchTool`) and then apply fixes.

<CpuIcon className="mr-1 size-3" />
<span>
{agent.provider && `${agent.provider} • `}
{(Boolean(agent.provider)) && `${agent.provider} • `}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The (Boolean(agent.provider)) check is verbose. A simple truthiness check with agent.provider is sufficient and more idiomatic in this context.

Suggested change
{(Boolean(agent.provider)) && `${agent.provider} • `}
{agent.provider && `${agent.provider} • `}

{/* Description section */}
<div className="p-4 pt-3">
{agent.description ? (
{(agent.description) ? (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The parentheses around agent.description in this ternary operator are unnecessary and can be removed for cleaner code.

Suggested change
{(agent.description) ? (
{agent.description ? (

<li className="text-sm text-muted-foreground">No skills found.</li>
) : (
skills.map((skill, idx) => (
<li key={`${skill.name ?? 'skill'}-${idx}`} className="rounded-md border p-2">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using the list index idx as part of a React key is not ideal, as it can lead to issues if the list is re-ordered. The skill object likely has a unique id property. Using skill.id would provide a stable identity for each item, which is the recommended practice.

Suggested change
<li key={`${skill.name ?? 'skill'}-${idx}`} className="rounded-md border p-2">
<li key={skill.id ?? `${skill.name ?? 'skill'}-${idx}`} className="rounded-md border p-2">

}

function sanitizeHtml(html: string): string {
const dom = new JSDOM(String(html), {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The String(html) conversion is redundant because the html parameter is already typed as a string. This also applies to String(markdown) in the applyContentWindow function on line 252. These can be safely removed.

Suggested change
const dom = new JSDOM(String(html), {
const dom = new JSDOM(html, {

Comment on lines +329 to +338
try {
const urlObj = new URL(href, 'https://duckduckgo.com')
const uddg = urlObj.searchParams.get('uddg')
resolvedUrl =
typeof uddg === 'string' && uddg.trim().length > 0
? decodeURIComponent(uddg)
: urlObj.href
} catch {
// Keep original href
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The try...catch block here is empty, which silently swallows any errors that might occur during URL parsing or decoding, making debugging difficult. The same issue exists in extractGoogleResults on line 364. It's better to at least log the error for easier troubleshooting.

        } catch (error) {
            log.warn('Failed to resolve DuckDuckGo URL', { href, error: error instanceof Error ? error.message : String(error) });
            // Keep original href
        }

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Mastra fetchTool for URL fetching + web/news search with markdown conversion, and introduces new chat UI pages for MCP/A2A and workspace browsing that consume the expanded Mastra client hooks.

Changes:

  • Added src/mastra/tools/fetch.tool.ts and exported it via the tools index; wired it into researchAgent.
  • Introduced new chat pages: /chat/workspaces (workspace files/skills) and /chat/mcp-a2a (MCP servers/tools + A2A card).
  • Extended frontend Mastra query hooks/query keys for workspaces/sandbox and MCP/A2A consumption (plus documentation/memory-bank updates).

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/mastra/tools/index.ts Exports the new fetchTool from the tools barrel.
src/mastra/tools/fetch.tool.ts Implements fetch/search + HTML sanitization + markdown conversion + RE2 filtering.
src/mastra/agents/researchAgent.ts Adds fetchTool to the agent toolset and updates tool-selection guidance.
src/components/ai-elements/tools/types.ts Adds FetchUITool type for UI tool rendering.
lib/hooks/use-mastra-query.ts Adds workspace/sandbox hooks + MCP/A2A hooks and query key structure.
app/chat/workspaces/page.tsx New workspace browser page (file tree + file viewer + skills list).
app/chat/mcp-a2a/page.tsx New MCP/A2A page for browsing MCP servers/tools and viewing A2A agent cards.
app/chat/components/main-sidebar.tsx Adds navigation links to the new pages and tweaks agent card rendering logic.
memory-bank/progress.md Documents recent hook cleanup/expansion work.
memory-bank/activeContext.md Records current context around workspace/sandbox + MCP/A2A hook integration.
lib/AGENTS.md Updates lib directory documentation to reflect new hooks.
.github/copilot-instructions.md Updates Copilot instructions around targeted error checking for page edits.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +53 to +57
return parsed.protocol === 'http:' || parsed.protocol === 'https:'
} catch {
return false
}
}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ValidationUtils.validateUrl() only checks the protocol, which still allows fetching localhost/private-network URLs (e.g. http://127.0.0.1, http://10.x.x.x) and enables SSRF against internal services/metadata endpoints. Please add host-level restrictions (at least block localhost + RFC1918 + link-local + IPv6 loopback) and/or support an allowlist similar to web-scraper-tool’s WEB_SCRAPER_ALLOWED_DOMAINS gating before calling httpFetch().

Suggested change
return parsed.protocol === 'http:' || parsed.protocol === 'https:'
} catch {
return false
}
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return false
}
const hostname = parsed.hostname
if (!hostname || this.isPrivateHostname(hostname)) {
return false
}
return true
} catch {
return false
}
}
private static isPrivateHostname(hostname: string): boolean {
const lower = hostname.toLowerCase()
// Block localhost and subdomains like foo.localhost
if (lower === 'localhost' || lower.endsWith('.localhost')) {
return true
}
// Block IPv6 loopback
if (lower === '::1' || lower === '0:0:0:0:0:0:0:1') {
return true
}
// Detect simple IPv4 literals
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/
if (ipv4Pattern.test(hostname)) {
const parts = hostname.split('.').map((part) => Number(part))
// Reject obviously invalid octets as unsafe
if (parts.some((octet) => Number.isNaN(octet) || octet < 0 || octet > 255)) {
return true
}
const [a, b] = parts
// 127.0.0.0/8 – loopback
if (a === 127) {
return true
}
// 10.0.0.0/8 – RFC1918 private
if (a === 10) {
return true
}
// 172.16.0.0/12 – RFC1918 private
if (a === 172 && b >= 16 && b <= 31) {
return true
}
// 192.168.0.0/16 – RFC1918 private
if (a === 192 && b === 168) {
return true
}
// 169.254.0.0/16 – link-local
if (a === 169 && b === 254) {
return true
}
}
return false
}

Copilot uses AI. Check for mistakes.
Comment on lines +266 to +279
return {
markdown: `${sliced}\n\n---\n_Truncated by content window (head mode)_`,
originalChars,
outputChars: sliced.length,
truncated: true,
}
}

if (window.mode === 'tail') {
const sliced = source.slice(-window.maxChars).trim()
return {
markdown: `_Truncated by content window (tail mode)_\n---\n\n${sliced}`,
originalChars,
outputChars: sliced.length,
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applyContentWindow() reports outputChars as sliced.length in head/tail modes, but the returned markdown includes additional truncation banner text. This makes the metadata inconsistent with the actual output. Update outputChars to reflect the final markdown length (including the truncation notice) or rename the field if it’s meant to measure only the excerpt length.

Suggested change
return {
markdown: `${sliced}\n\n---\n_Truncated by content window (head mode)_`,
originalChars,
outputChars: sliced.length,
truncated: true,
}
}
if (window.mode === 'tail') {
const sliced = source.slice(-window.maxChars).trim()
return {
markdown: `_Truncated by content window (tail mode)_\n---\n\n${sliced}`,
originalChars,
outputChars: sliced.length,
const markdown = `${sliced}\n\n---\n_Truncated by content window (head mode)_`
return {
markdown,
originalChars,
outputChars: markdown.length,
truncated: true,
}
}
if (window.mode === 'tail') {
const sliced = source.slice(-window.maxChars).trim()
const markdown = `_Truncated by content window (tail mode)_\n---\n\n${sliced}`
return {
markdown,
originalChars,
outputChars: markdown.length,

Copilot uses AI. Check for mistakes.
Comment on lines +627 to +636
searchProvider: z
.enum(['duckduckgo', 'google', 'bing', 'all'])
.optional()
.describe('Search backend. No fallback is applied.'),
searchVertical: z
.enum(['web', 'news', 'auto'])
.optional()
.describe(
'Search vertical. auto detects news-like queries and adds Google News RSS for reliability.'
),
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The input schema describes searchProvider as “No fallback is applied”, but the implementation does fallback/aggregation (e.g. provider all runs multiple providers, errors are caught per-provider, and searchVertical=auto/news adds Google News RSS regardless). Please update the .describe(...) text (and potentially the tool description) to match actual behavior so tool callers aren’t misled.

Copilot uses AI. Check for mistakes.
Comment on lines +731 to +737
export const fetchTool = createTool({
id: 'fetch',
description:
'Production fetch/search tool with RE2 filtering and markdown output. No fallback, no file writes.',
inputSchema: fetchToolInputSchema,
outputSchema: fetchToolOutputSchema,
onInputStart: ({ toolCallId, messages, abortSignal }) => {
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR introduces a new production-critical tool (fetchTool) with substantial parsing/search logic, but there are no accompanying tests under src/mastra/tools/tests/ (the repo has extensive tool test coverage already). Please add unit tests that mock httpFetch() to cover: direct URL fetch, search aggregation/dedupe/filtering, content window truncation modes, and provider error handling.

Copilot uses AI. Check for mistakes.
Comment on lines +170 to +175
{folders.map((folder) => (
<FileTreeFolder key={folder.path} path={folder.path} name={folder.name} />
))}
{plainFiles.map((file) => (
<FileTreeFile key={file.path} path={file.path} name={file.name} />
))}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FileTree is rendered with all FileTreeFolder and FileTreeFile nodes as flat siblings, but FileTreeFolder expects nested children to display hierarchy. As-is, folders will expand to empty content and nested files won’t appear under their parents. Consider building a nested tree structure from the returned paths and rendering folders with their child folders/files.

Suggested change
{folders.map((folder) => (
<FileTreeFolder key={folder.path} path={folder.path} name={folder.name} />
))}
{plainFiles.map((file) => (
<FileTreeFile key={file.path} path={file.path} name={file.name} />
))}
{(() => {
type FolderInfo = {
folder: WorkspaceFileNode
childFolderPaths: string[]
files: WorkspaceFileNode[]
}
const folderMap = new Map<string, FolderInfo>()
// Initialize map with all folders
for (const folder of folders) {
folderMap.set(folder.path, {
folder,
childFolderPaths: [],
files: [],
})
}
const rootFolderPaths: string[] = []
const rootFiles: WorkspaceFileNode[] = []
const getParentPath = (path: string): string | null => {
const lastSlashIndex = path.lastIndexOf('/')
if (lastSlashIndex === -1) {
return null
}
return path.slice(0, lastSlashIndex)
}
// Link folders to their parents
for (const folder of folders) {
const parentPath = getParentPath(folder.path)
if (parentPath && folderMap.has(parentPath)) {
const parentInfo = folderMap.get(parentPath)!
parentInfo.childFolderPaths.push(folder.path)
} else {
rootFolderPaths.push(folder.path)
}
}
// Assign files to their parent folders or root
for (const file of plainFiles) {
const parentPath = getParentPath(file.path)
if (parentPath && folderMap.has(parentPath)) {
const parentInfo = folderMap.get(parentPath)!
parentInfo.files.push(file)
} else {
rootFiles.push(file)
}
}
const renderFolder = (folderPath: string): JSX.Element | null => {
const info = folderMap.get(folderPath)
if (!info) return null
return (
<FileTreeFolder
key={info.folder.path}
path={info.folder.path}
name={info.folder.name}
>
{info.childFolderPaths.map((childPath) => renderFolder(childPath))}
{info.files.map((file) => (
<FileTreeFile
key={file.path}
path={file.path}
name={file.name}
/>
))}
</FileTreeFolder>
)
}
return (
<>
{rootFolderPaths.map((folderPath) => renderFolder(folderPath))}
{rootFiles.map((file) => (
<FileTreeFile
key={file.path}
path={file.path}
name={file.name}
/>
))}
</>
)
})()}

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +123
const [selectedFilePath, setSelectedFilePath] = useState<string>('')
const readFileResult = useSandboxReadFile(
activeWorkspaceId,
selectedFilePath,
'utf-8'
)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Selecting a FileTreeFolder triggers onSelect(path) (by design of FileTreeFolder), and this page wires onSelect directly to setSelectedFilePath. That means clicking a folder will attempt useSandboxReadFile(..., selectedFilePath) on a directory path. Filter selections to files only (e.g. ignore folder paths, or track selected node type) to avoid erroneous readFile calls and confusing UI state.

Copilot uses AI. Check for mistakes.
Comment on lines +309 to 316
{!(!(agent.provider ?? agent.modelId)) && (
<div className="mt-1.5 flex items-center gap-2">
<div className="flex h-5 items-center rounded-md border border-primary/20 bg-primary/10 px-1.5 text-[9px] font-bold uppercase tracking-wider text-primary">
<CpuIcon className="mr-1 size-3" />
<span>
{agent.provider && `${agent.provider} • `}
{(Boolean(agent.provider)) && `${agent.provider} • `}
{agent.modelId}
</span>
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new condition !(!(agent.provider ?? agent.modelId)) changes behavior vs the previous !!(agent.provider || agent.modelId). With the nullish-coalescing version, an empty-string provider will prevent the badge from showing even when modelId is present. Use a truthy check (agent.provider || agent.modelId) or explicitly check each field so the model badge still renders when only modelId is set.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants