forked from bytedance/UI-TARS-desktop
-
Notifications
You must be signed in to change notification settings - Fork 0
feat(search): add Firecrawl search provider + standalone web_fetch tool #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from 5 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
186fbdb
feat(search): add Firecrawl provider + web_scrape tool
rakshith48 2f86727
fix(search): address review — URL validation, log redaction, doc, web…
rakshith48 12731df
refactor(search): extract web_fetch into its own FetchToolProvider
rakshith48 35f1126
refactor(fetch): move web_fetch to its own capability dir
rakshith48 1aac19c
refactor(fetch): make web_fetch a standalone capability, decoupled fr…
rakshith48 debc76e
docs(firecrawl): fix stale fetch coupling + attribute scrape cost cor…
rakshith48 b43af08
fix(search): make @agent-infra/search + mcp-server-search build cleanly
rakshith48 4d28027
chore(search): add changeset for Firecrawl provider (search + shared,…
rakshith48 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 108 additions & 0 deletions
108
multimodal/agent-tars/core/src/environments/local/fetch/fetch-tool.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| /* | ||
| * Copyright (c) 2025 Bytedance, Inc. and its affiliates. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import { ConsoleLogger, Tool, z } from '@tarko/mcp-agent'; | ||
| import { firecrawl } from '@agent-infra/search'; | ||
|
|
||
| /** | ||
| * Configuration for the fetch tool. Reuses the search provider's credentials | ||
| * (`apiKey` is optional — Firecrawl has a keyless free tier; `baseUrl` targets | ||
| * a self-hosted instance). | ||
| */ | ||
| export interface FetchToolConfig { | ||
| apiKey?: string; | ||
| baseUrl?: string; | ||
| } | ||
|
|
||
| /** | ||
| * FetchToolProvider — a `web_fetch` tool backed by Firecrawl's scrape API. | ||
| * | ||
| * Reads the full, LLM-ready content of a single URL without driving the | ||
| * browser. Distinct from search (`web_search`, discovery) and from the | ||
| * headless browser's `browser_get_markdown` (which only reads the tab the | ||
| * browser is already on). Kept in its own provider so the search tool stays | ||
| * search-only. | ||
| */ | ||
| export class FetchToolProvider { | ||
| private logger: ConsoleLogger; | ||
| private config: FetchToolConfig; | ||
|
|
||
| constructor(logger: ConsoleLogger, config: FetchToolConfig) { | ||
| this.logger = logger.spawn('FetchToolProvider'); | ||
| this.config = config; | ||
| } | ||
|
|
||
| /** | ||
| * Create a `web_fetch` tool definition for agent registration. | ||
| */ | ||
| createFetchTool(): Tool { | ||
| const client = firecrawl({ | ||
| apiKey: this.config.apiKey, | ||
| apiUrl: this.config.baseUrl, | ||
| }); | ||
|
|
||
| return new Tool({ | ||
| id: 'web_fetch', | ||
| description: | ||
| 'Fetch the full content of a specific web page as clean, LLM-ready ' + | ||
| 'markdown — without opening it in the browser. Use this when you ' + | ||
| 'already have a URL (e.g. from web_search results) and need its full ' + | ||
| 'text, not just a snippet. Handles JavaScript-rendered pages and PDFs.', | ||
| parameters: z.object({ | ||
| url: z | ||
| .string() | ||
| .describe('The full URL to fetch (must start with http or https).'), | ||
| formats: z | ||
| .array(z.enum(['markdown', 'html', 'links'])) | ||
| .optional() | ||
| .describe('Output formats to return. Defaults to ["markdown"].'), | ||
| }), | ||
| function: async ({ url, formats }) => { | ||
| if (!url || url.trim() === '') { | ||
| return { error: 'A url is required' }; | ||
| } | ||
|
|
||
| // Enforce the http(s) contract stated in the description, and reject | ||
| // other schemes (file:, javascript:, ...) before spending a request. | ||
| let parsed: URL; | ||
| try { | ||
| parsed = new URL(url); | ||
| } catch { | ||
| return { error: 'A valid URL is required' }; | ||
| } | ||
| if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { | ||
| return { error: 'URL must start with http:// or https://' }; | ||
| } | ||
|
|
||
| // Log only origin + path — query strings may carry tokens / signed-link | ||
| // credentials that should not land in logs. | ||
| const safeUrl = `${parsed.origin}${parsed.pathname}`; | ||
|
|
||
| try { | ||
| this.logger.info(`Fetching: "${safeUrl}"`); | ||
|
|
||
| const doc = await client.scrape(url, { | ||
| formats: formats?.length ? formats : ['markdown'], | ||
| onlyMainContent: true, | ||
| }); | ||
|
|
||
| return { | ||
| url, | ||
| title: doc.metadata?.title, | ||
| markdown: doc.markdown, | ||
| html: doc.html, | ||
| links: doc.links, | ||
| metadata: doc.metadata, | ||
| }; | ||
| } catch (error) { | ||
| this.logger.error(`Fetch error for "${safeUrl}": ${error}`); | ||
| return { | ||
| error: `Fetch failed: ${error instanceof Error ? error.message : String(error)}`, | ||
| }; | ||
| } | ||
| }, | ||
| }); | ||
| } | ||
| } | ||
6 changes: 6 additions & 0 deletions
6
multimodal/agent-tars/core/src/environments/local/fetch/index.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| /* | ||
| * Copyright (c) 2025 Bytedance, Inc. and its affiliates. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| export * from './fetch-tool'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| # Firecrawl provider | ||
|
|
||
| [Firecrawl](https://firecrawl.dev) is a web-data API for AI agents: it searches | ||
| the web and returns **clean, LLM-ready markdown** for every result in a single | ||
| call. This package wires Firecrawl in as a first-class `SearchProvider` | ||
| (alongside `browser_search`, `tavily`, `bing_search`, `duckduckgo`, `searxng`) | ||
|
rakshith48 marked this conversation as resolved.
Outdated
|
||
| and Agent TARS additionally exposes a `web_fetch` tool when Firecrawl is the | ||
| configured provider. | ||
|
|
||
| ## Usage | ||
|
|
||
| ```ts | ||
| import { SearchClient, SearchProvider } from '@agent-infra/search'; | ||
|
|
||
| const client = new SearchClient({ | ||
| provider: SearchProvider.Firecrawl, | ||
| providerConfig: { apiKey: process.env.FIRECRAWL_API_KEY }, // optional: keyless free tier exists | ||
| }); | ||
|
|
||
| // Plain search (snippets) | ||
| await client.search({ query: 'UI-TARS', count: 5 }); | ||
|
|
||
| // Search + full-page content in one call (Firecrawl's differentiator) | ||
| await client.search( | ||
| { query: 'UI-TARS', count: 5 }, | ||
| { scrapeOptions: { formats: ['markdown'], onlyMainContent: true } }, | ||
| ); | ||
| ``` | ||
|
|
||
| In Agent TARS, **search and fetch are independent capabilities** — enable | ||
| either, both, or neither: | ||
|
|
||
| ```ts | ||
| { | ||
| // web_search — Firecrawl as the search provider | ||
| search: { | ||
| provider: 'firecrawl', | ||
| apiKey: process.env.FIRECRAWL_API_KEY, // optional | ||
| count: 10, | ||
| }, | ||
| // web_fetch — standalone, NOT tied to the search provider | ||
| fetch: { | ||
| apiKey: process.env.FIRECRAWL_API_KEY, // optional | ||
| }, | ||
| } | ||
| ``` | ||
|
|
||
| Because they're decoupled, you can mix freely — e.g. `browser_search` (or | ||
| `tavily`) for search **and** Firecrawl `fetch` for reading URLs, or `fetch` | ||
| alone with no search provider at all. | ||
|
|
||
| - `web_search` (from `search`) — discovery, optionally with full-page content | ||
| per result. | ||
| - `web_fetch` (from `fetch`) — fetch any single URL as clean markdown without | ||
| navigating the browser. Complements the existing `browser_get_markdown` | ||
| (which only reads the tab the browser is already on). | ||
|
|
||
| ## Why Firecrawl fits | ||
|
|
||
| Agent TARS today splits *find* (Tavily `web_search`) from *read* (a separate | ||
| `LinkReader` / `text_browser_view` MCP). Firecrawl's `/search` returns search | ||
| results **with** scraped content, and its `/scrape` reads arbitrary URLs — so a | ||
| single provider covers both motions, and `web_fetch` is a drop-in for the | ||
| commented-out `tavily_extract` path in `omni-tars`. | ||
|
|
||
| ## Firecrawl endpoint → Agent TARS fit | ||
|
|
||
| This PR ships **search + scrape**. The other endpoints were evaluated: | ||
|
|
||
| | Endpoint | Fit | Notes | | ||
| |---|---|---| | ||
| | **search** | ✅ shipped | First-class `SearchProvider`. Returns snippets, or full markdown per result via `scrapeOptions`. | | ||
| | **scrape** | ✅ shipped | `web_fetch` tool — read any URL (incl. JS-rendered pages and PDFs) to markdown without driving the browser. | | ||
| | **map** | 🟡 good follow-up | `map(url)` returns all discoverable URLs on a site — a natural cheap "site recon" tool the agent can call before deciding what to read. Bounded, fast, low risk. Recommended next addition. | | ||
| | **crawl** | 🟡 careful | `crawl()` is async and can return many large pages — risks long latency and blowing the context window inside a synchronous agent loop. Viable only as an opt-in tool with a hard `limit`; better suited to a batch/offline job than the interactive loop. | | ||
| | **parse** | 🟡 niche | `parse()` converts an **uploaded local file** (pdf/docx/xlsx/html) to markdown. Pairs with Agent TARS's filesystem environment ("read this local PDF"), but URL-hosted PDFs are already handled by `scrape`. Low-priority, real. | | ||
| | **monitor** | ❌ out of scope | Firecrawl Monitoring is a **scheduled change-tracking** product (watch a page/site over time, diff, notify). That's a cron/background capability, not an interactive agent tool — Agent TARS has no scheduled-task surface to host it today. | | ||
|
|
||
| ## Cost note | ||
|
|
||
| Plain search is 2 credits / 10 results. Adding `scrapeOptions` applies scrape | ||
| costs per result (1 credit/page basic). `web_fetch` is 1 credit/page. Keep | ||
| `scrapeOptions` off `web_search` unless you need content from *every* result; | ||
| otherwise search first, then `web_fetch` only the URLs you want. | ||
|
rakshith48 marked this conversation as resolved.
Outdated
|
||
38 changes: 38 additions & 0 deletions
38
packages/agent-infra/search/search/examples/firecrawl-search.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| /** | ||
| * Copyright (c) 2025 Bytedance, Inc. and its affiliates. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
| import { SearchClient, SearchProvider } from '../src'; | ||
|
|
||
| export async function firecrawlSearch() { | ||
| const client = new SearchClient({ | ||
| provider: SearchProvider.Firecrawl, | ||
| providerConfig: { | ||
| // Optional — Firecrawl has a keyless free tier (rate-limited per IP). | ||
| apiKey: process.env.FIRECRAWL_API_KEY, | ||
| }, | ||
| }); | ||
|
|
||
| const results = await client.search( | ||
| { | ||
| query: 'UI-TARS', | ||
| count: 5, | ||
| }, | ||
| { | ||
| // Firecrawl-specific: scrape full-page markdown for every result in the | ||
| // same call, so the agent gets grounded content rather than snippets. | ||
| scrapeOptions: { | ||
| formats: ['markdown'], | ||
| onlyMainContent: true, | ||
| }, | ||
| // tbs: 'qdr:w', // e.g. only results from the past week | ||
| }, | ||
| ); | ||
|
|
||
| console.log('Firecrawl Search Results:'); | ||
| console.log(JSON.stringify(results, null, 2)); | ||
| } | ||
|
|
||
| if (require.main === module) { | ||
| firecrawlSearch().catch(console.error); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.