diff --git a/docs/docs.json b/docs/docs.json
index 22a2c963fe..fde9bb9b9b 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -250,6 +250,7 @@
"edge/en/tools/web-scraping/firecrawlscrapewebsitetool",
"edge/en/tools/web-scraping/oxylabsscraperstool",
"edge/en/tools/web-scraping/brightdata-tools",
+ "edge/en/tools/web-scraping/mrscraper-tools",
"edge/en/tools/web-scraping/youai-contents"
]
},
@@ -13903,7 +13904,8 @@
"edge/pt-BR/tools/web-scraping/stagehandtool",
"edge/pt-BR/tools/web-scraping/firecrawlcrawlwebsitetool",
"edge/pt-BR/tools/web-scraping/firecrawlscrapewebsitetool",
- "edge/pt-BR/tools/web-scraping/oxylabsscraperstool"
+ "edge/pt-BR/tools/web-scraping/oxylabsscraperstool",
+ "edge/pt-BR/tools/web-scraping/mrscraper-tools"
]
},
{
@@ -26621,7 +26623,8 @@
"edge/ko/tools/web-scraping/firecrawlcrawlwebsitetool",
"edge/ko/tools/web-scraping/firecrawlscrapewebsitetool",
"edge/ko/tools/web-scraping/oxylabsscraperstool",
- "edge/ko/tools/web-scraping/brightdata-tools"
+ "edge/ko/tools/web-scraping/brightdata-tools",
+ "edge/ko/tools/web-scraping/mrscraper-tools"
]
},
{
@@ -39768,7 +39771,8 @@
"edge/ar/tools/web-scraping/firecrawlcrawlwebsitetool",
"edge/ar/tools/web-scraping/firecrawlscrapewebsitetool",
"edge/ar/tools/web-scraping/oxylabsscraperstool",
- "edge/ar/tools/web-scraping/brightdata-tools"
+ "edge/ar/tools/web-scraping/brightdata-tools",
+ "edge/ar/tools/web-scraping/mrscraper-tools"
]
},
{
diff --git a/docs/edge/ar/tools/web-scraping/mrscraper-tools.mdx b/docs/edge/ar/tools/web-scraping/mrscraper-tools.mdx
new file mode 100644
index 0000000000..7cbf918aee
--- /dev/null
+++ b/docs/edge/ar/tools/web-scraping/mrscraper-tools.mdx
@@ -0,0 +1,128 @@
+---
+title: أدوات MrScraper
+description: استخدم MrScraper مع CrewAI لاستخراج بيانات المواقع والزحف إليها بموثوقية، وجمع بيانات الويب العامة وتحويلها إلى مخرجات منظمة ونظيفة لمسارات عمل AI.
+icon: spider
+mode: "wide"
+---
+
+# أدوات MrScraper
+
+يوفر [MrScraper](https://mrscraper.com) استكشاف الويب والاستخراج باستخدام AI وجلب الصفحات المعروضة ومسارات عمل scraper القابلة لإعادة الاستخدام. يتضمن تكامل CrewAI خمس عشرة أداة مستقلة وfactory لاختيار الإمكانات التي يحتاجها Agent فقط.
+
+## التثبيت
+
+ثبّت `crewai-tools`. يستخدم التكامل تبعيات HTTP الموجودة، لذلك لا يلزم تثبيت SDK منفصل لـ MrScraper.
+
+```shell
+uv add crewai-tools
+```
+
+عيّن رمز API الخاص بـ MrScraper كمتغير بيئة:
+
+```shell
+export MRSCRAPER_API_TOKEN="your-mrscraper-token"
+```
+
+احتفظ بالرمز في متغير بيئة أو مدير أسرار. لا تضعه في prompts أو أوصاف Task أو ملفات المصدر أو وسيطات الأدوات التي يمكن للنموذج رؤيتها.
+
+## البدء السريع
+
+تعيد factory الخاصة بـ toolkit جميع الأدوات الخمس عشرة افتراضيًا. يُفضّل اختيار أصغر مجموعة مناسبة حتى يمتلك Agent مجموعة أدوات مركزة.
+
+```python
+from crewai import Agent, Crew, Task
+from crewai_tools import create_mrscraper_toolkit
+
+researcher = Agent(
+ role="Web researcher",
+ goal="Find public product pages and extract structured product data",
+ backstory="You make focused, cost-aware web extraction calls.",
+ tools=create_mrscraper_toolkit(groups=["Discovery", "Extraction"]),
+)
+
+task = Task(
+ description="Find the relevant product page and extract its name and price.",
+ expected_output="A concise summary containing the source URL, product name, and price.",
+ agent=researcher,
+)
+
+result = Crew(agents=[researcher], tasks=[task]).kickoff()
+```
+
+## الأدوات المتاحة
+
+| المجموعة | الأداة | الاستخدام | المدخلات الرئيسية |
+| --- | --- | --- | --- |
+| Account | `MrScraperGetAccountInfoTool` | فحص تفاصيل الحساب وحدود الاستخدام | لا يوجد |
+| Discovery | `MrScraperCrawlWebsiteUrlsTool` | اكتشاف عناوين URL بدءًا من موقع | `url`, `max_depth`, `max_pages`, `limit` |
+| Discovery | `MrScraperSearchGoogleSerpTool` | تنفيذ بحث Google SERP متزامن | `query`, `region`, `language`, `page` |
+| Extraction | `MrScraperExtractPageByPromptTool` | استخراج البيانات من صفحة واحدة باستخدام تعليمات AI | `url`, `prompt`, `output_schema`, `mode` |
+| Extraction | `MrScraperExtractListingsTool` | استخراج قوائم متكررة أو مقسمة إلى صفحات | `url`, `prompt`, `output_schema`, `max_pages` |
+| Extraction | `MrScraperExtractStructuredDataTool` | استخراج preset مدعوم للبيانات المنظمة | `url`, `category`, `mode` |
+| Extraction | `MrScraperFetchRenderedHtmlTool` | جلب HTML معروض باستخدام JavaScript أو Markdown أو cookies أو screenshots | `url`, `html`, `markdown`, `screenshot` |
+| Results | `MrScraperGetResultsTool` | عرض نتائج scraper مقسمة إلى صفحات | `scraper_id`, `page`, `page_size` |
+| Results | `MrScraperGetLatestResultsTool` | استرجاع أحدث نتائج scraper | `scraper_id`, `count` |
+| Results | `MrScraperGetResultDetailTool` | استرجاع نتيجة واحدة باستخدام المعرّف | `result_id` |
+| Scraper Creation | `MrScraperCreatePromptScraperTool` | إنشاء scraper قابل لإعادة الاستخدام يعتمد على prompt | `url`, `prompt`, `output_schema`, `mode` |
+| Scraper Creation | `MrScraperCreateListingScraperTool` | إنشاء listing scraper قابل لإعادة الاستخدام | `url`, `prompt`, `output_schema`, `max_pages` |
+| Scraper Creation | `MrScraperCreateWebsiteCrawlScraperTool` | إنشاء website crawl scraper قابل لإعادة الاستخدام | `url`, `max_depth`, `max_pages`, `limit` |
+| Scraper Runs | `MrScraperRunExistingScraperTool` | تشغيل AI scraper أو manual scraper حالي لعنوان URL واحد | `scraper_type`, `scraper_id`, `url` |
+| Scraper Runs | `MrScraperRunExistingScraperBatchTool` | تشغيل scraper حالي لعدة عناوين URL | `scraper_type`, `scraper_id`, `urls` |
+
+فئات البيانات المنظمة المدعومة هي `article` و`forumThread` و`hotel` و`jobPosting` و`post` و`product` و`property` و`restaurant` و`socialMediaProfile` و`tourAttraction`.
+
+## استخدام أداة واحدة مباشرة
+
+استخدم أداة واحدة عندما يحتاج Agent أو التطبيق إلى إمكانية واحدة فقط:
+
+```python
+from crewai_tools import MrScraperExtractPageByPromptTool
+
+tool = MrScraperExtractPageByPromptTool()
+result = tool.run(
+ url="https://example.com/products/123",
+ prompt="Extract the product name and current price",
+ output_schema={"name": "string", "price": "number"},
+)
+```
+
+يمكنك أيضًا تمرير `api_token="..."` إلى constructor الأداة أو factory الخاصة بـ toolkit. يُوصى باستخدام بيانات الاعتماد المعتمدة على البيئة لأنه يسهل إبقاؤها خارج كود التطبيق.
+
+## اختيار إمكانات Toolkit
+
+اختر الأدوات حسب اسم المجموعة دون حساسية لحالة الأحرف أو حسب الاسم العام الدقيق للأداة. لا تمرر `groups` و`tool_names` معًا.
+
+```python
+from crewai_tools import create_mrscraper_toolkit
+
+# All tools share one configured HTTP client.
+all_tools = create_mrscraper_toolkit()
+
+read_tools = create_mrscraper_toolkit(groups=["Account", "Results"])
+
+selected_tools = create_mrscraper_toolkit(
+ tool_names=[
+ "mrscraper_search_google_serp",
+ "mrscraper_fetch_rendered_html",
+ ]
+)
+```
+
+المجموعات المتاحة هي `Account` و`Discovery` و`Extraction` و`Results` و`Scraper Creation` و`Scraper Runs`.
+
+## القيم المعادة
+
+تُعاد كائنات JSON والمصفوفات والقيم scalar كنص JSON مضغوط حتى تظل مستقرة عند تمريرها عبر Agents وTasks وFlows. تُعاد استجابات HTML والنصوص العادية الأخرى كما يوفرها MrScraper.
+
+## إرشادات التشغيل
+
+- اجعل حدود صفحات الزحف ودفعات URL صغيرة بقدر ما تسمح به Task. يمكن أن تستهلك عمليات الزحف واستدعاءات المتصفح المعروض واستخراج القوائم والتشغيل بالدفعات قدرًا كبيرًا من حصة API.
+- لا تُعاد محاولة طلبات POST تلقائيًا لأن ذلك قد يؤدي إلى إنشاء jobs مكررة. تُرسل الحقول المتعلقة بإعادة المحاولة فقط للعمليات التي تدعمها.
+- استخرج فقط المحتوى المصرح لك بالوصول إليه. راجع شروط الموقع المستهدف ومتطلبات الخصوصية وسياسة robots والقانون المعمول به، خصوصًا للبيانات الشخصية أو المحمية بتسجيل الدخول.
+
+## استكشاف الأخطاء وإصلاحها
+
+- **الرمز مفقود:** عيّن قيمة غير فارغة لـ `MRSCRAPER_API_TOKEN` قبل إنشاء الأداة.
+- **وسيطات غير صالحة:** ترفض أدوات MrScraper المدخلات غير الموثقة. تحقق من schema الأداة واستخدم أسماء الحقول المقبولة بدقة.
+- **استدعاءات بطيئة:** قلّل `max_pages` أو عمق الزحف أو حجم الدفعة أو مخرجات المتصفح المطلوبة عند الحاجة.
+- **تنسيق إخراج غير متوقع:** يعيد بحث SERP صيغة JSON افتراضيًا؛ عيّن `format="html"` فقط عند الحاجة إلى HTML خام.
diff --git a/docs/edge/ar/tools/web-scraping/overview.mdx b/docs/edge/ar/tools/web-scraping/overview.mdx
index 3ba3b500e9..a649a99264 100644
--- a/docs/edge/ar/tools/web-scraping/overview.mdx
+++ b/docs/edge/ar/tools/web-scraping/overview.mdx
@@ -65,6 +65,10 @@ mode: "wide"
تكاملات بحث SERP و Web Unlocker و Dataset API.
+
+
+ استكشاف الويب والاستخراج باستخدام AI والصفحات المعروضة ومسارات عمل scraper القابلة لإعادة الاستخدام.
+
## **حالات الاستخدام الشائعة**
@@ -109,4 +113,4 @@ agent = Agent(
- **المواقع كثيفة JavaScript**: استخدم `SeleniumScrapingTool` للمحتوى الديناميكي
- **التوسع والأداء**: استخدم `FirecrawlScrapeWebsiteTool` للاستخراج بكميات كبيرة
- **البنية التحتية السحابية**: استخدم `BrowserBaseLoadTool` لأتمتة المتصفح القابلة للتوسع
-- **سير العمل المعقدة**: استخدم `StagehandTool` لتفاعلات المتصفح الذكية
\ No newline at end of file
+- **سير العمل المعقدة**: استخدم `StagehandTool` لتفاعلات المتصفح الذكية
diff --git a/docs/edge/en/tools/web-scraping/mrscraper-tools.mdx b/docs/edge/en/tools/web-scraping/mrscraper-tools.mdx
new file mode 100644
index 0000000000..633bb4b698
--- /dev/null
+++ b/docs/edge/en/tools/web-scraping/mrscraper-tools.mdx
@@ -0,0 +1,128 @@
+---
+title: MrScraper Tools
+description: Use MrScraper with CrewAI to reliably scrape and crawl websites, extract public web data, and turn it into clean, structured output for AI workflows.
+icon: spider
+mode: "wide"
+---
+
+# MrScraper Tools
+
+[MrScraper](https://mrscraper.com) provides web discovery, AI extraction, rendered-page fetching, and reusable scraper workflows. The CrewAI integration includes 15 independent tools and a factory for selecting only the capabilities an agent needs.
+
+## Installation
+
+Install `crewai-tools`. The integration uses its existing HTTP dependencies, so no separate MrScraper SDK is required.
+
+```shell
+uv add crewai-tools
+```
+
+Set your MrScraper API token as an environment variable:
+
+```shell
+export MRSCRAPER_API_TOKEN="your-mrscraper-token"
+```
+
+Keep the token in an environment variable or secret manager. Do not place it in prompts, task descriptions, source files, or tool arguments that the model can see.
+
+## Quick start
+
+The toolkit factory returns all 15 tools by default. Prefer selecting the smallest relevant group so the agent has a focused toolset.
+
+```python
+from crewai import Agent, Crew, Task
+from crewai_tools import create_mrscraper_toolkit
+
+researcher = Agent(
+ role="Web researcher",
+ goal="Find public product pages and extract structured product data",
+ backstory="You make focused, cost-aware web extraction calls.",
+ tools=create_mrscraper_toolkit(groups=["Discovery", "Extraction"]),
+)
+
+task = Task(
+ description="Find the relevant product page and extract its name and price.",
+ expected_output="A concise summary containing the source URL, product name, and price.",
+ agent=researcher,
+)
+
+result = Crew(agents=[researcher], tasks=[task]).kickoff()
+```
+
+## Available tools
+
+| Group | Tool | Use it for | Main inputs |
+| --- | --- | --- | --- |
+| Account | `MrScraperGetAccountInfoTool` | Inspect account details and usage limits | None |
+| Discovery | `MrScraperCrawlWebsiteUrlsTool` | Discover URLs starting from a website | `url`, `max_depth`, `max_pages`, `limit` |
+| Discovery | `MrScraperSearchGoogleSerpTool` | Run a synchronous Google SERP search | `query`, `region`, `language`, `page` |
+| Extraction | `MrScraperExtractPageByPromptTool` | Extract data from one page with AI instructions | `url`, `prompt`, `output_schema`, `mode` |
+| Extraction | `MrScraperExtractListingsTool` | Extract repeated or paginated listings | `url`, `prompt`, `output_schema`, `max_pages` |
+| Extraction | `MrScraperExtractStructuredDataTool` | Extract a supported structured-data preset | `url`, `category`, `mode` |
+| Extraction | `MrScraperFetchRenderedHtmlTool` | Fetch JavaScript-rendered HTML, Markdown, cookies, or screenshots | `url`, `html`, `markdown`, `screenshot` |
+| Results | `MrScraperGetResultsTool` | List paginated results for a scraper | `scraper_id`, `page`, `page_size` |
+| Results | `MrScraperGetLatestResultsTool` | Retrieve the newest results for a scraper | `scraper_id`, `count` |
+| Results | `MrScraperGetResultDetailTool` | Retrieve one result by ID | `result_id` |
+| Scraper Creation | `MrScraperCreatePromptScraperTool` | Create a reusable prompt-based scraper | `url`, `prompt`, `output_schema`, `mode` |
+| Scraper Creation | `MrScraperCreateListingScraperTool` | Create a reusable listing scraper | `url`, `prompt`, `output_schema`, `max_pages` |
+| Scraper Creation | `MrScraperCreateWebsiteCrawlScraperTool` | Create a reusable website crawl scraper | `url`, `max_depth`, `max_pages`, `limit` |
+| Scraper Runs | `MrScraperRunExistingScraperTool` | Run an existing AI or manual scraper for one URL | `scraper_type`, `scraper_id`, `url` |
+| Scraper Runs | `MrScraperRunExistingScraperBatchTool` | Run an existing scraper for multiple URLs | `scraper_type`, `scraper_id`, `urls` |
+
+The supported structured-data categories are `article`, `forumThread`, `hotel`, `jobPosting`, `post`, `product`, `property`, `restaurant`, `socialMediaProfile`, and `tourAttraction`.
+
+## Use one tool directly
+
+Use a single tool when the agent or application only needs one capability:
+
+```python
+from crewai_tools import MrScraperExtractPageByPromptTool
+
+tool = MrScraperExtractPageByPromptTool()
+result = tool.run(
+ url="https://example.com/products/123",
+ prompt="Extract the product name and current price",
+ output_schema={"name": "string", "price": "number"},
+)
+```
+
+You can also pass `api_token="..."` to a tool constructor or the toolkit factory. Environment-based credentials are recommended because they are easier to keep out of application code.
+
+## Select toolkit capabilities
+
+Select tools by case-insensitive group name or by exact public tool name. Do not pass `groups` and `tool_names` together.
+
+```python
+from crewai_tools import create_mrscraper_toolkit
+
+# All tools share one configured HTTP client.
+all_tools = create_mrscraper_toolkit()
+
+read_tools = create_mrscraper_toolkit(groups=["Account", "Results"])
+
+selected_tools = create_mrscraper_toolkit(
+ tool_names=[
+ "mrscraper_search_google_serp",
+ "mrscraper_fetch_rendered_html",
+ ]
+)
+```
+
+Available groups are `Account`, `Discovery`, `Extraction`, `Results`, `Scraper Creation`, and `Scraper Runs`.
+
+## Return values
+
+JSON objects, arrays, and scalar values are returned as compact JSON text so they remain stable when passed through Agents, Tasks, and Flows. HTML and other plain-text responses are returned as provided by MrScraper.
+
+## Operational guidance
+
+- Keep crawl page limits and URL batches as small as the task permits. Crawls, rendered browser calls, listing extraction, and batch runs can consume significant API allowance.
+- POST requests are not automatically retried because retrying can create duplicate jobs. Retry-related fields are sent only for operations that support them.
+- Only scrape content you are authorized to access. Review the target site's terms, privacy requirements, robots policy, and applicable law, especially for login-protected or personal data.
+
+## Troubleshooting
+
+- **Missing token:** Set a nonblank `MRSCRAPER_API_TOKEN` before constructing a tool.
+- **Invalid arguments:** MrScraper tools reject undocumented inputs. Check the tool schema and use the exact accepted field names.
+- **Slow calls:** Reduce `max_pages`, crawl depth, batch size, or requested browser outputs where appropriate.
+- **Unexpected output format:** SERP searches return JSON by default; set `format="html"` only when raw HTML is required.
diff --git a/docs/edge/en/tools/web-scraping/overview.mdx b/docs/edge/en/tools/web-scraping/overview.mdx
index 0031cf33e9..bbf52e0611 100644
--- a/docs/edge/en/tools/web-scraping/overview.mdx
+++ b/docs/edge/en/tools/web-scraping/overview.mdx
@@ -65,6 +65,10 @@ These tools enable your agents to interact with the web, extract data from websi
SERP search, Web Unlocker, and Dataset API integrations.
+
+
+ Web discovery, AI extraction, rendered pages, and reusable scraper workflows.
+
## **Common Use Cases**
@@ -108,5 +112,6 @@ agent = Agent(
- **Simple Tasks**: Use `ScrapeWebsiteTool` for basic content extraction
- **JavaScript-Heavy Sites**: Use `SeleniumScrapingTool` for dynamic content
- **Scale & Performance**: Use `FirecrawlScrapeWebsiteTool` for high-volume scraping
+- **Discovery & Structured Extraction**: Use MrScraper tools for URL discovery, SERP search, rendered pages, and reusable extraction workflows
- **Cloud Infrastructure**: Use `BrowserBaseLoadTool` for scalable browser automation
- **Complex Workflows**: Use `StagehandTool` for intelligent browser interactions
diff --git a/docs/edge/ko/tools/web-scraping/mrscraper-tools.mdx b/docs/edge/ko/tools/web-scraping/mrscraper-tools.mdx
new file mode 100644
index 0000000000..f54110cb17
--- /dev/null
+++ b/docs/edge/ko/tools/web-scraping/mrscraper-tools.mdx
@@ -0,0 +1,128 @@
+---
+title: MrScraper 도구
+description: CrewAI에서 MrScraper를 사용하여 웹사이트를 안정적으로 scraping 및 crawling하고, 공개 웹 데이터를 추출하여 AI 워크플로를 위한 깔끔한 구조화 결과로 변환합니다.
+icon: spider
+mode: "wide"
+---
+
+# MrScraper 도구
+
+[MrScraper](https://mrscraper.com)는 웹 탐색, AI 추출, 렌더링된 페이지 가져오기 및 재사용 가능한 스크래퍼 워크플로를 제공합니다. CrewAI 통합에는 15개의 독립적인 도구와 Agent에 필요한 기능만 선택할 수 있는 factory가 포함되어 있습니다.
+
+## 설치
+
+`crewai-tools`를 설치합니다. 이 통합은 기존 HTTP 종속성을 사용하므로 별도의 MrScraper SDK가 필요하지 않습니다.
+
+```shell
+uv add crewai-tools
+```
+
+MrScraper API 토큰을 환경 변수로 설정합니다.
+
+```shell
+export MRSCRAPER_API_TOKEN="your-mrscraper-token"
+```
+
+토큰은 환경 변수나 비밀 관리자에 보관하세요. 모델이 볼 수 있는 prompt, Task 설명, 소스 파일 또는 도구 인수에 토큰을 넣지 마세요.
+
+## 빠른 시작
+
+toolkit factory는 기본적으로 15개 도구를 모두 반환합니다. Agent가 집중된 도구 세트를 갖도록 가장 작은 관련 그룹을 선택하는 것이 좋습니다.
+
+```python
+from crewai import Agent, Crew, Task
+from crewai_tools import create_mrscraper_toolkit
+
+researcher = Agent(
+ role="Web researcher",
+ goal="Find public product pages and extract structured product data",
+ backstory="You make focused, cost-aware web extraction calls.",
+ tools=create_mrscraper_toolkit(groups=["Discovery", "Extraction"]),
+)
+
+task = Task(
+ description="Find the relevant product page and extract its name and price.",
+ expected_output="A concise summary containing the source URL, product name, and price.",
+ agent=researcher,
+)
+
+result = Crew(agents=[researcher], tasks=[task]).kickoff()
+```
+
+## 사용 가능한 도구
+
+| 그룹 | 도구 | 용도 | 주요 입력 |
+| --- | --- | --- | --- |
+| Account | `MrScraperGetAccountInfoTool` | 계정 세부 정보 및 사용 한도 확인 | 없음 |
+| Discovery | `MrScraperCrawlWebsiteUrlsTool` | 웹사이트에서 시작하여 URL 탐색 | `url`, `max_depth`, `max_pages`, `limit` |
+| Discovery | `MrScraperSearchGoogleSerpTool` | 동기식 Google SERP 검색 실행 | `query`, `region`, `language`, `page` |
+| Extraction | `MrScraperExtractPageByPromptTool` | AI 지침을 사용하여 단일 페이지에서 데이터 추출 | `url`, `prompt`, `output_schema`, `mode` |
+| Extraction | `MrScraperExtractListingsTool` | 반복되거나 페이지가 나뉜 목록 추출 | `url`, `prompt`, `output_schema`, `max_pages` |
+| Extraction | `MrScraperExtractStructuredDataTool` | 지원되는 구조화 데이터 preset 추출 | `url`, `category`, `mode` |
+| Extraction | `MrScraperFetchRenderedHtmlTool` | JavaScript로 렌더링된 HTML, Markdown, cookie 또는 screenshot 가져오기 | `url`, `html`, `markdown`, `screenshot` |
+| Results | `MrScraperGetResultsTool` | scraper의 페이지별 결과 목록 조회 | `scraper_id`, `page`, `page_size` |
+| Results | `MrScraperGetLatestResultsTool` | scraper의 최신 결과 조회 | `scraper_id`, `count` |
+| Results | `MrScraperGetResultDetailTool` | ID로 단일 결과 조회 | `result_id` |
+| Scraper Creation | `MrScraperCreatePromptScraperTool` | 재사용 가능한 prompt 기반 scraper 생성 | `url`, `prompt`, `output_schema`, `mode` |
+| Scraper Creation | `MrScraperCreateListingScraperTool` | 재사용 가능한 listing scraper 생성 | `url`, `prompt`, `output_schema`, `max_pages` |
+| Scraper Creation | `MrScraperCreateWebsiteCrawlScraperTool` | 재사용 가능한 웹사이트 crawl scraper 생성 | `url`, `max_depth`, `max_pages`, `limit` |
+| Scraper Runs | `MrScraperRunExistingScraperTool` | 하나의 URL에 기존 AI 또는 manual scraper 실행 | `scraper_type`, `scraper_id`, `url` |
+| Scraper Runs | `MrScraperRunExistingScraperBatchTool` | 여러 URL에 기존 scraper 실행 | `scraper_type`, `scraper_id`, `urls` |
+
+지원되는 구조화 데이터 category는 `article`, `forumThread`, `hotel`, `jobPosting`, `post`, `product`, `property`, `restaurant`, `socialMediaProfile`, `tourAttraction`입니다.
+
+## 단일 도구 직접 사용
+
+Agent나 애플리케이션에 하나의 기능만 필요할 때는 단일 도구를 사용하세요.
+
+```python
+from crewai_tools import MrScraperExtractPageByPromptTool
+
+tool = MrScraperExtractPageByPromptTool()
+result = tool.run(
+ url="https://example.com/products/123",
+ prompt="Extract the product name and current price",
+ output_schema={"name": "string", "price": "number"},
+)
+```
+
+도구 생성자나 toolkit factory에 `api_token="..."`을 전달할 수도 있습니다. 환경 기반 자격 증명은 애플리케이션 코드 외부에 보관하기 더 쉬우므로 권장됩니다.
+
+## Toolkit 기능 선택
+
+대소문자를 구분하지 않는 그룹 이름이나 정확한 공개 도구 이름으로 도구를 선택합니다. `groups`와 `tool_names`를 함께 전달하지 마세요.
+
+```python
+from crewai_tools import create_mrscraper_toolkit
+
+# All tools share one configured HTTP client.
+all_tools = create_mrscraper_toolkit()
+
+read_tools = create_mrscraper_toolkit(groups=["Account", "Results"])
+
+selected_tools = create_mrscraper_toolkit(
+ tool_names=[
+ "mrscraper_search_google_serp",
+ "mrscraper_fetch_rendered_html",
+ ]
+)
+```
+
+사용 가능한 그룹은 `Account`, `Discovery`, `Extraction`, `Results`, `Scraper Creation`, `Scraper Runs`입니다.
+
+## 반환값
+
+JSON 객체, 배열 및 scalar 값은 Agent, Task 및 Flow를 통과할 때 안정적으로 유지되도록 압축된 JSON 텍스트로 반환됩니다. HTML 및 기타 일반 텍스트 응답은 MrScraper가 제공한 그대로 반환됩니다.
+
+## 운영 지침
+
+- crawl 페이지 제한과 URL batch는 Task에 필요한 만큼만 작게 유지하세요. crawl, 렌더링된 browser 호출, listing 추출 및 batch 실행은 상당한 API 할당량을 소비할 수 있습니다.
+- POST 요청은 중복 job을 생성할 수 있으므로 자동으로 재시도되지 않습니다. 재시도 관련 필드는 이를 지원하는 작업에만 전송됩니다.
+- 접근 권한이 있는 콘텐츠만 scraping하세요. 특히 login으로 보호되거나 개인 데이터가 포함된 경우 대상 사이트의 약관, 개인정보 보호 요구 사항, robots 정책 및 관련 법률을 검토하세요.
+
+## 문제 해결
+
+- **토큰 누락:** 도구를 생성하기 전에 비어 있지 않은 `MRSCRAPER_API_TOKEN`을 설정하세요.
+- **잘못된 인수:** MrScraper 도구는 문서화되지 않은 입력을 거부합니다. 도구 schema를 확인하고 허용된 정확한 필드 이름을 사용하세요.
+- **느린 호출:** 필요에 따라 `max_pages`, crawl 깊이, batch 크기 또는 요청된 browser 출력을 줄이세요.
+- **예상치 못한 출력 형식:** SERP 검색은 기본적으로 JSON을 반환합니다. 원시 HTML이 필요한 경우에만 `format="html"`을 설정하세요.
diff --git a/docs/edge/ko/tools/web-scraping/overview.mdx b/docs/edge/ko/tools/web-scraping/overview.mdx
index 070f310bcf..3b93e38403 100644
--- a/docs/edge/ko/tools/web-scraping/overview.mdx
+++ b/docs/edge/ko/tools/web-scraping/overview.mdx
@@ -65,6 +65,10 @@ mode: "wide"
SERP 검색, 웹 언락커, 데이터셋 API 통합 기능을 지원합니다.
+
+
+ 웹 탐색, AI 추출, 렌더링된 페이지 및 재사용 가능한 scraper 워크플로를 제공합니다.
+
## **일반적인 사용 사례**
@@ -109,4 +113,4 @@ agent = Agent(
- **JavaScript 기반 사이트**: 동적 콘텐츠에는 `SeleniumScrapingTool`을 사용하세요
- **확장성 및 성능**: 대량 스크래핑에는 `FirecrawlScrapeWebsiteTool`을 사용하세요
- **클라우드 인프라**: 확장 가능한 브라우저 자동화에는 `BrowserBaseLoadTool`을 사용하세요
-- **복잡한 워크플로우**: 지능형 브라우저 상호작용에는 `StagehandTool`을 사용하세요
\ No newline at end of file
+- **복잡한 워크플로우**: 지능형 브라우저 상호작용에는 `StagehandTool`을 사용하세요
diff --git a/docs/edge/pt-BR/tools/web-scraping/mrscraper-tools.mdx b/docs/edge/pt-BR/tools/web-scraping/mrscraper-tools.mdx
new file mode 100644
index 0000000000..bfd35c7e1e
--- /dev/null
+++ b/docs/edge/pt-BR/tools/web-scraping/mrscraper-tools.mdx
@@ -0,0 +1,128 @@
+---
+title: Ferramentas MrScraper
+description: Use o MrScraper com CrewAI para extrair e rastrear sites com confiabilidade, coletar dados públicos da web e transformá-los em resultados limpos e estruturados para fluxos de AI.
+icon: spider
+mode: "wide"
+---
+
+# Ferramentas MrScraper
+
+O [MrScraper](https://mrscraper.com) oferece descoberta na web, extração com AI, obtenção de páginas renderizadas e fluxos de scrapers reutilizáveis. A integração com CrewAI inclui 15 ferramentas independentes e uma factory para selecionar apenas os recursos de que um Agent precisa.
+
+## Instalação
+
+Instale `crewai-tools`. A integração usa as dependências HTTP já existentes, portanto não é necessário instalar um SDK separado do MrScraper.
+
+```shell
+uv add crewai-tools
+```
+
+Defina seu token de API do MrScraper como variável de ambiente:
+
+```shell
+export MRSCRAPER_API_TOKEN="your-mrscraper-token"
+```
+
+Mantenha o token em uma variável de ambiente ou gerenciador de segredos. Não o coloque em prompts, descrições de Task, arquivos de código-fonte ou argumentos de ferramenta visíveis ao modelo.
+
+## Início rápido
+
+Por padrão, a factory do toolkit retorna todas as 15 ferramentas. Prefira selecionar o menor grupo relevante para que o Agent tenha um conjunto de ferramentas focado.
+
+```python
+from crewai import Agent, Crew, Task
+from crewai_tools import create_mrscraper_toolkit
+
+researcher = Agent(
+ role="Web researcher",
+ goal="Find public product pages and extract structured product data",
+ backstory="You make focused, cost-aware web extraction calls.",
+ tools=create_mrscraper_toolkit(groups=["Discovery", "Extraction"]),
+)
+
+task = Task(
+ description="Find the relevant product page and extract its name and price.",
+ expected_output="A concise summary containing the source URL, product name, and price.",
+ agent=researcher,
+)
+
+result = Crew(agents=[researcher], tasks=[task]).kickoff()
+```
+
+## Ferramentas disponíveis
+
+| Grupo | Ferramenta | Use para | Principais entradas |
+| --- | --- | --- | --- |
+| Account | `MrScraperGetAccountInfoTool` | Consultar detalhes da conta e limites de uso | Nenhuma |
+| Discovery | `MrScraperCrawlWebsiteUrlsTool` | Descobrir URLs a partir de um site | `url`, `max_depth`, `max_pages`, `limit` |
+| Discovery | `MrScraperSearchGoogleSerpTool` | Executar uma busca síncrona no Google SERP | `query`, `region`, `language`, `page` |
+| Extraction | `MrScraperExtractPageByPromptTool` | Extrair dados de uma página com instruções de AI | `url`, `prompt`, `output_schema`, `mode` |
+| Extraction | `MrScraperExtractListingsTool` | Extrair listagens repetidas ou paginadas | `url`, `prompt`, `output_schema`, `max_pages` |
+| Extraction | `MrScraperExtractStructuredDataTool` | Extrair uma predefinição de dados estruturados | `url`, `category`, `mode` |
+| Extraction | `MrScraperFetchRenderedHtmlTool` | Obter HTML renderizado por JavaScript, Markdown, cookies ou capturas de tela | `url`, `html`, `markdown`, `screenshot` |
+| Results | `MrScraperGetResultsTool` | Listar resultados paginados de um scraper | `scraper_id`, `page`, `page_size` |
+| Results | `MrScraperGetLatestResultsTool` | Obter os resultados mais recentes de um scraper | `scraper_id`, `count` |
+| Results | `MrScraperGetResultDetailTool` | Obter um resultado pelo ID | `result_id` |
+| Scraper Creation | `MrScraperCreatePromptScraperTool` | Criar um scraper reutilizável baseado em prompt | `url`, `prompt`, `output_schema`, `mode` |
+| Scraper Creation | `MrScraperCreateListingScraperTool` | Criar um scraper reutilizável de listagens | `url`, `prompt`, `output_schema`, `max_pages` |
+| Scraper Creation | `MrScraperCreateWebsiteCrawlScraperTool` | Criar um scraper reutilizável de rastreamento de site | `url`, `max_depth`, `max_pages`, `limit` |
+| Scraper Runs | `MrScraperRunExistingScraperTool` | Executar um scraper AI ou manual existente para uma URL | `scraper_type`, `scraper_id`, `url` |
+| Scraper Runs | `MrScraperRunExistingScraperBatchTool` | Executar um scraper existente para várias URLs | `scraper_type`, `scraper_id`, `urls` |
+
+As categorias de dados estruturados aceitas são `article`, `forumThread`, `hotel`, `jobPosting`, `post`, `product`, `property`, `restaurant`, `socialMediaProfile` e `tourAttraction`.
+
+## Uso direto de uma ferramenta
+
+Use uma única ferramenta quando o Agent ou a aplicação precisar apenas de um recurso:
+
+```python
+from crewai_tools import MrScraperExtractPageByPromptTool
+
+tool = MrScraperExtractPageByPromptTool()
+result = tool.run(
+ url="https://example.com/products/123",
+ prompt="Extract the product name and current price",
+ output_schema={"name": "string", "price": "number"},
+)
+```
+
+Também é possível passar `api_token="..."` para o construtor de uma ferramenta ou para a factory do toolkit. Credenciais baseadas no ambiente são recomendadas porque são mais fáceis de manter fora do código da aplicação.
+
+## Seleção de recursos do toolkit
+
+Selecione ferramentas pelo nome de grupo, sem distinção entre maiúsculas e minúsculas, ou pelo nome público exato da ferramenta. Não passe `groups` e `tool_names` juntos.
+
+```python
+from crewai_tools import create_mrscraper_toolkit
+
+# All tools share one configured HTTP client.
+all_tools = create_mrscraper_toolkit()
+
+read_tools = create_mrscraper_toolkit(groups=["Account", "Results"])
+
+selected_tools = create_mrscraper_toolkit(
+ tool_names=[
+ "mrscraper_search_google_serp",
+ "mrscraper_fetch_rendered_html",
+ ]
+)
+```
+
+Os grupos disponíveis são `Account`, `Discovery`, `Extraction`, `Results`, `Scraper Creation` e `Scraper Runs`.
+
+## Valores retornados
+
+Objetos, arrays e valores escalares JSON são retornados como texto JSON compacto para permanecerem estáveis ao passar por Agents, Tasks e Flows. HTML e outras respostas de texto simples são retornados como fornecidos pelo MrScraper.
+
+## Orientações operacionais
+
+- Mantenha os limites de páginas e os lotes de URLs tão pequenos quanto a Task permitir. Rastreamentos, chamadas de navegador renderizado, extração de listagens e execuções em lote podem consumir uma parcela significativa da cota da API.
+- Requisições POST não são repetidas automaticamente, pois a repetição pode criar jobs duplicados. Campos relacionados a novas tentativas são enviados apenas para operações compatíveis.
+- Extraia apenas conteúdo que você está autorizado a acessar. Analise os termos, requisitos de privacidade, política de robots e legislação aplicável do site de destino, especialmente para dados pessoais ou protegidos por login.
+
+## Solução de problemas
+
+- **Token ausente:** defina um `MRSCRAPER_API_TOKEN` não vazio antes de criar uma ferramenta.
+- **Argumentos inválidos:** as ferramentas MrScraper rejeitam entradas não documentadas. Verifique o schema da ferramenta e use os nomes exatos dos campos aceitos.
+- **Chamadas lentas:** reduza `max_pages`, a profundidade do rastreamento, o tamanho do lote ou as saídas solicitadas do navegador, quando apropriado.
+- **Formato de saída inesperado:** buscas SERP retornam JSON por padrão; use `format="html"` somente quando HTML bruto for necessário.
diff --git a/docs/edge/pt-BR/tools/web-scraping/overview.mdx b/docs/edge/pt-BR/tools/web-scraping/overview.mdx
index 0a493acaf1..8e6ef317db 100644
--- a/docs/edge/pt-BR/tools/web-scraping/overview.mdx
+++ b/docs/edge/pt-BR/tools/web-scraping/overview.mdx
@@ -61,6 +61,10 @@ Essas ferramentas permitem que seus agentes interajam com a web, extraiam dados
Acesse dados web em escala com o Oxylabs.
+
+
+ Descoberta na web, extração com AI, páginas renderizadas e fluxos de scrapers reutilizáveis.
+
## **Casos de Uso Comuns**
diff --git a/lib/crewai-tools/src/crewai_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/__init__.py
index 2db6be89bc..dd25668971 100644
--- a/lib/crewai-tools/src/crewai_tools/__init__.py
+++ b/lib/crewai-tools/src/crewai_tools/__init__.py
@@ -119,6 +119,24 @@
MongoDBVectorSearchConfig,
MongoDBVectorSearchTool,
)
+from crewai_tools.tools.mrscraper import (
+ MrScraperCrawlWebsiteUrlsTool,
+ MrScraperCreateListingScraperTool,
+ MrScraperCreatePromptScraperTool,
+ MrScraperCreateWebsiteCrawlScraperTool,
+ MrScraperExtractListingsTool,
+ MrScraperExtractPageByPromptTool,
+ MrScraperExtractStructuredDataTool,
+ MrScraperFetchRenderedHtmlTool,
+ MrScraperGetAccountInfoTool,
+ MrScraperGetLatestResultsTool,
+ MrScraperGetResultDetailTool,
+ MrScraperGetResultsTool,
+ MrScraperRunExistingScraperBatchTool,
+ MrScraperRunExistingScraperTool,
+ MrScraperSearchGoogleSerpTool,
+ create_mrscraper_toolkit,
+)
from crewai_tools.tools.multion_tool.multion_tool import MultiOnTool
from crewai_tools.tools.mysql_search_tool.mysql_search_tool import MySQLSearchTool
from crewai_tools.tools.nl2sql.nl2sql_tool import NL2SQLTool
@@ -286,6 +304,21 @@
"MergeAgentHandlerTool",
"MongoDBVectorSearchConfig",
"MongoDBVectorSearchTool",
+ "MrScraperCrawlWebsiteUrlsTool",
+ "MrScraperCreateListingScraperTool",
+ "MrScraperCreatePromptScraperTool",
+ "MrScraperCreateWebsiteCrawlScraperTool",
+ "MrScraperExtractListingsTool",
+ "MrScraperExtractPageByPromptTool",
+ "MrScraperExtractStructuredDataTool",
+ "MrScraperFetchRenderedHtmlTool",
+ "MrScraperGetAccountInfoTool",
+ "MrScraperGetLatestResultsTool",
+ "MrScraperGetResultDetailTool",
+ "MrScraperGetResultsTool",
+ "MrScraperRunExistingScraperBatchTool",
+ "MrScraperRunExistingScraperTool",
+ "MrScraperSearchGoogleSerpTool",
"MultiOnTool",
"MySQLSearchTool",
"NL2SQLTool",
@@ -338,6 +371,7 @@
"YoutubeVideoSearchTool",
"ZapierActionTool",
"ZapierActionTools",
+ "create_mrscraper_toolkit",
]
__version__ = "1.15.18"
diff --git a/lib/crewai-tools/src/crewai_tools/tools/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/__init__.py
index 2653490f76..51e9ae5db6 100644
--- a/lib/crewai-tools/src/crewai_tools/tools/__init__.py
+++ b/lib/crewai-tools/src/crewai_tools/tools/__init__.py
@@ -109,6 +109,24 @@
MongoDBVectorSearchConfig,
MongoDBVectorSearchTool,
)
+from crewai_tools.tools.mrscraper import (
+ MrScraperCrawlWebsiteUrlsTool,
+ MrScraperCreateListingScraperTool,
+ MrScraperCreatePromptScraperTool,
+ MrScraperCreateWebsiteCrawlScraperTool,
+ MrScraperExtractListingsTool,
+ MrScraperExtractPageByPromptTool,
+ MrScraperExtractStructuredDataTool,
+ MrScraperFetchRenderedHtmlTool,
+ MrScraperGetAccountInfoTool,
+ MrScraperGetLatestResultsTool,
+ MrScraperGetResultDetailTool,
+ MrScraperGetResultsTool,
+ MrScraperRunExistingScraperBatchTool,
+ MrScraperRunExistingScraperTool,
+ MrScraperSearchGoogleSerpTool,
+ create_mrscraper_toolkit,
+)
from crewai_tools.tools.multion_tool.multion_tool import MultiOnTool
from crewai_tools.tools.mysql_search_tool.mysql_search_tool import MySQLSearchTool
from crewai_tools.tools.nl2sql.nl2sql_tool import NL2SQLTool
@@ -270,6 +288,21 @@
"MongoDBToolSchema",
"MongoDBVectorSearchConfig",
"MongoDBVectorSearchTool",
+ "MrScraperCrawlWebsiteUrlsTool",
+ "MrScraperCreateListingScraperTool",
+ "MrScraperCreatePromptScraperTool",
+ "MrScraperCreateWebsiteCrawlScraperTool",
+ "MrScraperExtractListingsTool",
+ "MrScraperExtractPageByPromptTool",
+ "MrScraperExtractStructuredDataTool",
+ "MrScraperFetchRenderedHtmlTool",
+ "MrScraperGetAccountInfoTool",
+ "MrScraperGetLatestResultsTool",
+ "MrScraperGetResultDetailTool",
+ "MrScraperGetResultsTool",
+ "MrScraperRunExistingScraperBatchTool",
+ "MrScraperRunExistingScraperTool",
+ "MrScraperSearchGoogleSerpTool",
"MultiOnTool",
"MySQLSearchTool",
"NL2SQLTool",
@@ -320,4 +353,5 @@
"YoutubeChannelSearchTool",
"YoutubeVideoSearchTool",
"ZapierActionTools",
+ "create_mrscraper_toolkit",
]
diff --git a/lib/crewai-tools/src/crewai_tools/tools/mrscraper/README.md b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/README.md
new file mode 100644
index 0000000000..38bd0b3ea3
--- /dev/null
+++ b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/README.md
@@ -0,0 +1,122 @@
+# MrScraper tools
+
+The MrScraper integration exposes 15 independent CrewAI tools. It uses the
+`requests` dependency already included with `crewai-tools`; no vendor SDK or
+optional extra is required.
+
+## Installation and authentication
+
+```bash
+uv add crewai-tools
+export MRSCRAPER_API_TOKEN="your-mrscraper-token"
+```
+
+Keep the token in the environment or a secret manager. Do not put it in a tool
+argument, prompt, source file, trace, or task description.
+
+## Available tools
+
+Account:
+
+- `MrScraperGetAccountInfoTool` (`mrscraper_get_account_info`)
+
+Discovery:
+
+- `MrScraperCrawlWebsiteUrlsTool` (`mrscraper_crawl_website_urls`)
+- `MrScraperSearchGoogleSerpTool` (`mrscraper_search_google_serp`)
+
+Extraction:
+
+- `MrScraperExtractPageByPromptTool` (`mrscraper_extract_page_by_prompt`)
+- `MrScraperExtractListingsTool` (`mrscraper_extract_listings`)
+- `MrScraperExtractStructuredDataTool` (`mrscraper_extract_structured_data`)
+- `MrScraperFetchRenderedHtmlTool` (`mrscraper_fetch_rendered_html`)
+
+Results:
+
+- `MrScraperGetResultsTool` (`mrscraper_get_results`)
+- `MrScraperGetLatestResultsTool` (`mrscraper_get_latest_results`)
+- `MrScraperGetResultDetailTool` (`mrscraper_get_result_detail`)
+
+Scraper Creation:
+
+- `MrScraperCreatePromptScraperTool` (`mrscraper_create_prompt_scraper`)
+- `MrScraperCreateListingScraperTool` (`mrscraper_create_listing_scraper`)
+- `MrScraperCreateWebsiteCrawlScraperTool` (`mrscraper_create_website_crawl_scraper`)
+
+Scraper Runs:
+
+- `MrScraperRunExistingScraperTool` (`mrscraper_run_existing_scraper`)
+- `MrScraperRunExistingScraperBatchTool` (`mrscraper_run_existing_scraper_batch`)
+
+## Direct and toolkit usage
+
+Use a single narrow tool when that is all an agent needs:
+
+```python
+from crewai_tools import MrScraperExtractPageByPromptTool
+
+extract_product = MrScraperExtractPageByPromptTool()
+result = extract_product.run(
+ url="https://example.com/products/123",
+ prompt="Extract the product name and current price",
+ output_schema={"name": "string", "price": "number"},
+)
+```
+
+The factory returns new independent tool instances. By default it returns all
+15, configured with one shared HTTP client. Select case-insensitive groups or
+exact public tool names when an agent should have a smaller capability set:
+
+```python
+from crewai_tools import create_mrscraper_toolkit
+
+all_tools = create_mrscraper_toolkit()
+read_tools = create_mrscraper_toolkit(groups=["Account", "Results"])
+selected = create_mrscraper_toolkit(
+ tool_names=[
+ "mrscraper_search_google_serp",
+ "mrscraper_fetch_rendered_html",
+ ]
+)
+```
+
+## Agent and Crew example
+
+```python
+from crewai import Agent, Crew, Task
+from crewai_tools import create_mrscraper_toolkit
+
+researcher = Agent(
+ role="Web researcher",
+ goal="Collect authorized public product information",
+ backstory="You make narrow, cost-aware scraping calls.",
+ tools=create_mrscraper_toolkit(groups=["Discovery", "Extraction"]),
+)
+
+task = Task(
+ description="Find the relevant page and extract its product name and price.",
+ expected_output="A concise JSON-backed summary with the source URL.",
+ agent=researcher,
+)
+
+result = Crew(agents=[researcher], tasks=[task]).kickoff()
+```
+
+## Return values and operational notes
+
+JSON objects, arrays, and scalar values are returned as deterministic compact
+UTF-8 JSON text so they remain stable through Agents, Tasks, and Flows. HTML and
+other plain-text responses are returned as the exact upstream string. The tools
+provide synchronous `_run` implementations, matching comparable `requests`-based
+integrations in this package; no duplicate async transport is maintained.
+
+Crawls, rendered browser calls, listing extraction, and batch runs can take time
+and consume significant API allowance. Keep page counts and URL batches as small
+as the task permits. POST requests are not retried automatically because they can
+create duplicate jobs; API operation retry fields are passed only where the
+MrScraper contract defines them.
+
+Only scrape content you are authorized to access. Review the target site's terms,
+privacy requirements, robots policy, and applicable law before enabling automated
+access, especially for login-protected or personal data.
diff --git a/lib/crewai-tools/src/crewai_tools/tools/mrscraper/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/__init__.py
new file mode 100644
index 0000000000..679e42bd75
--- /dev/null
+++ b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/__init__.py
@@ -0,0 +1,48 @@
+"""Native MrScraper tools for CrewAI."""
+
+from crewai_tools.tools.mrscraper.account import MrScraperGetAccountInfoTool
+from crewai_tools.tools.mrscraper.discovery import (
+ MrScraperCrawlWebsiteUrlsTool,
+ MrScraperSearchGoogleSerpTool,
+)
+from crewai_tools.tools.mrscraper.extraction import (
+ MrScraperExtractListingsTool,
+ MrScraperExtractPageByPromptTool,
+ MrScraperExtractStructuredDataTool,
+ MrScraperFetchRenderedHtmlTool,
+)
+from crewai_tools.tools.mrscraper.results import (
+ MrScraperGetLatestResultsTool,
+ MrScraperGetResultDetailTool,
+ MrScraperGetResultsTool,
+)
+from crewai_tools.tools.mrscraper.scraper_creation import (
+ MrScraperCreateListingScraperTool,
+ MrScraperCreatePromptScraperTool,
+ MrScraperCreateWebsiteCrawlScraperTool,
+)
+from crewai_tools.tools.mrscraper.scraper_runs import (
+ MrScraperRunExistingScraperBatchTool,
+ MrScraperRunExistingScraperTool,
+)
+from crewai_tools.tools.mrscraper.toolkit import create_mrscraper_toolkit
+
+
+__all__ = [
+ "MrScraperCrawlWebsiteUrlsTool",
+ "MrScraperCreateListingScraperTool",
+ "MrScraperCreatePromptScraperTool",
+ "MrScraperCreateWebsiteCrawlScraperTool",
+ "MrScraperExtractListingsTool",
+ "MrScraperExtractPageByPromptTool",
+ "MrScraperExtractStructuredDataTool",
+ "MrScraperFetchRenderedHtmlTool",
+ "MrScraperGetAccountInfoTool",
+ "MrScraperGetLatestResultsTool",
+ "MrScraperGetResultDetailTool",
+ "MrScraperGetResultsTool",
+ "MrScraperRunExistingScraperBatchTool",
+ "MrScraperRunExistingScraperTool",
+ "MrScraperSearchGoogleSerpTool",
+ "create_mrscraper_toolkit",
+]
diff --git a/lib/crewai-tools/src/crewai_tools/tools/mrscraper/account.py b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/account.py
new file mode 100644
index 0000000000..ace87354ad
--- /dev/null
+++ b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/account.py
@@ -0,0 +1,24 @@
+"""MrScraper account tool."""
+
+from pydantic import BaseModel
+
+from crewai_tools.tools.mrscraper.base import MrScraperBaseTool
+from crewai_tools.tools.mrscraper.schemas import GetAccountInfoInput
+
+
+class MrScraperGetAccountInfoTool(MrScraperBaseTool):
+ """Retrieve subscription and token usage information."""
+
+ name: str = "mrscraper_get_account_info"
+ description: str = (
+ "Use this narrow read-only tool to inspect MrScraper account details, "
+ "token usage, and token limits. It does not scrape a page or create a job."
+ )
+ args_schema: type[BaseModel] = GetAccountInfoInput
+
+ def _run(self) -> str:
+ """Return subscription and token-usage details for the account."""
+ return self._client.request("GET", "primary", "/api/v1/subscription-accounts")
+
+
+__all__ = ["MrScraperGetAccountInfoTool"]
diff --git a/lib/crewai-tools/src/crewai_tools/tools/mrscraper/base.py b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/base.py
new file mode 100644
index 0000000000..e1725276ed
--- /dev/null
+++ b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/base.py
@@ -0,0 +1,49 @@
+"""Base class and credential resolution for MrScraper tools."""
+
+import os
+from typing import Any
+
+from crewai.tools import BaseTool, EnvVar
+from pydantic import Field, PrivateAttr
+
+from crewai_tools.tools.mrscraper.client import MrScraperClient
+
+
+def resolve_api_token(api_token: str | None = None) -> str:
+ """Resolve a nonblank token without exposing its value in an error."""
+ token = api_token if api_token is not None else os.getenv("MRSCRAPER_API_TOKEN")
+ if token is None or not token.strip():
+ raise ValueError(
+ "MRSCRAPER_API_TOKEN is required; set it in the environment before "
+ "creating a MrScraper tool"
+ )
+ return token
+
+
+class MrScraperBaseTool(BaseTool):
+ """Base for public MrScraper tools with a private shared client."""
+
+ env_vars: list[EnvVar] = Field(
+ default_factory=lambda: [
+ EnvVar(
+ name="MRSCRAPER_API_TOKEN",
+ description="MrScraper API token",
+ required=True,
+ )
+ ]
+ )
+ _client: MrScraperClient = PrivateAttr()
+
+ def __init__(
+ self,
+ *,
+ api_token: str | None = None,
+ client: MrScraperClient | None = None,
+ **kwargs: Any,
+ ) -> None:
+ """Initialize the tool with an injected client or resolved API token."""
+ super().__init__(**kwargs)
+ self._client = client or MrScraperClient(resolve_api_token(api_token))
+
+
+__all__ = ["MrScraperBaseTool", "resolve_api_token"]
diff --git a/lib/crewai-tools/src/crewai_tools/tools/mrscraper/client.py b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/client.py
new file mode 100644
index 0000000000..ade4be1942
--- /dev/null
+++ b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/client.py
@@ -0,0 +1,103 @@
+"""Shared, secret-safe HTTP transport for MrScraper tools."""
+
+import json
+import re
+from typing import Any, Literal
+
+import requests
+
+
+Origin = Literal["primary", "serp", "rendered"]
+
+_ORIGINS: dict[Origin, str] = {
+ "primary": "https://api.app.mrscraper.com",
+ "serp": "https://sync.scraper.mrscraper.com",
+ "rendered": "https://api.mrscraper.com",
+}
+_TIMEOUT = (10, 660)
+_ERROR_BODY_LIMIT = 1000
+_TOKEN_QUERY_RE = re.compile(r"([?&]token=)[^&\s]+", re.IGNORECASE)
+
+
+class MrScraperClient:
+ """Make requests only to MrScraper's fixed API origins."""
+
+ def __init__(self, token: str, *, session: requests.Session | None = None) -> None:
+ """Initialize a client with a nonblank token and optional HTTP session."""
+ if not token.strip():
+ raise ValueError("MRSCRAPER_API_TOKEN must be a nonblank value")
+ self._token = token
+ self._session = session or requests.Session()
+
+ def request(
+ self,
+ method: Literal["GET", "POST"],
+ origin: Origin,
+ path: str,
+ *,
+ params: dict[str, Any] | None = None,
+ json_body: dict[str, Any] | None = None,
+ force_text: bool = False,
+ ) -> str:
+ """Send one request and return deterministic JSON text or exact response text."""
+ url = f"{_ORIGINS[origin]}{path}"
+ headers = self._headers(origin)
+ if origin == "rendered":
+ params = {
+ "token": self._token,
+ "browserRendering": "true",
+ **(params or {}),
+ }
+ try:
+ response = self._session.request(
+ method,
+ url,
+ headers=headers,
+ params=params,
+ json=json_body,
+ timeout=_TIMEOUT,
+ )
+ except requests.RequestException as exc:
+ detail = self._sanitize(str(exc))
+ raise RuntimeError(f"MrScraper request failed: {detail}") from None
+
+ if not 200 <= response.status_code < 300:
+ sanitized = self._sanitize(response.text)
+ body = sanitized[:_ERROR_BODY_LIMIT]
+ suffix = "…" if len(sanitized) > _ERROR_BODY_LIMIT else ""
+ raise RuntimeError(
+ f"MrScraper API error (HTTP {response.status_code}): {body}{suffix}"
+ )
+
+ if force_text:
+ return self._sanitize(response.text)
+
+ content_type = response.headers.get("Content-Type", "").lower()
+ if "json" not in content_type:
+ return self._sanitize(response.text)
+ try:
+ value = response.json()
+ except requests.JSONDecodeError:
+ return self._sanitize(response.text)
+ serialized = json.dumps(value, ensure_ascii=False, separators=(",", ":"))
+ return self._sanitize(serialized)
+
+ def _headers(self, origin: Origin) -> dict[str, str]:
+ """Build the authentication headers required by an API origin."""
+ headers = {
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ }
+ if origin == "primary":
+ headers["x-api-token"] = self._token
+ elif origin == "serp":
+ headers["Authorization"] = f"Bearer {self._token}"
+ return headers
+
+ def _sanitize(self, value: str) -> str:
+ """Redact the configured token from response and error text."""
+ redacted = value.replace(self._token, "[REDACTED]")
+ return _TOKEN_QUERY_RE.sub(r"\1[REDACTED]", redacted)
+
+
+__all__ = ["MrScraperClient"]
diff --git a/lib/crewai-tools/src/crewai_tools/tools/mrscraper/discovery.py b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/discovery.py
new file mode 100644
index 0000000000..c1dbf9d586
--- /dev/null
+++ b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/discovery.py
@@ -0,0 +1,83 @@
+"""MrScraper discovery tools."""
+
+from typing import Literal
+
+from pydantic import BaseModel
+
+from crewai_tools.tools.mrscraper.base import MrScraperBaseTool
+from crewai_tools.tools.mrscraper.payloads import map_payload
+from crewai_tools.tools.mrscraper.schemas import MapScraperInput, SearchGoogleSerpInput
+
+
+class MrScraperCrawlWebsiteUrlsTool(MrScraperBaseTool):
+ """Discover URLs from a starting page."""
+
+ name: str = "mrscraper_crawl_website_urls"
+ description: str = (
+ "Use this potentially expensive immediate Map crawl to discover website URLs. "
+ "Use the website-crawl creation tool when the intent is to create a reusable scraper."
+ )
+ args_schema: type[BaseModel] = MapScraperInput
+
+ def _run(
+ self,
+ url: str,
+ max_depth: int = 2,
+ max_pages: int = 50,
+ limit: int = 50,
+ include_patterns: str | None = None,
+ exclude_patterns: str | None = None,
+ ) -> str:
+ """Discover URLs from a starting page with bounded crawl controls."""
+ return self._client.request(
+ "POST",
+ "primary",
+ "/api/v1/scrapers-ai",
+ json_body=map_payload(
+ url=url,
+ max_depth=max_depth,
+ max_pages=max_pages,
+ limit=limit,
+ include_patterns=include_patterns,
+ exclude_patterns=exclude_patterns,
+ ),
+ )
+
+
+class MrScraperSearchGoogleSerpTool(MrScraperBaseTool):
+ """Search Google synchronously through MrScraper."""
+
+ name: str = "mrscraper_search_google_serp"
+ description: str = (
+ "Use this for one narrow synchronous Google search. It returns compact JSON text "
+ "for JSON format or the exact upstream HTML string for HTML format."
+ )
+ args_schema: type[BaseModel] = SearchGoogleSerpInput
+
+ def _run(
+ self,
+ query: str,
+ region: str = "us",
+ language: str = "en",
+ page: int = 1,
+ format: Literal["json", "html"] = "json",
+ render_js: bool = False,
+ ) -> str:
+ """Run a synchronous Google search and return JSON or HTML text."""
+ return self._client.request(
+ "POST",
+ "serp",
+ "/api/google/serp/v2/sync",
+ json_body={
+ "query": query,
+ "region": region,
+ "language": language,
+ "page": page,
+ "format": format,
+ "renderJs": render_js,
+ },
+ force_text=format == "html",
+ )
+
+
+__all__ = ["MrScraperCrawlWebsiteUrlsTool", "MrScraperSearchGoogleSerpTool"]
diff --git a/lib/crewai-tools/src/crewai_tools/tools/mrscraper/extraction.py b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/extraction.py
new file mode 100644
index 0000000000..f40cb5a77a
--- /dev/null
+++ b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/extraction.py
@@ -0,0 +1,190 @@
+"""MrScraper immediate extraction tools."""
+
+from functools import lru_cache
+import json
+from pathlib import Path
+from typing import Any, Literal, cast
+
+from pydantic import BaseModel
+
+from crewai_tools.tools.mrscraper.base import MrScraperBaseTool
+from crewai_tools.tools.mrscraper.payloads import (
+ general_payload,
+ listing_payload,
+ rendered_request,
+)
+from crewai_tools.tools.mrscraper.schemas import (
+ ExtractStructuredDataInput,
+ FetchRenderedHtmlInput,
+ GeneralScraperInput,
+ ListingScraperInput,
+ ScrapingMode,
+ StructuredDataCategory,
+)
+
+
+@lru_cache(maxsize=1)
+def load_structured_data_prompts() -> dict[str, str]:
+ """Load the byte-preserved n8n structured extraction presets."""
+ path = Path(__file__).with_name("structured_data_prompts.json")
+ with path.open(encoding="utf-8") as preset_file:
+ return cast(dict[str, str], json.load(preset_file))
+
+
+class MrScraperExtractPageByPromptTool(MrScraperBaseTool):
+ """Perform immediate General extraction."""
+
+ name: str = "mrscraper_extract_page_by_prompt"
+ description: str = (
+ "Use this for immediate AI extraction from one page using a prompt. "
+ "Use create_prompt_scraper when the primary intent is reusable scraper creation."
+ )
+ args_schema: type[BaseModel] = GeneralScraperInput
+
+ def _run(
+ self,
+ url: str,
+ prompt: str | None = None,
+ output_schema: dict[str, Any] | None = None,
+ mode: ScrapingMode = "Super",
+ proxy_country: str | None = None,
+ ) -> str:
+ """Extract prompted data from one page immediately."""
+ return self._client.request(
+ "POST",
+ "primary",
+ "/api/v1/scrapers-ai",
+ json_body=general_payload(
+ url=url,
+ prompt=prompt,
+ output_schema=output_schema,
+ mode=mode,
+ proxy_country=proxy_country,
+ ),
+ )
+
+
+class MrScraperExtractListingsTool(MrScraperBaseTool):
+ """Perform immediate Listing extraction."""
+
+ name: str = "mrscraper_extract_listings"
+ description: str = (
+ "Use this potentially multi-page immediate extraction for repeated listings or "
+ "paginated content. Use create_listing_scraper for reusable scraper creation."
+ )
+ args_schema: type[BaseModel] = ListingScraperInput
+
+ def _run(
+ self,
+ url: str,
+ prompt: str | None = None,
+ output_schema: dict[str, Any] | None = None,
+ max_pages: int = 1,
+ proxy_country: str | None = None,
+ ) -> str:
+ """Extract repeated listings across one or more pages immediately."""
+ return self._client.request(
+ "POST",
+ "primary",
+ "/api/v1/scrapers-ai",
+ json_body=listing_payload(
+ url=url,
+ prompt=prompt,
+ output_schema=output_schema,
+ max_pages=max_pages,
+ proxy_country=proxy_country,
+ ),
+ )
+
+
+class MrScraperExtractStructuredDataTool(MrScraperBaseTool):
+ """Extract one of the bundled structured-data presets."""
+
+ name: str = "mrscraper_extract_structured_data"
+ description: str = (
+ "Use this immediate extraction tool when a page matches one supported structured "
+ "category, such as article, product, hotel, job, property, restaurant, or tour."
+ )
+ args_schema: type[BaseModel] = ExtractStructuredDataInput
+
+ def _run(
+ self,
+ url: str,
+ category: StructuredDataCategory = "article",
+ mode: ScrapingMode = "Super",
+ proxy_country: str | None = None,
+ ) -> str:
+ """Extract data using the selected bundled structured-data preset."""
+ payload: dict[str, Any] = {
+ "graph": "general",
+ "url": url,
+ "message": load_structured_data_prompts()[category],
+ "mode": mode,
+ }
+ if proxy_country is not None:
+ payload["proxyCountry"] = proxy_country
+ return self._client.request(
+ "POST", "primary", "/api/v1/scrapers-ai", json_body=payload
+ )
+
+
+class MrScraperFetchRenderedHtmlTool(MrScraperBaseTool):
+ """Fetch a browser-rendered page."""
+
+ name: str = "mrscraper_fetch_rendered_html"
+ description: str = (
+ "Use this immediate stealth-browser call when JavaScript-rendered HTML, Markdown, "
+ "cookies, or a screenshot is needed. Keep the requested outputs narrow to control cost."
+ )
+ args_schema: type[BaseModel] = FetchRenderedHtmlInput
+
+ def _run(
+ self,
+ url: str,
+ max_retries: int = 3,
+ timeout: int = 300,
+ geo_code: str = "us",
+ proxy_country: str = "us",
+ screenshot: bool = False,
+ screenshot_mode: Literal["full", "top"] | None = None,
+ html: bool = True,
+ markdown: bool = False,
+ token_cap: int | None = None,
+ wait_for_selector: str | None = None,
+ wait_until: Literal["domcontentloaded", "load", "networkidle"] | None = None,
+ block_resources: bool = False,
+ home_page: bool = False,
+ return_cookie: bool = False,
+ super_mode: bool = False,
+ ) -> str:
+ """Fetch a rendered page with optional browser outputs and controls."""
+ params, body = rendered_request(
+ url=url,
+ max_retries=max_retries,
+ timeout=timeout,
+ geo_code=geo_code,
+ proxy_country=proxy_country,
+ screenshot=screenshot,
+ screenshot_mode=screenshot_mode,
+ html=html,
+ markdown=markdown,
+ token_cap=token_cap,
+ wait_for_selector=wait_for_selector,
+ wait_until=wait_until,
+ block_resources=block_resources,
+ home_page=home_page,
+ return_cookie=return_cookie,
+ super_mode=super_mode,
+ )
+ return self._client.request(
+ "POST", "rendered", "/", params=params, json_body=body
+ )
+
+
+__all__ = [
+ "MrScraperExtractListingsTool",
+ "MrScraperExtractPageByPromptTool",
+ "MrScraperExtractStructuredDataTool",
+ "MrScraperFetchRenderedHtmlTool",
+ "load_structured_data_prompts",
+]
diff --git a/lib/crewai-tools/src/crewai_tools/tools/mrscraper/payloads.py b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/payloads.py
new file mode 100644
index 0000000000..c0f208b15c
--- /dev/null
+++ b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/payloads.py
@@ -0,0 +1,219 @@
+"""Request payload builders shared by MrScraper operations."""
+
+import json
+from typing import Any
+
+
+def _include_if_present(
+ payload: dict[str, Any], key: str, value: Any
+) -> dict[str, Any]:
+ """Add a payload value unless it is absent."""
+ if value is not None:
+ payload[key] = value
+ return payload
+
+
+def append_output_schema(
+ prompt: str | None, output_schema: dict[str, Any] | None, label: str
+) -> str | None:
+ """Append a compact output schema exactly once using the API's label."""
+ if output_schema is None:
+ return prompt
+ schema = json.dumps(output_schema, ensure_ascii=False, separators=(",", ":"))
+ schema_instruction = f"{label}\n{schema}"
+ return (
+ f"{prompt}\n\n{schema_instruction}"
+ if prompt is not None
+ else schema_instruction
+ )
+
+
+def general_payload(
+ *,
+ url: str,
+ prompt: str | None,
+ output_schema: dict[str, Any] | None,
+ mode: str,
+ proxy_country: str | None,
+) -> dict[str, Any]:
+ """Build a General AI extraction payload."""
+ payload: dict[str, Any] = {"graph": "general", "url": url, "mode": mode}
+ message = append_output_schema(
+ prompt, output_schema, "Return the output as JSON matching this schema:"
+ )
+ _include_if_present(payload, "message", message)
+ _include_if_present(payload, "proxyCountry", proxy_country)
+ return payload
+
+
+def listing_payload(
+ *,
+ url: str,
+ prompt: str | None,
+ output_schema: dict[str, Any] | None,
+ max_pages: int,
+ proxy_country: str | None,
+) -> dict[str, Any]:
+ """Build a Listing AI extraction payload."""
+ payload: dict[str, Any] = {
+ "graph": "listing",
+ "url": url,
+ "maxPages": max_pages,
+ }
+ message = append_output_schema(
+ prompt, output_schema, "Return each item as JSON matching this schema:"
+ )
+ _include_if_present(payload, "message", message)
+ _include_if_present(payload, "proxyCountry", proxy_country)
+ return payload
+
+
+def map_payload(
+ *,
+ url: str,
+ max_depth: int,
+ max_pages: int,
+ limit: int,
+ include_patterns: str | None,
+ exclude_patterns: str | None,
+) -> dict[str, Any]:
+ """Build a Map AI crawl payload."""
+ payload: dict[str, Any] = {
+ "graph": "map",
+ "url": url,
+ "maxDepth": max_depth,
+ "maxPages": max_pages,
+ "limit": limit,
+ }
+ _include_if_present(payload, "includePatterns", include_patterns)
+ _include_if_present(payload, "excludePatterns", exclude_patterns)
+ return payload
+
+
+def rendered_request(
+ *,
+ url: str,
+ max_retries: int,
+ timeout: int,
+ geo_code: str,
+ proxy_country: str,
+ screenshot: bool,
+ screenshot_mode: str | None,
+ html: bool,
+ markdown: bool,
+ token_cap: int | None,
+ wait_for_selector: str | None,
+ wait_until: str | None,
+ block_resources: bool,
+ home_page: bool,
+ return_cookie: bool,
+ super_mode: bool,
+) -> tuple[dict[str, Any], dict[str, Any]]:
+ """Split rendered-page options into query parameters and a JSON body."""
+
+ def bool_text(value: bool) -> str:
+ """Serialize a boolean for rendered-page query parameters."""
+ return "true" if value else "false"
+
+ params: dict[str, Any] = {
+ "timeout": timeout,
+ "geoCode": geo_code,
+ "html": bool_text(html),
+ "markdown": bool_text(markdown),
+ "proxyCountry": proxy_country,
+ }
+ if screenshot:
+ params["screenshot"] = screenshot_mode or "full"
+ _include_if_present(params, "waitForSelector", wait_for_selector)
+ _include_if_present(params, "waitUntil", wait_until)
+ if block_resources:
+ params["blockResources"] = bool_text(block_resources)
+ if return_cookie:
+ params["returnCookie"] = bool_text(return_cookie)
+ if super_mode:
+ params["super"] = bool_text(super_mode)
+ body = {
+ "url": url,
+ "maxRetries": max_retries,
+ }
+ _include_if_present(body, "tokenCap", token_cap)
+ if home_page:
+ body["homePage"] = home_page
+ return params, body
+
+
+def existing_run_payload(values: dict[str, Any]) -> dict[str, Any]:
+ """Build an agent-specific rerun payload from validated public inputs."""
+ payload: dict[str, Any] = {
+ "scraperId": values["scraper_id"],
+ "url": values["url"],
+ "maxRetry": values["max_retry"],
+ }
+ _include_if_present(payload, "proxyCountry", values.get("proxy_country"))
+
+ if values["scraper_type"] == "manual":
+ mapping = {
+ "bypass_proxy": "bypassProxy",
+ "cookie_jar": "cookieJar",
+ "cookies": "cookies",
+ "home_page": "homePage",
+ "home_page_timeout": "homePageTimeout",
+ "html": "html",
+ "markdown": "markdown",
+ "paginator": "paginator",
+ "proxy": "proxy",
+ "record": "record",
+ "return_cookie": "returnCookie",
+ "stream": "stream",
+ "timeout": "timeout",
+ "token_cap": "tokenCap",
+ }
+ for source, target in mapping.items():
+ _include_if_present(payload, target, values.get(source))
+ screenshot = values.get("screenshot")
+ if screenshot is not None:
+ payload["screenshot"] = "true" if screenshot else "false"
+ return payload
+
+ agent_type = values["agent_type"]
+ if agent_type == "map":
+ for source, target in {
+ "max_depth": "maxDepth",
+ "max_pages": "maxPages",
+ "limit": "limit",
+ "include_patterns": "includePatterns",
+ "exclude_patterns": "excludePatterns",
+ }.items():
+ _include_if_present(payload, target, values.get(source))
+ return payload
+
+ ai_mapping = {
+ "bypass_proxy": "bypassProxy",
+ "html": "html",
+ "markdown": "markdown",
+ "render_javascript": "renderJavascript",
+ "return_cookies": "returnCookies",
+ "screenshot": "screenshot",
+ "use_home_page": "useHomePage",
+ "wait_for_selector": "waitForSelector",
+ }
+ for source, target in ai_mapping.items():
+ _include_if_present(payload, target, values.get(source))
+ if agent_type == "listing":
+ for source, target in {
+ "max_pages": "maxPages",
+ "timeout": "timeout",
+ "stream": "stream",
+ }.items():
+ _include_if_present(payload, target, values.get(source))
+ return payload
+
+
+__all__ = [
+ "append_output_schema",
+ "existing_run_payload",
+ "general_payload",
+ "listing_payload",
+ "map_payload",
+ "rendered_request",
+]
diff --git a/lib/crewai-tools/src/crewai_tools/tools/mrscraper/results.py b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/results.py
new file mode 100644
index 0000000000..c92d05bec6
--- /dev/null
+++ b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/results.py
@@ -0,0 +1,95 @@
+"""MrScraper result retrieval tools."""
+
+from typing import Literal
+from urllib.parse import quote
+
+from pydantic import BaseModel
+
+from crewai_tools.tools.mrscraper.base import MrScraperBaseTool
+from crewai_tools.tools.mrscraper.schemas import (
+ GetLatestResultsInput,
+ GetResultDetailInput,
+ GetResultsInput,
+)
+
+
+class MrScraperGetResultsTool(MrScraperBaseTool):
+ """Retrieve a configurable page of results."""
+
+ name: str = "mrscraper_get_results"
+ description: str = (
+ "Use this to page through results for one scraper with explicit paging and sort "
+ "controls. Use get_latest_results when only the newest N records are needed."
+ )
+ args_schema: type[BaseModel] = GetResultsInput
+
+ def _run(
+ self,
+ scraper_id: str,
+ page: int = 1,
+ page_size: int = 10,
+ sort_by: Literal["createdAt"] = "createdAt",
+ sort_order: Literal["ASC", "DESC"] = "DESC",
+ ) -> str:
+ """Return a configured page of results for one scraper."""
+ return self._client.request(
+ "GET",
+ "primary",
+ "/api/v1/results",
+ params={
+ "filters[scraperId]": scraper_id,
+ "page": page,
+ "pageSize": page_size,
+ "sort": sort_by,
+ "sortOrder": sort_order,
+ },
+ )
+
+
+class MrScraperGetLatestResultsTool(MrScraperBaseTool):
+ """Retrieve the newest N results."""
+
+ name: str = "mrscraper_get_latest_results"
+ description: str = (
+ "Use this shortcut for the newest N results from one scraper. Use get_results "
+ "instead when page navigation or ascending order is required."
+ )
+ args_schema: type[BaseModel] = GetLatestResultsInput
+
+ def _run(self, scraper_id: str, count: int = 10) -> str:
+ """Return the newest results for one scraper."""
+ return self._client.request(
+ "GET",
+ "primary",
+ "/api/v1/results",
+ params={
+ "filters[scraperId]": scraper_id,
+ "page": 1,
+ "pageSize": count,
+ "sort": "createdAt",
+ "sortOrder": "DESC",
+ },
+ )
+
+
+class MrScraperGetResultDetailTool(MrScraperBaseTool):
+ """Retrieve one result by ID."""
+
+ name: str = "mrscraper_get_result_detail"
+ description: str = (
+ "Use this narrow lookup when a specific MrScraper result ID is already known. "
+ "It returns that record rather than a paginated collection."
+ )
+ args_schema: type[BaseModel] = GetResultDetailInput
+
+ def _run(self, result_id: str) -> str:
+ """Return one result after safely encoding its identifier."""
+ encoded_id = quote(result_id, safe="")
+ return self._client.request("GET", "primary", f"/api/v1/results/{encoded_id}")
+
+
+__all__ = [
+ "MrScraperGetLatestResultsTool",
+ "MrScraperGetResultDetailTool",
+ "MrScraperGetResultsTool",
+]
diff --git a/lib/crewai-tools/src/crewai_tools/tools/mrscraper/schemas.py b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/schemas.py
new file mode 100644
index 0000000000..ea23df3e78
--- /dev/null
+++ b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/schemas.py
@@ -0,0 +1,427 @@
+"""Pydantic input schemas for MrScraper tools."""
+
+from __future__ import annotations
+
+from typing import Annotated, Any, Literal
+
+from pydantic import BaseModel, ConfigDict, Field, StringConstraints, model_validator
+
+
+NonBlankStr = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
+TwoLetterCode = Annotated[
+ str, StringConstraints(strip_whitespace=True, pattern=r"^[A-Za-z]{2}$")
+]
+StrictInt = Annotated[int, Field(strict=True)]
+NonNegativeInt = Annotated[int, Field(strict=True, ge=0)]
+PositiveInt = Annotated[int, Field(strict=True, ge=1)]
+
+ScrapingMode = Literal["Super", "Cheap"]
+StructuredDataCategory = Literal[
+ "article",
+ "forumThread",
+ "hotel",
+ "jobPosting",
+ "post",
+ "product",
+ "property",
+ "restaurant",
+ "socialMediaProfile",
+ "tourAttraction",
+]
+
+
+class MrScraperInput(BaseModel):
+ """Base schema that rejects undocumented arguments."""
+
+ model_config = ConfigDict(extra="forbid")
+
+
+class GetAccountInfoInput(MrScraperInput):
+ """The account operation has no model-supplied inputs."""
+
+
+class MapScraperInput(MrScraperInput):
+ """Inputs shared by immediate and reusable website crawl tools."""
+
+ url: NonBlankStr = Field(description="Required starting URL to crawl.")
+ max_depth: StrictInt = Field(
+ default=2, description="Maximum link depth to crawl; defaults to 2."
+ )
+ max_pages: StrictInt = Field(
+ default=50, description="Maximum pages to evaluate; defaults to 50."
+ )
+ limit: PositiveInt = Field(
+ default=50, description="Maximum URLs to return; defaults to 50; minimum 1."
+ )
+ include_patterns: NonBlankStr | None = Field(
+ default=None,
+ description="Optional pipe-separated regular expressions for URLs to include.",
+ )
+ exclude_patterns: NonBlankStr | None = Field(
+ default=None,
+ description="Optional pipe-separated regular expressions for URLs to exclude.",
+ )
+
+
+class SearchGoogleSerpInput(MrScraperInput):
+ """Inputs for synchronous Google SERP search."""
+
+ query: NonBlankStr = Field(description="Required Google search query.")
+ region: TwoLetterCode = Field(
+ default="us", description="Two-letter result region code; defaults to 'us'."
+ )
+ language: TwoLetterCode = Field(
+ default="en", description="Two-letter result language code; defaults to 'en'."
+ )
+ page: PositiveInt = Field(
+ default=1, description="Google result page number; defaults to 1; minimum 1."
+ )
+ format: Literal["json", "html"] = Field(
+ default="json",
+ description="Response format: 'json' or 'html'; defaults to 'json'.",
+ )
+ render_js: bool = Field(
+ default=False,
+ description="Whether to render JavaScript before collecting results; defaults to false.",
+ )
+
+
+class GeneralScraperInput(MrScraperInput):
+ """Inputs shared by immediate and reusable prompt scrapers."""
+
+ url: NonBlankStr = Field(description="Required page URL to scrape.")
+ prompt: NonBlankStr | None = Field(
+ default=None, description="Optional extraction instructions for the AI scraper."
+ )
+ output_schema: dict[str, Any] | None = Field(
+ default=None,
+ description="Optional JSON object describing the expected output shape.",
+ )
+ mode: ScrapingMode = Field(
+ default="Super",
+ description="Scraping mode, 'Super' or 'Cheap'; defaults to 'Super'.",
+ )
+ proxy_country: TwoLetterCode | None = Field(
+ default=None, description="Optional ISO country code for the proxy."
+ )
+
+
+class ListingScraperInput(MrScraperInput):
+ """Inputs shared by immediate and reusable listing scrapers."""
+
+ url: NonBlankStr = Field(description="Required listing page URL to scrape.")
+ prompt: NonBlankStr | None = Field(
+ default=None,
+ description="Optional instructions describing each listing item to extract.",
+ )
+ output_schema: dict[str, Any] | None = Field(
+ default=None,
+ description="Optional JSON object describing each expected listing item.",
+ )
+ max_pages: PositiveInt = Field(
+ default=1,
+ description="Maximum pagination pages to scrape; defaults to 1; minimum 1.",
+ )
+ proxy_country: TwoLetterCode | None = Field(
+ default=None, description="Optional ISO country code for the proxy."
+ )
+
+
+class ExtractStructuredDataInput(MrScraperInput):
+ """Inputs for preset structured-data extraction."""
+
+ url: NonBlankStr = Field(description="Required page URL to scrape.")
+ category: StructuredDataCategory = Field(
+ default="article",
+ description="Structured extraction preset category; defaults to 'article'.",
+ )
+ mode: ScrapingMode = Field(
+ default="Super",
+ description="Scraping mode, 'Super' or 'Cheap'; defaults to 'Super'.",
+ )
+ proxy_country: TwoLetterCode | None = Field(
+ default=None, description="Optional ISO country code for the proxy."
+ )
+
+
+class FetchRenderedHtmlInput(MrScraperInput):
+ """Inputs for the rendered-page API."""
+
+ url: NonBlankStr = Field(description="Required target URL to render.")
+ max_retries: NonNegativeInt = Field(
+ default=3, description="Maximum retry attempts; defaults to 3; minimum 0."
+ )
+ timeout: PositiveInt = Field(
+ default=300,
+ description="Maximum page-load time in seconds; defaults to 300; minimum 1.",
+ )
+ geo_code: TwoLetterCode = Field(
+ default="us", description="Geolocation country code; defaults to 'us'."
+ )
+ proxy_country: TwoLetterCode = Field(
+ default="us", description="Proxy country code; defaults to 'us'."
+ )
+ screenshot: bool = Field(
+ default=False, description="Whether to capture a screenshot; defaults to false."
+ )
+ screenshot_mode: Literal["full", "top"] | None = Field(
+ default=None,
+ description="Optional screenshot mode; used only when screenshot is true.",
+ )
+ html: bool = Field(
+ default=True, description="Whether to include rendered HTML; defaults to true."
+ )
+ markdown: bool = Field(
+ default=False, description="Whether to include Markdown; defaults to false."
+ )
+ token_cap: PositiveInt | None = Field(
+ default=None,
+ description="Optional maximum processing token allowance; minimum 1.",
+ )
+ wait_for_selector: NonBlankStr | None = Field(
+ default=None, description="Optional CSS selector to await before returning."
+ )
+ wait_until: Literal["domcontentloaded", "load", "networkidle"] | None = Field(
+ default=None,
+ description="Optional browser lifecycle event to await.",
+ )
+ block_resources: bool = Field(
+ default=False,
+ description="Whether to block images, fonts, and stylesheets; defaults to false.",
+ )
+ home_page: bool = Field(
+ default=False,
+ description="Whether to visit the site home page first; defaults to false.",
+ )
+ return_cookie: bool = Field(
+ default=False,
+ description="Whether to include browser cookies; defaults to false.",
+ )
+ super_mode: bool = Field(
+ default=False,
+ description="Whether to use stronger device mode; defaults to false.",
+ )
+
+
+class GetResultsInput(MrScraperInput):
+ """Inputs for paginated scraper results."""
+
+ scraper_id: NonBlankStr = Field(
+ description="Required scraper ID whose results to list."
+ )
+ page: StrictInt = Field(
+ default=1, description="Results page number; defaults to 1."
+ )
+ page_size: StrictInt = Field(
+ default=10, description="Number of results per page; defaults to 10."
+ )
+ sort_by: Literal["createdAt"] = Field(
+ default="createdAt", description="Sort field; only 'createdAt' is supported."
+ )
+ sort_order: Literal["ASC", "DESC"] = Field(
+ default="DESC",
+ description="Sort direction, 'ASC' or 'DESC'; defaults to 'DESC'.",
+ )
+
+
+class GetLatestResultsInput(MrScraperInput):
+ """Inputs for the newest scraper results."""
+
+ scraper_id: NonBlankStr = Field(
+ description="Required scraper ID whose newest results to list."
+ )
+ count: StrictInt = Field(
+ default=10, description="Number of newest results; defaults to 10."
+ )
+
+
+class GetResultDetailInput(MrScraperInput):
+ """Inputs for one result record."""
+
+ result_id: NonBlankStr = Field(description="Required result ID to retrieve.")
+
+
+class RunExistingScraperInput(MrScraperInput):
+ """Stable conditional schema for AI and manual single scraper runs."""
+
+ scraper_type: Literal["ai", "manual"] = Field(
+ description="Required scraper kind selecting the AI or manual endpoint."
+ )
+ scraper_id: NonBlankStr = Field(description="Required existing scraper ID.")
+ url: NonBlankStr = Field(description="Required URL to process in this run.")
+ max_retry: NonNegativeInt = Field(
+ default=3, description="Maximum retry attempts; defaults to 3; minimum 0."
+ )
+ proxy_country: TwoLetterCode | None = Field(
+ default=None, description="Optional proxy country code."
+ )
+ agent_type: Literal["general", "listing", "map"] = Field(
+ default="general",
+ description="AI agent type; defaults to 'general' for AI and is forbidden for manual runs.",
+ )
+
+ bypass_proxy: bool | None = Field(
+ default=None,
+ description="General/Listing default false; Manual default true; forbidden for Map.",
+ )
+ html: bool | None = Field(
+ default=None,
+ description="Optional General, Listing, or Manual HTML output flag.",
+ )
+ markdown: bool | None = Field(
+ default=None,
+ description="Optional General, Listing, or Manual Markdown output flag.",
+ )
+ screenshot: bool | None = Field(
+ default=None,
+ description="Optional General, Listing, or Manual screenshot flag.",
+ )
+ stream: bool | None = Field(
+ default=None,
+ description="Optional Listing or Manual streaming flag.",
+ )
+ timeout: PositiveInt | None = Field(
+ default=None,
+ description="Listing timeout defaults to 300; Manual timeout defaults to 600; minimum 1.",
+ )
+
+ render_javascript: bool | None = Field(
+ default=None,
+ description="Optional General/Listing JavaScript rendering flag.",
+ )
+ return_cookies: bool | None = Field(
+ default=None,
+ description="Optional General/Listing cookie-return flag.",
+ )
+ use_home_page: bool | None = Field(
+ default=None,
+ description="Optional General/Listing home-page visit flag.",
+ )
+ wait_for_selector: NonBlankStr | None = Field(
+ default=None, description="Optional General/Listing CSS selector to await."
+ )
+
+ max_pages: PositiveInt | None = Field(
+ default=None,
+ description="Listing defaults to 5; Map defaults to 50; minimum 1.",
+ )
+
+ max_depth: NonNegativeInt | None = Field(
+ default=None, description="Optional Map crawl depth; minimum 0."
+ )
+ limit: PositiveInt | None = Field(
+ default=None, description="Optional Map result limit; minimum 1."
+ )
+ include_patterns: NonBlankStr | None = Field(
+ default=None, description="Optional Map include-pattern expressions."
+ )
+ exclude_patterns: NonBlankStr | None = Field(
+ default=None, description="Optional Map exclude-pattern expressions."
+ )
+
+ cookie_jar: NonBlankStr | None = Field(
+ default=None, description="Optional Manual cookie-jar identifier or value."
+ )
+ cookies: list[dict[str, Any]] | None = Field(
+ default=None,
+ description="Optional Manual browser-cookie objects.",
+ )
+ home_page: bool | None = Field(
+ default=None, description="Optional Manual home-page visit flag."
+ )
+ home_page_timeout: PositiveInt | None = Field(
+ default=None, description="Optional Manual home-page timeout; minimum 1."
+ )
+ paginator: dict[str, Any] | None = Field(
+ default=None,
+ description="Optional Manual paginator configuration.",
+ )
+ proxy: NonBlankStr | None = Field(
+ default=None, description="Optional Manual proxy URL."
+ )
+ record: bool | None = Field(
+ default=None,
+ description="Optional Manual browser-session recording flag.",
+ )
+ return_cookie: bool | None = Field(
+ default=None, description="Optional Manual cookie-return flag."
+ )
+ token_cap: NonNegativeInt | None = Field(
+ default=None, description="Optional Manual token cap; minimum 0."
+ )
+
+ @model_validator(mode="after")
+ def validate_conditional_fields(self) -> RunExistingScraperInput:
+ """Reject explicitly supplied fields that do not apply to the selected run."""
+ supplied = self.model_fields_set
+ common = {"scraper_type", "scraper_id", "url", "max_retry", "proxy_country"}
+ general = {
+ "agent_type",
+ "bypass_proxy",
+ "html",
+ "markdown",
+ "render_javascript",
+ "return_cookies",
+ "screenshot",
+ "use_home_page",
+ "wait_for_selector",
+ }
+ listing = general | {"max_pages", "timeout", "stream"}
+ mapping = {
+ "agent_type",
+ "max_depth",
+ "max_pages",
+ "limit",
+ "include_patterns",
+ "exclude_patterns",
+ }
+ manual = {
+ "bypass_proxy",
+ "cookie_jar",
+ "cookies",
+ "home_page",
+ "home_page_timeout",
+ "html",
+ "markdown",
+ "paginator",
+ "proxy",
+ "record",
+ "return_cookie",
+ "screenshot",
+ "stream",
+ "timeout",
+ "token_cap",
+ }
+
+ if self.scraper_type == "manual":
+ incompatible = supplied - common - manual
+ if incompatible:
+ names = ", ".join(sorted(incompatible))
+ raise ValueError(f"Manual scraper runs do not accept: {names}")
+ return self
+
+ allowed = {
+ "general": general,
+ "listing": listing,
+ "map": mapping,
+ }[self.agent_type]
+ incompatible = supplied - common - allowed
+ if incompatible:
+ names = ", ".join(sorted(incompatible))
+ raise ValueError(
+ f"AI {self.agent_type} scraper runs do not accept: {names}"
+ )
+ return self
+
+
+class RunExistingScraperBatchInput(MrScraperInput):
+ """Inputs for batch runs of an existing scraper."""
+
+ scraper_type: Literal["ai", "manual"] = Field(
+ description="Required scraper kind selecting the AI or manual bulk endpoint."
+ )
+ scraper_id: NonBlankStr = Field(description="Required existing scraper ID.")
+ urls: list[NonBlankStr] = Field(
+ min_length=1,
+ description="Required nonempty array of nonblank URLs to process in this batch.",
+ )
diff --git a/lib/crewai-tools/src/crewai_tools/tools/mrscraper/scraper_creation.py b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/scraper_creation.py
new file mode 100644
index 0000000000..b62ec29394
--- /dev/null
+++ b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/scraper_creation.py
@@ -0,0 +1,126 @@
+"""MrScraper reusable scraper creation tools."""
+
+from typing import Any
+
+from pydantic import BaseModel
+
+from crewai_tools.tools.mrscraper.base import MrScraperBaseTool
+from crewai_tools.tools.mrscraper.payloads import (
+ general_payload,
+ listing_payload,
+ map_payload,
+)
+from crewai_tools.tools.mrscraper.schemas import (
+ GeneralScraperInput,
+ ListingScraperInput,
+ MapScraperInput,
+ ScrapingMode,
+)
+
+
+class MrScraperCreatePromptScraperTool(MrScraperBaseTool):
+ """Create a reusable General AI scraper."""
+
+ name: str = "mrscraper_create_prompt_scraper"
+ description: str = (
+ "Use this to create a reusable General AI scraper from a page, prompt, and optional "
+ "output schema. Use extract_page_by_prompt for immediate one-page extraction intent."
+ )
+ args_schema: type[BaseModel] = GeneralScraperInput
+
+ def _run(
+ self,
+ url: str,
+ prompt: str | None = None,
+ output_schema: dict[str, Any] | None = None,
+ mode: ScrapingMode = "Super",
+ proxy_country: str | None = None,
+ ) -> str:
+ """Create a reusable General AI scraper."""
+ return self._client.request(
+ "POST",
+ "primary",
+ "/api/v1/scrapers-ai",
+ json_body=general_payload(
+ url=url,
+ prompt=prompt,
+ output_schema=output_schema,
+ mode=mode,
+ proxy_country=proxy_country,
+ ),
+ )
+
+
+class MrScraperCreateListingScraperTool(MrScraperBaseTool):
+ """Create a reusable Listing AI scraper."""
+
+ name: str = "mrscraper_create_listing_scraper"
+ description: str = (
+ "Use this to create a reusable Listing AI scraper for repeated or paginated items. "
+ "Use extract_listings when the intent is immediate extraction."
+ )
+ args_schema: type[BaseModel] = ListingScraperInput
+
+ def _run(
+ self,
+ url: str,
+ prompt: str | None = None,
+ output_schema: dict[str, Any] | None = None,
+ max_pages: int = 1,
+ proxy_country: str | None = None,
+ ) -> str:
+ """Create a reusable Listing AI scraper."""
+ return self._client.request(
+ "POST",
+ "primary",
+ "/api/v1/scrapers-ai",
+ json_body=listing_payload(
+ url=url,
+ prompt=prompt,
+ output_schema=output_schema,
+ max_pages=max_pages,
+ proxy_country=proxy_country,
+ ),
+ )
+
+
+class MrScraperCreateWebsiteCrawlScraperTool(MrScraperBaseTool):
+ """Create a reusable Map AI scraper."""
+
+ name: str = "mrscraper_create_website_crawl_scraper"
+ description: str = (
+ "Use this potentially expensive operation to create a reusable Map scraper for URL "
+ "discovery. Use crawl_website_urls for immediate crawl intent."
+ )
+ args_schema: type[BaseModel] = MapScraperInput
+
+ def _run(
+ self,
+ url: str,
+ max_depth: int = 2,
+ max_pages: int = 50,
+ limit: int = 50,
+ include_patterns: str | None = None,
+ exclude_patterns: str | None = None,
+ ) -> str:
+ """Create a reusable Map AI scraper."""
+ return self._client.request(
+ "POST",
+ "primary",
+ "/api/v1/scrapers-ai",
+ json_body=map_payload(
+ url=url,
+ max_depth=max_depth,
+ max_pages=max_pages,
+ limit=limit,
+ include_patterns=include_patterns,
+ exclude_patterns=exclude_patterns,
+ ),
+ )
+
+
+__all__ = [
+ "MrScraperCreateListingScraperTool",
+ "MrScraperCreatePromptScraperTool",
+ "MrScraperCreateWebsiteCrawlScraperTool",
+]
diff --git a/lib/crewai-tools/src/crewai_tools/tools/mrscraper/scraper_runs.py b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/scraper_runs.py
new file mode 100644
index 0000000000..7269f17cf7
--- /dev/null
+++ b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/scraper_runs.py
@@ -0,0 +1,74 @@
+"""MrScraper existing-scraper run tools."""
+
+from typing import Any, Literal
+
+from pydantic import BaseModel
+
+from crewai_tools.tools.mrscraper.base import MrScraperBaseTool
+from crewai_tools.tools.mrscraper.payloads import existing_run_payload
+from crewai_tools.tools.mrscraper.schemas import (
+ RunExistingScraperBatchInput,
+ RunExistingScraperInput,
+)
+
+
+class MrScraperRunExistingScraperTool(MrScraperBaseTool):
+ """Run an existing AI or manual scraper on one URL."""
+
+ name: str = "mrscraper_run_existing_scraper"
+ description: str = (
+ "Use this to run one URL through an existing AI or manual scraper. Choose the AI "
+ "agent type carefully; conditional options are validated before any request."
+ )
+ args_schema: type[BaseModel] = RunExistingScraperInput
+
+ def _run(self, **values: Any) -> str:
+ """Run one validated URL through an existing AI or manual scraper."""
+ scraper_type = values["scraper_type"]
+ endpoint = (
+ "/api/v1/scrapers-manual-rerun"
+ if scraper_type == "manual"
+ else "/api/v1/scrapers-ai-rerun"
+ )
+ return self._client.request(
+ "POST",
+ "primary",
+ endpoint,
+ json_body=existing_run_payload(values),
+ )
+
+
+class MrScraperRunExistingScraperBatchTool(MrScraperBaseTool):
+ """Run an existing AI or manual scraper on a URL batch."""
+
+ name: str = "mrscraper_run_existing_scraper_batch"
+ description: str = (
+ "Use this potentially expensive batch operation to run multiple URLs through one "
+ "existing AI or manual scraper. Use run_existing_scraper for a single URL."
+ )
+ args_schema: type[BaseModel] = RunExistingScraperBatchInput
+
+ def _run(
+ self,
+ scraper_type: Literal["ai", "manual"],
+ scraper_id: str,
+ urls: list[str],
+ ) -> str:
+ """Run a URL batch through an existing AI or manual scraper."""
+ base = (
+ "/api/v1/scrapers-manual-rerun"
+ if scraper_type == "manual"
+ else "/api/v1/scrapers-ai-rerun"
+ )
+ return self._client.request(
+ "POST",
+ "primary",
+ f"{base}/bulk",
+ json_body={"scraperId": scraper_id, "urls": urls},
+ )
+
+
+__all__ = [
+ "MrScraperRunExistingScraperBatchTool",
+ "MrScraperRunExistingScraperTool",
+]
diff --git a/lib/crewai-tools/src/crewai_tools/tools/mrscraper/structured_data_prompts.json b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/structured_data_prompts.json
new file mode 100644
index 0000000000..bf00cccc1e
--- /dev/null
+++ b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/structured_data_prompts.json
@@ -0,0 +1 @@
+{"article":"\nPlease extract the data by following the schema:\n{\n \"fields\": [\n {\n \"name\": \"headline\",\n \"type\": \"string\",\n \"description\": \"Article headline or title\"\n },\n {\n \"name\": \"articleBody\",\n \"type\": \"string\",\n \"description\": \"Full text content of the article\"\n },\n {\n \"name\": \"articleBodyHtml\",\n \"type\": \"string\",\n \"description\": \"HTML markup of the article body\"\n },\n {\n \"name\": \"description\",\n \"type\": \"string\",\n \"description\": \"Short summary or description of the article\"\n },\n {\n \"name\": \"datePublished\",\n \"type\": \"string\",\n \"description\": \"Publication date in ISO 8601 format\"\n },\n {\n \"name\": \"datePublishedRaw\",\n \"type\": \"string\",\n \"description\": \"Publication date as displayed on the page\"\n },\n {\n \"name\": \"dateModified\",\n \"type\": \"string\",\n \"description\": \"Last modified date in ISO 8601 format\"\n },\n {\n \"name\": \"dateModifiedRaw\",\n \"type\": \"string\",\n \"description\": \"Last modified date as displayed on the page\"\n },\n {\n \"name\": \"authors\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"List of article authors\",\n \"item_structure\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Parsed individual author name\"\n },\n \"nameRaw\": {\n \"type\": \"string\",\n \"description\": \"Raw author name as displayed on the page\"\n }\n }\n },\n {\n \"name\": \"inLanguage\",\n \"type\": \"string\",\n \"description\": \"Language code of the article (e.g., en)\"\n },\n {\n \"name\": \"breadcrumbs\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Navigation breadcrumb trail\",\n \"item_structure\": {\n \"url\": {\n \"type\": \"string\",\n \"description\": \"Breadcrumb link URL\"\n },\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Breadcrumb label\"\n }\n }\n },\n {\n \"name\": \"mainImage\",\n \"type\": \"object\",\n \"structure\": {\n \"url\": {\n \"type\": \"string\",\n \"description\": \"URL of the main image\"\n }\n },\n \"description\": \"Primary image of the article\"\n },\n {\n \"name\": \"images\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"All images found in the article\",\n \"item_structure\": {\n \"url\": {\n \"type\": \"string\",\n \"description\": \"Image URL\"\n }\n }\n },\n {\n \"name\": \"videos\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"All videos found in the article\",\n \"item_structure\": {\n \"url\": {\n \"type\": \"string\",\n \"description\": \"Video URL\"\n }\n }\n },\n {\n \"name\": \"audios\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"All audio files found in the article\",\n \"item_structure\": {\n \"url\": {\n \"type\": \"string\",\n \"description\": \"Audio URL\"\n }\n }\n },\n {\n \"name\": \"url\",\n \"type\": \"string\",\n \"description\": \"URL of the article page\"\n },\n {\n \"name\": \"canonicalUrl\",\n \"type\": \"string\",\n \"description\": \"Canonical URL of the article\"\n }\n ]\n}\n","forumThread":"\nPlease extract the data by following the schema:\n{\n \"fields\": [\n {\n \"name\": \"topic\",\n \"type\": \"object\",\n \"structure\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Topic title or question\"\n }\n },\n \"description\": \"Forum thread topic\"\n },\n {\n \"name\": \"posts\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"List of posts in the thread\",\n \"item_structure\": {\n \"text\": {\n \"type\": \"string\",\n \"description\": \"Post text content\"\n },\n \"reactions\": {\n \"type\": \"object\",\n \"structure\": {\n \"likes\": {\n \"type\": \"number\",\n \"description\": \"Number of likes\"\n },\n \"replies\": {\n \"type\": \"number\",\n \"description\": \"Number of replies\"\n }\n },\n \"description\": \"Reaction counts for the post\"\n },\n \"datePublished\": {\n \"type\": \"string\",\n \"description\": \"Post date in ISO 8601 format\"\n },\n \"datePublishedRaw\": {\n \"type\": \"string\",\n \"description\": \"Post date as displayed on the page\"\n }\n }\n },\n {\n \"name\": \"url\",\n \"type\": \"string\",\n \"description\": \"URL of the forum thread\"\n }\n ]\n}\n","hotel":"\nPlease extract the data by following the schema:\n{\n \"fields\": [\n {\n \"name\": \"name\",\n \"type\": \"string\",\n \"description\": \"Hotel or accommodation name\"\n },\n {\n \"name\": \"about\",\n \"type\": \"string\",\n \"description\": \"Description and overview of the property\"\n },\n {\n \"name\": \"address\",\n \"type\": \"string\",\n \"description\": \"Full street address\"\n },\n {\n \"name\": \"location\",\n \"type\": \"string\",\n \"description\": \"City, area, or region name\"\n },\n {\n \"name\": \"coordinates\",\n \"type\": \"object\",\n \"structure\": {\n \"latitude\": {\n \"type\": \"number\",\n \"description\": \"Latitude\"\n },\n \"longitude\": {\n \"type\": \"number\",\n \"description\": \"Longitude\"\n }\n },\n \"description\": \"Geographic coordinates\"\n },\n {\n \"name\": \"stars\",\n \"type\": \"number\",\n \"description\": \"Star rating of the property (1-5)\"\n },\n {\n \"name\": \"year_opened\",\n \"type\": \"number\",\n \"description\": \"Year the property was opened\"\n },\n {\n \"name\": \"check_in\",\n \"type\": \"string\",\n \"description\": \"Check-in time instructions\"\n },\n {\n \"name\": \"check_out\",\n \"type\": \"string\",\n \"description\": \"Check-out time instructions\"\n },\n {\n \"name\": \"contact\",\n \"type\": \"object\",\n \"structure\": {\n \"email\": {\n \"type\": \"string\",\n \"description\": \"Contact email\"\n },\n \"phone\": {\n \"type\": \"string\",\n \"description\": \"Contact phone number\"\n }\n },\n \"description\": \"Property contact information\"\n },\n {\n \"name\": \"host\",\n \"type\": \"object\",\n \"structure\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Host name\"\n },\n \"joined\": {\n \"type\": \"string\",\n \"description\": \"Date joined\"\n },\n \"response_time\": {\n \"type\": \"string\",\n \"description\": \"Average response time\"\n }\n },\n \"description\": \"Host information (for homestays/apartments)\"\n },\n {\n \"name\": \"pricing\",\n \"type\": \"object\",\n \"structure\": {\n \"currency\": {\n \"type\": \"string\",\n \"description\": \"Currency code\"\n },\n \"original_price\": {\n \"type\": \"string\",\n \"description\": \"Price before discount\"\n },\n \"starting_price\": {\n \"type\": \"string\",\n \"description\": \"Lowest price available\"\n },\n \"discounted_price\": {\n \"type\": \"string\",\n \"description\": \"Price after discount\"\n }\n },\n \"description\": \"General pricing info\"\n },\n {\n \"name\": \"review_stats\",\n \"type\": \"object\",\n \"structure\": {\n \"overall_score\": {\n \"type\": \"number\",\n \"description\": \"Overall numeric rating\"\n },\n \"total_reviews\": {\n \"type\": \"number\",\n \"description\": \"Total count of reviews\"\n },\n \"score_breakdown\": {\n \"type\": \"object\",\n \"structure\": {},\n \"description\": \"Scores by category\"\n },\n \"category_summaries\": {\n \"type\": \"object\",\n \"structure\": {},\n \"description\": \"Textual summary of reviews by category\"\n }\n },\n \"description\": \"Aggregated review statistics\"\n },\n {\n \"name\": \"highlights\",\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Key selling points or property highlights\"\n },\n {\n \"name\": \"review_highlights\",\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Snippets of positive feedback from reviews\"\n },\n {\n \"name\": \"reviews\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Individual guest reviews\",\n \"item_structure\": {\n \"date\": {\n \"type\": \"string\",\n \"description\": \"Date of stay or review\"\n },\n \"title\": {\n \"type\": \"string\",\n \"description\": \"Review title\"\n },\n \"rating\": {\n \"type\": \"number\",\n \"description\": \"Rating given\"\n },\n \"content\": {\n \"type\": \"string\",\n \"description\": \"Review content\"\n },\n \"country\": {\n \"type\": \"string\",\n \"description\": \"Reviewer'''s country\"\n },\n \"reviewer\": {\n \"type\": \"string\",\n \"description\": \"Name of reviewer\"\n },\n \"room_type\": {\n \"type\": \"string\",\n \"description\": \"Room type stayed in\"\n },\n \"trip_type\": {\n \"type\": \"string\",\n \"description\": \"Type of trip (Business, Couple, etc.)\"\n },\n \"stay_duration\": {\n \"type\": \"string\",\n \"description\": \"Duration of stay\"\n }\n }\n },\n {\n \"name\": \"popular_facilities\",\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Flat list of popular facilities or amenities\"\n },\n {\n \"name\": \"facilities\",\n \"type\": \"object\",\n \"structure\": {\n \"dining\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Dining options\"\n },\n \"internet\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Internet amenities\"\n },\n \"services\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Hotel services\"\n },\n \"recreation\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Recreation and sports\"\n },\n \"room_amenities\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"In-room features\"\n },\n \"transportation\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Transport facilities\"\n },\n \"access_and_security\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Security features\"\n },\n \"cleanliness_and_safety\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Sanitation protocols\"\n }\n },\n \"description\": \"Facilities grouped by category\"\n },\n {\n \"name\": \"policies\",\n \"type\": \"object\",\n \"structure\": {\n \"pets\": {\n \"type\": \"string\",\n \"description\": \"Pet policy\"\n },\n \"deposit\": {\n \"type\": \"string\",\n \"description\": \"Deposit requirements\"\n },\n \"smoking\": {\n \"type\": \"string\",\n \"description\": \"Smoking policy\"\n },\n \"children\": {\n \"type\": \"string\",\n \"description\": \"Child policy\"\n },\n \"breakfast\": {\n \"type\": \"string\",\n \"description\": \"Breakfast availability/hours\"\n },\n \"cancellation\": {\n \"type\": \"string\",\n \"description\": \"Cancellation rules\"\n },\n \"check_in_instructions\": {\n \"type\": \"string\",\n \"description\": \"Detailed check-in rules\"\n }\n },\n \"description\": \"Property rules and policies\"\n },\n {\n \"name\": \"room_types\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Available room configurations\",\n \"item_structure\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Room type name\"\n },\n \"size\": {\n \"type\": \"string\",\n \"description\": \"Room dimensions\"\n },\n \"prices\": {\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Price options\",\n \"item_structure\": {\n \"kids\": {\n \"type\": \"number\",\n \"description\": \"Child capacity\"\n },\n \"adults\": {\n \"type\": \"number\",\n \"description\": \"Adult capacity\"\n },\n \"cashback\": {\n \"type\": \"string\",\n \"description\": \"Potential cashback\"\n },\n \"price_total\": {\n \"type\": \"string\",\n \"description\": \"Final price\"\n },\n \"price_breakdown\": {\n \"type\": \"string\",\n \"description\": \"Price before tax/fees\"\n },\n \"breakfast_included\": {\n \"type\": \"boolean\",\n \"description\": \"Is breakfast included\"\n },\n \"cancellation_policy\": {\n \"type\": \"string\",\n \"description\": \"Cancellation terms for this price\"\n }\n }\n },\n \"bed_type\": {\n \"type\": \"string\",\n \"description\": \"Bed configuration\"\n },\n \"features\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"List of room features\"\n }\n }\n },\n {\n \"name\": \"location_nearby\",\n \"type\": \"object\",\n \"structure\": {\n \"airports\": {\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"item_structure\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"distance\": {\n \"type\": \"string\"\n }\n }\n },\n \"culinary\": {\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"item_structure\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"distance\": {\n \"type\": \"string\"\n }\n }\n },\n \"shopping\": {\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"item_structure\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"distance\": {\n \"type\": \"string\"\n }\n }\n },\n \"hospitals\": {\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"item_structure\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"distance\": {\n \"type\": \"string\"\n }\n }\n },\n \"landmarks\": {\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"item_structure\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"distance\": {\n \"type\": \"string\"\n }\n }\n },\n \"attractions\": {\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"item_structure\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"type\": {\n \"type\": \"string\"\n },\n \"distance\": {\n \"type\": \"string\"\n }\n }\n },\n \"cash_withdrawal\": {\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"item_structure\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"distance\": {\n \"type\": \"string\"\n }\n }\n },\n \"public_transport\": {\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"item_structure\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"distance\": {\n \"type\": \"string\"\n }\n }\n },\n \"convenience_stores\": {\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"item_structure\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"distance\": {\n \"type\": \"string\"\n }\n }\n }\n },\n \"description\": \"Points of interest near the property\"\n }\n ]\n}\n","jobPosting":"\nPlease extract the data by following the schema:\n{\n \"fields\": [\n {\n \"name\": \"jobTitle\",\n \"type\": \"string\",\n \"description\": \"Title of the job posting\"\n },\n {\n \"name\": \"datePublished\",\n \"type\": \"string\",\n \"description\": \"Publication date in ISO 8601 format\"\n },\n {\n \"name\": \"datePublishedRaw\",\n \"type\": \"string\",\n \"description\": \"Publication date as displayed on the page\"\n },\n {\n \"name\": \"validThrough\",\n \"type\": \"string\",\n \"description\": \"Expiration date of the job posting in ISO 8601 format\"\n },\n {\n \"name\": \"description\",\n \"type\": \"string\",\n \"description\": \"Full job description text\"\n },\n {\n \"name\": \"descriptionHtml\",\n \"type\": \"string\",\n \"description\": \"HTML markup of the job description\"\n },\n {\n \"name\": \"employmentType\",\n \"type\": \"string\",\n \"description\": \"Type of employment (e.g., Full-time, Part-time)\"\n },\n {\n \"name\": \"hiringOrganization\",\n \"type\": \"object\",\n \"structure\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Company or organization name\"\n }\n },\n \"description\": \"Organization offering the job\"\n },\n {\n \"name\": \"baseSalary\",\n \"type\": \"object\",\n \"structure\": {\n \"raw\": {\n \"type\": \"string\",\n \"description\": \"Raw salary string as displayed\"\n },\n \"currency\": {\n \"type\": \"string\",\n \"description\": \"ISO 4217 currency code\"\n },\n \"valueMax\": {\n \"type\": \"string\",\n \"description\": \"Maximum salary value\"\n },\n \"currencyRaw\": {\n \"type\": \"string\",\n \"description\": \"Currency symbol as displayed\"\n }\n },\n \"description\": \"Salary information for the job\"\n },\n {\n \"name\": \"jobLocation\",\n \"type\": \"object\",\n \"structure\": {\n \"raw\": {\n \"type\": \"string\",\n \"description\": \"Raw location string as displayed\"\n }\n },\n \"description\": \"Location of the job\"\n },\n {\n \"name\": \"url\",\n \"type\": \"string\",\n \"description\": \"URL of the job posting page\"\n }\n ]\n}\n","post":"\nPlease extract the data by following the schema:\n{\n \"fields\": [\n {\n \"name\": \"id\",\n \"type\": \"string\",\n \"description\": \"Unique post identifier\"\n },\n {\n \"name\": \"platform\",\n \"type\": \"string\",\n \"description\": \"Social media platform (e.g., Instagram, Twitter, Facebook, LinkedIn, TikTok)\"\n },\n {\n \"name\": \"post_type\",\n \"type\": \"string\",\n \"description\": \"Type of post (text, photo, video, reel, story, live, poll, event, article, link)\"\n },\n {\n \"name\": \"status\",\n \"type\": \"string\",\n \"description\": \"Post status (published, draft, scheduled, archived, deleted)\"\n },\n {\n \"name\": \"content\",\n \"type\": \"object\",\n \"structure\": {\n \"text\": {\n \"type\": \"string\",\n \"description\": \"Main post text/caption\"\n },\n \"links\": {\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"URLs in the post\",\n \"item_structure\": {\n \"url\": {\n \"type\": \"string\",\n \"description\": \"Link URL\"\n },\n \"image\": {\n \"type\": \"string\",\n \"description\": \"Link preview image URL\"\n },\n \"title\": {\n \"type\": \"string\",\n \"description\": \"Link preview title\"\n },\n \"domain\": {\n \"type\": \"string\",\n \"description\": \"Domain of the link\"\n },\n \"description\": {\n \"type\": \"string\",\n \"description\": \"Link preview description\"\n }\n }\n },\n \"title\": {\n \"type\": \"string\",\n \"description\": \"Post title or headline\"\n },\n \"hashtags\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Hashtags used in the post\"\n },\n \"language\": {\n \"type\": \"string\",\n \"description\": \"Content language code (e.g., en, es, fr)\"\n },\n \"mentions\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"User mentions (@username)\"\n }\n },\n \"description\": \"Post content details\"\n },\n {\n \"name\": \"author\",\n \"type\": \"object\",\n \"structure\": {\n \"id\": {\n \"type\": \"string\",\n \"description\": \"Author user ID\"\n },\n \"username\": {\n \"type\": \"string\",\n \"description\": \"Author username/handle\"\n },\n \"verified\": {\n \"type\": \"boolean\",\n \"description\": \"Is the author verified\"\n },\n \"avatar_url\": {\n \"type\": \"string\",\n \"description\": \"Author profile picture URL\"\n },\n \"profile_url\": {\n \"type\": \"string\",\n \"description\": \"Link to author profile\"\n },\n \"display_name\": {\n \"type\": \"string\",\n \"description\": \"Author display name\"\n },\n \"follower_count\": {\n \"type\": \"number\",\n \"description\": \"Author'''s follower count at time of post\"\n }\n },\n \"description\": \"Post author information\"\n },\n {\n \"name\": \"media\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Media attachments (photos, videos, etc.)\",\n \"item_structure\": {\n \"id\": {\n \"type\": \"string\",\n \"description\": \"Media file ID\"\n },\n \"url\": {\n \"type\": \"string\",\n \"description\": \"Media URL\"\n },\n \"type\": {\n \"type\": \"string\",\n \"description\": \"Media type (image, video, gif, audio)\"\n },\n \"order\": {\n \"type\": \"number\",\n \"description\": \"Display order in carousel\"\n },\n \"width\": {\n \"type\": \"number\",\n \"description\": \"Media width in pixels\"\n },\n \"height\": {\n \"type\": \"number\",\n \"description\": \"Media height in pixels\"\n },\n \"alt_text\": {\n \"type\": \"string\",\n \"description\": \"Accessibility description\"\n },\n \"duration\": {\n \"type\": \"number\",\n \"description\": \"Video/audio duration in seconds\"\n },\n \"file_size\": {\n \"type\": \"number\",\n \"description\": \"File size in bytes\"\n },\n \"mime_type\": {\n \"type\": \"string\",\n \"description\": \"MIME type (e.g., image/jpeg, video/mp4)\"\n },\n \"thumbnail_url\": {\n \"type\": \"string\",\n \"description\": \"Thumbnail/preview image URL\"\n }\n }\n },\n {\n \"name\": \"location\",\n \"type\": \"object\",\n \"structure\": {\n \"city\": {\n \"type\": \"string\",\n \"description\": \"City name\"\n },\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Location name/venue\"\n },\n \"state\": {\n \"type\": \"string\",\n \"description\": \"State/province\"\n },\n \"address\": {\n \"type\": \"string\",\n \"description\": \"Full address\"\n },\n \"country\": {\n \"type\": \"string\",\n \"description\": \"Country name\"\n },\n \"latitude\": {\n \"type\": \"number\",\n \"description\": \"Geographic latitude\"\n },\n \"place_id\": {\n \"type\": \"string\",\n \"description\": \"Platform-specific place ID\"\n },\n \"zip_code\": {\n \"type\": \"string\",\n \"description\": \"Postal/ZIP code\"\n },\n \"longitude\": {\n \"type\": \"number\",\n \"description\": \"Geographic longitude\"\n },\n \"country_code\": {\n \"type\": \"string\",\n \"description\": \"Country code (ISO 3166-1 alpha-2)\"\n }\n },\n \"description\": \"Geographic location data\"\n },\n {\n \"name\": \"engagement\",\n \"type\": \"object\",\n \"structure\": {\n \"reactions\": {\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Detailed reaction breakdown\",\n \"item_structure\": {\n \"type\": {\n \"type\": \"string\",\n \"description\": \"Reaction type (like, love, haha, wow, sad, angry)\"\n },\n \"count\": {\n \"type\": \"number\",\n \"description\": \"Number of reactions of this type\"\n }\n }\n },\n \"likes_count\": {\n \"type\": \"number\",\n \"description\": \"Number of likes\"\n },\n \"saves_count\": {\n \"type\": \"number\",\n \"description\": \"Number of saves/bookmarks\"\n },\n \"views_count\": {\n \"type\": \"number\",\n \"description\": \"Number of views/impressions\"\n },\n \"clicks_count\": {\n \"type\": \"number\",\n \"description\": \"Number of link clicks\"\n },\n \"shares_count\": {\n \"type\": \"number\",\n \"description\": \"Number of shares/reposts\"\n },\n \"comments_count\": {\n \"type\": \"number\",\n \"description\": \"Number of comments\"\n },\n \"dislikes_count\": {\n \"type\": \"number\",\n \"description\": \"Number of dislikes (if applicable)\"\n }\n },\n \"description\": \"Engagement metrics\"\n },\n {\n \"name\": \"comments\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Top comments on the post\",\n \"item_structure\": {\n \"id\": {\n \"type\": \"string\",\n \"description\": \"Comment ID\"\n },\n \"text\": {\n \"type\": \"string\",\n \"description\": \"Comment text\"\n },\n \"author\": {\n \"type\": \"object\",\n \"structure\": {\n \"id\": {\n \"type\": \"string\",\n \"description\": \"Author user ID\"\n },\n \"username\": {\n \"type\": \"string\",\n \"description\": \"Author username\"\n },\n \"verified\": {\n \"type\": \"boolean\",\n \"description\": \"Is author verified\"\n },\n \"avatar_url\": {\n \"type\": \"string\",\n \"description\": \"Author avatar URL\"\n },\n \"display_name\": {\n \"type\": \"string\",\n \"description\": \"Author display name\"\n }\n },\n \"description\": \"Comment author info\"\n },\n \"is_pinned\": {\n \"type\": \"boolean\",\n \"description\": \"Is comment pinned by author\"\n },\n \"created_at\": {\n \"type\": \"string\",\n \"description\": \"Comment timestamp\"\n },\n \"likes_count\": {\n \"type\": \"number\",\n \"description\": \"Comment likes\"\n },\n \"replies_count\": {\n \"type\": \"number\",\n \"description\": \"Number of replies to this comment\"\n },\n \"is_liked_by_author\": {\n \"type\": \"boolean\",\n \"description\": \"Is comment liked by post author\"\n }\n }\n },\n {\n \"name\": \"timestamps\",\n \"type\": \"object\",\n \"structure\": {\n \"edited\": {\n \"type\": \"boolean\",\n \"description\": \"Has post been edited\"\n },\n \"created_at\": {\n \"type\": \"string\",\n \"description\": \"When post was created\"\n },\n \"expires_at\": {\n \"type\": \"string\",\n \"description\": \"Story/expiration time\"\n },\n \"updated_at\": {\n \"type\": \"string\",\n \"description\": \"When post was last edited\"\n },\n \"published_at\": {\n \"type\": \"string\",\n \"description\": \"When post was published\"\n },\n \"scheduled_for\": {\n \"type\": \"string\",\n \"description\": \"Scheduled publish time\"\n }\n },\n \"description\": \"Post timing information\"\n },\n {\n \"name\": \"tags\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"People, products, and other tags in the post\",\n \"item_structure\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Tag name/label\"\n },\n \"type\": {\n \"type\": \"string\",\n \"description\": \"Tag type (person, product, location, topic)\"\n },\n \"user_id\": {\n \"type\": \"string\",\n \"description\": \"Tagged user ID (if applicable)\"\n },\n \"position_x\": {\n \"type\": \"number\",\n \"description\": \"X position in image (0-1)\"\n },\n \"position_y\": {\n \"type\": \"number\",\n \"description\": \"Y position in image (0-1)\"\n }\n }\n },\n {\n \"name\": \"audio\",\n \"type\": \"object\",\n \"structure\": {\n \"id\": {\n \"type\": \"string\",\n \"description\": \"Audio track ID\"\n },\n \"url\": {\n \"type\": \"string\",\n \"description\": \"Audio preview URL\"\n },\n \"album\": {\n \"type\": \"string\",\n \"description\": \"Album name\"\n },\n \"title\": {\n \"type\": \"string\",\n \"description\": \"Song/audio title\"\n },\n \"artist\": {\n \"type\": \"string\",\n \"description\": \"Artist name\"\n },\n \"duration\": {\n \"type\": \"number\",\n \"description\": \"Audio duration in seconds\"\n },\n \"provider\": {\n \"type\": \"string\",\n \"description\": \"Audio provider (Spotify, Apple Music, etc.)\"\n },\n \"start_time\": {\n \"type\": \"number\",\n \"description\": \"Start time in audio (for trimmed clips)\"\n },\n \"original_audio\": {\n \"type\": \"boolean\",\n \"description\": \"Is original user-created audio\"\n }\n },\n \"description\": \"Audio information (for video posts with music/sound)\"\n },\n {\n \"name\": \"collaboration\",\n \"type\": \"object\",\n \"structure\": {\n \"collaborators\": {\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Co-authors\",\n \"item_structure\": {\n \"id\": {\n \"type\": \"string\",\n \"description\": \"Collaborator user ID\"\n },\n \"username\": {\n \"type\": \"string\",\n \"description\": \"Collaborator username\"\n },\n \"avatar_url\": {\n \"type\": \"string\",\n \"description\": \"Collaborator avatar URL\"\n },\n \"display_name\": {\n \"type\": \"string\",\n \"description\": \"Collaborator display name\"\n }\n }\n },\n \"invited_users\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Users invited to collaborate\"\n },\n \"is_collaborative\": {\n \"type\": \"boolean\",\n \"description\": \"Is this a collaborative post\"\n }\n },\n \"description\": \"Collaborative post details\"\n },\n {\n \"name\": \"parent_post\",\n \"type\": \"object\",\n \"structure\": {\n \"id\": {\n \"type\": \"string\",\n \"description\": \"Parent post ID\"\n },\n \"type\": {\n \"type\": \"string\",\n \"description\": \"Relationship type (reply, repost, quote, thread)\"\n },\n \"author\": {\n \"type\": \"object\",\n \"structure\": {\n \"id\": {\n \"type\": \"string\",\n \"description\": \"Author user ID\"\n },\n \"username\": {\n \"type\": \"string\",\n \"description\": \"Author username\"\n },\n \"display_name\": {\n \"type\": \"string\",\n \"description\": \"Author display name\"\n }\n },\n \"description\": \"Parent post author\"\n }\n },\n \"description\": \"Parent post info (for replies, reposts, quotes)\"\n },\n {\n \"name\": \"more_posts\",\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"More posts URLs\"\n },\n {\n \"name\": \"metadata\",\n \"type\": \"object\",\n \"structure\": {\n \"post_url\": {\n \"type\": \"string\",\n \"description\": \"Direct URL to the post\"\n },\n \"embed_url\": {\n \"type\": \"string\",\n \"description\": \"Embed iframe URL\"\n },\n \"shortcode\": {\n \"type\": \"string\",\n \"description\": \"Platform shortcode for sharing\"\n },\n \"source_app\": {\n \"type\": \"string\",\n \"description\": \"App used to create post\"\n },\n \"created_via\": {\n \"type\": \"string\",\n \"description\": \"Method of creation (web, mobile, api, third_party)\"\n },\n \"auto_translated\": {\n \"type\": \"boolean\",\n \"description\": \"Was content auto-translated\"\n },\n \"translation_source\": {\n \"type\": \"string\",\n \"description\": \"Original language if translated\"\n }\n },\n \"description\": \"Additional metadata\"\n }\n ]\n}\n","product":"\nPlease extract the data by following the schema:\n{\n \"fields\": [\n {\n \"name\": \"product_info\",\n \"type\": \"object\",\n \"structure\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Full product title\"\n },\n \"brand\": {\n \"type\": \"string\",\n \"description\": \"Normalized brand name\"\n },\n \"category\": {\n \"type\": \"string\",\n \"description\": \"Category name\"\n },\n \"condition\": {\n \"type\": \"string\",\n \"description\": \"New, Used, Refurbished\"\n },\n \"breadcrumbs\": {\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Navigation breadcrumb trail\",\n \"item_structure\": {\n \"url\": {\n \"type\": \"string\",\n \"description\": \"Breadcrumb link URL\"\n },\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Breadcrumb label\"\n }\n }\n }\n },\n \"description\": \"Core product details\"\n },\n {\n \"name\": \"sku\",\n \"type\": \"string\",\n \"description\": \"Stock Keeping Unit identifier\"\n },\n {\n \"name\": \"mpn\",\n \"type\": \"string\",\n \"description\": \"Manufacturer Part Number\"\n },\n {\n \"name\": \"gtin\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Global Trade Item Numbers\",\n \"item_structure\": {\n \"type\": {\n \"type\": \"string\",\n \"description\": \"GTIN type (e.g., isbn13, ean13, upc)\"\n },\n \"value\": {\n \"type\": \"number\",\n \"description\": \"GTIN value\"\n }\n }\n },\n {\n \"name\": \"price\",\n \"type\": \"object\",\n \"structure\": {\n \"currency\": {\n \"type\": \"string\",\n \"description\": \"ISO 4217 code (e.g., USD, IDR)\"\n },\n \"current_price\": {\n \"type\": \"number\",\n \"description\": \"Active selling price\"\n },\n \"is_flash_sale\": {\n \"type\": \"boolean\",\n \"description\": \"If currently in a time-limited deal\"\n },\n \"original_price\": {\n \"type\": \"number\",\n \"description\": \"MSRP or price before discount\"\n },\n \"currency_symbol\": {\n \"type\": \"string\",\n \"description\": \"Display symbol (e.g., $, Rp)\"\n },\n \"discount_percentage\": {\n \"type\": \"number\",\n \"description\": \"Calculated off percentage\"\n }\n },\n \"description\": \"Price, currency, and discounts\"\n },\n {\n \"name\": \"wholesale_tiers\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Bulk pricing (common in Tokopedia/Shopee)\",\n \"item_structure\": {\n \"min_qty\": {\n \"type\": \"number\",\n \"description\": \"Minimum quantity\"\n },\n \"price_per_unit\": {\n \"type\": \"number\",\n \"description\": \"Discounted unit price\"\n }\n }\n },\n {\n \"name\": \"stock_status\",\n \"type\": \"object\",\n \"structure\": {\n \"status\": {\n \"type\": \"string\",\n \"description\": \"e.g., in_stock, out_of_stock, pre_order\"\n },\n \"quantity\": {\n \"type\": \"number\",\n \"description\": \"Exact stock count if available\"\n }\n },\n \"description\": \"Inventory availability\"\n },\n {\n \"name\": \"media\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Images and videos\",\n \"item_structure\": {\n \"url\": {\n \"type\": \"string\",\n \"description\": \"Resource URL\"\n },\n \"type\": {\n \"type\": \"string\",\n \"description\": \"'''image''' or '''video'''\"\n },\n \"is_thumbnail\": {\n \"type\": \"boolean\",\n \"description\": \"True if main display image\"\n }\n }\n },\n {\n \"name\": \"description\",\n \"type\": \"object\",\n \"structure\": {\n \"full_text\": {\n \"type\": \"string\",\n \"description\": \"Plain text description\"\n },\n \"html_content\": {\n \"type\": \"string\",\n \"description\": \"Raw HTML if preserved\"\n },\n \"key_features\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Bullet points/Highlights\"\n }\n },\n \"description\": \"Product text content\"\n },\n {\n \"name\": \"specifications\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Technical specs (Material, Dimensions, Tech Specs)\",\n \"item_structure\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Attribute name (e.g., Material)\"\n },\n \"value\": {\n \"type\": \"string\",\n \"description\": \"Attribute value (e.g., Cotton)\"\n }\n }\n },\n {\n \"name\": \"variants\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Product variants (e.g., different colors, sizes)\",\n \"item_structure\": {\n \"mpn\": {\n \"type\": \"string\",\n \"description\": \"Manufacturer Part Number\"\n },\n \"sku\": {\n \"type\": \"string\",\n \"description\": \"Stock Keeping Unit identifier\"\n },\n \"url\": {\n \"type\": \"string\",\n \"description\": \"URL of the variant\"\n },\n \"gtin\": {\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Global Trade Item Numbers\",\n \"item_structure\": {\n \"type\": {\n \"type\": \"string\",\n \"description\": \"GTIN type\"\n },\n \"value\": {\n \"type\": \"number\",\n \"description\": \"GTIN value\"\n }\n }\n },\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Variant name\"\n },\n \"media\": {\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Images and videos\",\n \"item_structure\": {\n \"url\": {\n \"type\": \"string\",\n \"description\": \"Resource URL\"\n },\n \"type\": {\n \"type\": \"string\",\n \"description\": \"'''image''' or '''video'''\"\n }\n }\n },\n \"price\": {\n \"type\": \"object\",\n \"structure\": {\n \"currency\": {\n \"type\": \"string\",\n \"description\": \"ISO 4217 code (e.g., USD, IDR)\"\n },\n \"current_price\": {\n \"type\": \"number\",\n \"description\": \"Active selling price\"\n },\n \"original_price\": {\n \"type\": \"number\",\n \"description\": \"Price before discount\"\n },\n \"currency_symbol\": {\n \"type\": \"string\",\n \"description\": \"Display symbol (e.g., $, Rp)\"\n },\n \"discount_percentage\": {\n \"type\": \"number\",\n \"description\": \"Calculated off percentage\"\n }\n },\n \"description\": \"Price, currency, and discounts of the variant\"\n },\n \"option\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Variant option (array of string)\"\n },\n \"product_url\": {\n \"type\": \"string\",\n \"description\": \"URL of the variant\"\n },\n \"stock_status\": {\n \"type\": \"object\",\n \"structure\": {\n \"status\": {\n \"type\": \"string\",\n \"description\": \"e.g., in_stock, out_of_stock, pre_order\"\n },\n \"quantity\": {\n \"type\": \"number\",\n \"description\": \"Exact stock count if available\"\n }\n },\n \"description\": \"Inventory availability\"\n },\n \"additionalProperties\": {\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Additional variant properties\",\n \"item_structure\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Property name\"\n },\n \"value\": {\n \"type\": \"string\",\n \"description\": \"Property value\"\n }\n }\n }\n }\n },\n {\n \"name\": \"seller\",\n \"type\": \"object\",\n \"structure\": {\n \"id\": {\n \"type\": \"string\",\n \"description\": \"Store ID\"\n },\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Store name\"\n },\n \"type\": {\n \"type\": \"string\",\n \"description\": \"e.g., Official Store, Power Merchant, Mall\"\n },\n \"badges\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Trust badges\"\n },\n \"rating\": {\n \"type\": \"number\",\n \"description\": \"Seller aggregate rating (0-5)\"\n },\n \"location\": {\n \"type\": \"string\",\n \"description\": \"City/Region of origin\"\n },\n \"sold_count\": {\n \"type\": \"number\",\n \"description\": \"Total units sold by the seller/store\"\n },\n \"review_count\": {\n \"type\": \"number\",\n \"description\": \"Total number of reviews received by the seller\"\n },\n \"product_count\": {\n \"type\": \"number\",\n \"description\": \"Total number of products listed by the seller/store\"\n }\n },\n \"description\": \"Merchant information\"\n },\n {\n \"name\": \"rating_summary\",\n \"type\": \"object\",\n \"structure\": {\n \"sold_count\": {\n \"type\": \"number\",\n \"description\": \"Total units sold\"\n },\n \"rating_count\": {\n \"type\": \"number\",\n \"description\": \"Total number of ratings\"\n },\n \"review_count\": {\n \"type\": \"number\",\n \"description\": \"Total textual/media reviews\"\n },\n \"average_rating\": {\n \"type\": \"number\",\n \"description\": \"0-5 scale\"\n }\n },\n \"description\": \"Product reviews\"\n },\n {\n \"name\": \"rating_distribution\",\n \"type\": \"object\",\n \"structure\": {\n \"1\": {\n \"type\": \"object\",\n \"structure\": {\n \"count\": {\n \"type\": \"number\",\n \"description\": \"Number of 1-star ratings\"\n },\n \"percentage\": {\n \"type\": \"number\",\n \"description\": \"Percentage of 1-star ratings\"\n }\n }\n },\n \"2\": {\n \"type\": \"object\",\n \"structure\": {\n \"count\": {\n \"type\": \"number\",\n \"description\": \"Number of 2-star ratings\"\n },\n \"percentage\": {\n \"type\": \"number\",\n \"description\": \"Percentage of 2-star ratings\"\n }\n }\n },\n \"3\": {\n \"type\": \"object\",\n \"structure\": {\n \"count\": {\n \"type\": \"number\",\n \"description\": \"Number of 3-star ratings\"\n },\n \"percentage\": {\n \"type\": \"number\",\n \"description\": \"Percentage of 3-star ratings\"\n }\n }\n },\n \"4\": {\n \"type\": \"object\",\n \"structure\": {\n \"count\": {\n \"type\": \"number\",\n \"description\": \"Number of 4-star ratings\"\n },\n \"percentage\": {\n \"type\": \"number\",\n \"description\": \"Percentage of 4-star ratings\"\n }\n }\n },\n \"5\": {\n \"type\": \"object\",\n \"structure\": {\n \"count\": {\n \"type\": \"number\",\n \"description\": \"Number of 5-star ratings\"\n },\n \"percentage\": {\n \"type\": \"number\",\n \"description\": \"Percentage of 5-star ratings\"\n }\n }\n }\n },\n \"description\": \"Rating distribution\"\n },\n {\n \"name\": \"shipping_info\",\n \"type\": \"object\",\n \"structure\": {\n \"weight\": {\n \"type\": \"object\",\n \"structure\": {\n \"unit\": \"string\",\n \"value\": \"number\"\n }\n },\n \"dimensions\": {\n \"type\": \"string\",\n \"description\": \"LxWxH format\"\n },\n \"free_shipping\": {\n \"type\": \"boolean\",\n \"description\": \"Is free shipping available?\"\n },\n \"fulfillment_type\": {\n \"type\": \"string\",\n \"description\": \"e.g., Pickup, Standard, Express\"\n }\n },\n \"description\": \"Logistics details\"\n },\n {\n \"name\": \"warranty_and_returns\",\n \"type\": \"object\",\n \"structure\": {\n \"has_warranty\": {\n \"type\": \"boolean\",\n \"description\": \"True/False\"\n },\n \"warranty_type\": {\n \"type\": \"string\",\n \"description\": \"e.g., Manufacturer, Seller\"\n },\n \"warranty_period\": {\n \"type\": \"string\",\n \"description\": \"e.g., 1 Year, 7 Days\"\n },\n \"return_policy_text\": {\n \"type\": \"string\",\n \"description\": \"Brief return conditions\"\n }\n },\n \"description\": \"Post-purchase policies\"\n }\n ]\n}\n","property":"\nPlease extract the data by following the schema:\n{\n \"fields\": [\n {\n \"name\": \"title\",\n \"type\": \"string\",\n \"description\": \"Listing title or marketing headline (e.g., '''Castillo Caribe''')\"\n },\n {\n \"name\": \"mls_number\",\n \"type\": \"string\",\n \"description\": \"Multiple Listing Service identifier (MLS ID/Number)\"\n },\n {\n \"name\": \"status\",\n \"type\": \"string\",\n \"description\": \"Current listing status (e.g., For Sale, Active, Pending, Sold, Current, New, etc.)\"\n },\n {\n \"name\": \"price\",\n \"type\": \"object\",\n \"structure\": {\n \"amount\": {\n \"type\": \"string\",\n \"description\": \"Listing price value\"\n },\n \"currency\": {\n \"type\": \"string\",\n \"description\": \"Currency code (e.g., USD, CAD)\"\n },\n \"display_price\": {\n \"type\": \"string\",\n \"description\": \"Formatted price string (e.g., $1,250,000)\"\n },\n \"price_per_sqft\": {\n \"type\": \"string\",\n \"description\": \"Price per square foot\"\n }\n },\n \"description\": \"Price information\"\n },\n {\n \"name\": \"address\",\n \"type\": \"object\",\n \"structure\": {\n \"city\": {\n \"type\": \"string\",\n \"description\": \"City or locality\"\n },\n \"unit\": {\n \"type\": \"string\",\n \"description\": \"Apartment or unit number\"\n },\n \"state\": {\n \"type\": \"string\",\n \"description\": \"State, region, or province\"\n },\n \"county\": {\n \"type\": \"string\",\n \"description\": \"County or parish\"\n },\n \"island\": {\n \"type\": \"string\",\n \"description\": \"Island name (for Caribbean/island properties)\"\n },\n \"street\": {\n \"type\": \"string\",\n \"description\": \"Street number and name\"\n },\n \"country\": {\n \"type\": \"string\",\n \"description\": \"Country code or name\"\n },\n \"latitude\": {\n \"type\": \"number\",\n \"description\": \"Geographic latitude\"\n },\n \"zip_code\": {\n \"type\": \"string\",\n \"description\": \"Postal or ZIP code\"\n },\n \"longitude\": {\n \"type\": \"number\",\n \"description\": \"Geographic longitude\"\n },\n \"maps_link\": {\n \"type\": \"string\",\n \"description\": \"URL to map location\"\n },\n \"full_address\": {\n \"type\": \"string\",\n \"description\": \"Full formatted address\"\n }\n },\n \"description\": \"Location details\"\n },\n {\n \"name\": \"property_details\",\n \"type\": \"object\",\n \"structure\": {\n \"sq_ft\": {\n \"type\": \"number\",\n \"description\": \"Total living area in square feet\"\n },\n \"stories\": {\n \"type\": \"number\",\n \"description\": \"Number of stories/levels\"\n },\n \"bedrooms\": {\n \"type\": \"number\",\n \"description\": \"Total number of bedrooms\"\n },\n \"bathrooms\": {\n \"type\": \"number\",\n \"description\": \"Total number of bathrooms\"\n },\n \"furnished\": {\n \"type\": \"boolean\",\n \"description\": \"Is the property furnished\"\n },\n \"year_built\": {\n \"type\": \"number\",\n \"description\": \"Year of construction\"\n },\n \"property_type\": {\n \"type\": \"string\",\n \"description\": \"Type of property (e.g., Single Family, Condo)\"\n },\n \"bathrooms_full\": {\n \"type\": \"number\",\n \"description\": \"Number of full bathrooms\"\n },\n \"property_style\": {\n \"type\": \"string\",\n \"description\": \"Architectural style (e.g., 2 Story, Ranch)\"\n },\n \"bedrooms_details\": {\n \"type\": \"string\",\n \"description\": \"Details like above/below grade\"\n },\n \"bathrooms_partial\": {\n \"type\": \"number\",\n \"description\": \"Number of half/partial bathrooms\"\n },\n \"construction_materials\": {\n \"type\": \"string\",\n \"description\": \"Materials used (e.g., Stone, Brick)\"\n }\n },\n \"description\": \"Core property specifications\"\n },\n {\n \"name\": \"lot_info\",\n \"type\": \"object\",\n \"structure\": {\n \"views\": {\n \"type\": \"string\",\n \"description\": \"Description of views (e.g., Mountain, Panoramic)\"\n },\n \"zoning\": {\n \"type\": \"string\",\n \"description\": \"Zoning classification\"\n },\n \"waterfront\": {\n \"type\": \"boolean\",\n \"description\": \"Does the property have water frontage\"\n },\n \"lot_size_sqft\": {\n \"type\": \"number\",\n \"description\": \"Lot size converted to square feet\"\n },\n \"lot_size_text\": {\n \"type\": \"string\",\n \"description\": \"Lot size string (e.g., '''0.51 Acres''')\"\n },\n \"lot_size_acres\": {\n \"type\": \"number\",\n \"description\": \"Lot size converted to acres\"\n }\n },\n \"description\": \"Land and lot details\"\n },\n {\n \"name\": \"description\",\n \"type\": \"string\",\n \"description\": \"Full marketing description of the property\"\n },\n {\n \"name\": \"dates\",\n \"type\": \"object\",\n \"structure\": {\n \"sold\": {\n \"type\": \"string\",\n \"description\": \"Date sold\"\n },\n \"posted\": {\n \"type\": \"string\",\n \"description\": \"Date listed/posted\"\n },\n \"updated\": {\n \"type\": \"string\",\n \"description\": \"Last updated date\"\n },\n \"days_on_market\": {\n \"type\": \"number\",\n \"description\": \"Number of days listed\"\n }\n },\n \"description\": \"Relevant listing dates\"\n },\n {\n \"name\": \"interior_features\",\n \"type\": \"object\",\n \"structure\": {\n \"cooling\": {\n \"type\": \"string\",\n \"description\": \"Cooling system details\"\n },\n \"heating\": {\n \"type\": \"string\",\n \"description\": \"Heating system details\"\n },\n \"kitchen\": {\n \"type\": \"string\",\n \"description\": \"Kitchen features\"\n },\n \"laundry\": {\n \"type\": \"string\",\n \"description\": \"Laundry features\"\n },\n \"basement\": {\n \"type\": \"string\",\n \"description\": \"Basement description\"\n },\n \"flooring\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Types of flooring\"\n },\n \"appliances\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"List of appliances included\"\n },\n \"fireplaces\": {\n \"type\": \"number\",\n \"description\": \"Number of fireplaces\"\n },\n \"other_interior\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Additional interior features (e.g., Vaulted Ceilings)\"\n },\n \"security_features\": {\n \"type\": \"string\",\n \"description\": \"Interior security systems\"\n },\n \"fireplace_features\": {\n \"type\": \"string\",\n \"description\": \"Details about fireplaces\"\n }\n },\n \"description\": \"Interior amenities and systems\"\n },\n {\n \"name\": \"rooms\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Detailed room information\",\n \"item_structure\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Room name (e.g., Kitchen, Master Bed)\"\n },\n \"level\": {\n \"type\": \"string\",\n \"description\": \"Floor level\"\n },\n \"features\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Specific features of this room\"\n },\n \"dimensions\": {\n \"type\": \"string\",\n \"description\": \"Room dimensions\"\n }\n }\n },\n {\n \"name\": \"exterior_features\",\n \"type\": \"object\",\n \"structure\": {\n \"pool\": {\n \"type\": \"string\",\n \"description\": \"Pool features\"\n },\n \"roof\": {\n \"type\": \"string\",\n \"description\": \"Roofing material\"\n },\n \"fencing\": {\n \"type\": \"string\",\n \"description\": \"Fence details\"\n },\n \"patio_porch\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Patio, deck, or porch features\"\n },\n \"other_structures\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Sheds, workshops, etc.\"\n }\n },\n \"description\": \"Exterior amenities and structures\"\n },\n {\n \"name\": \"parking\",\n \"type\": \"object\",\n \"structure\": {\n \"type\": {\n \"type\": \"string\",\n \"description\": \"Type of parking (Garage, Carport, Off-street)\"\n },\n \"spaces\": {\n \"type\": \"number\",\n \"description\": \"Number of parking spaces\"\n },\n \"has_garage\": {\n \"type\": \"boolean\",\n \"description\": \"Is there a garage\"\n },\n \"description\": {\n \"type\": \"string\",\n \"description\": \"Parking features description\"\n }\n },\n \"description\": \"Parking and garage details\"\n },\n {\n \"name\": \"utilities\",\n \"type\": \"object\",\n \"structure\": {\n \"sewer\": {\n \"type\": \"string\",\n \"description\": \"Sewer system\"\n },\n \"water\": {\n \"type\": \"string\",\n \"description\": \"Water source\"\n },\n \"electricity\": {\n \"type\": \"string\",\n \"description\": \"Electric provider or details\"\n },\n \"energy_info\": {\n \"type\": \"string\",\n \"description\": \"Solar or energy efficiency details\"\n },\n \"available_utilities\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"List of available utility connections\"\n }\n },\n \"description\": \"Utility connections\"\n },\n {\n \"name\": \"financial\",\n \"type\": \"object\",\n \"structure\": {\n \"hoa_fee\": {\n \"type\": \"string\",\n \"description\": \"HOA or maintenance monthly fee\"\n },\n \"annual_tax\": {\n \"type\": \"string\",\n \"description\": \"Most recent annual tax amount\"\n },\n \"hoa_includes\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"What the HOA fee covers\"\n },\n \"tax_property_id\": {\n \"type\": \"string\",\n \"description\": \"Tax ID / APN\"\n },\n \"tax_assessed_value\": {\n \"type\": \"string\",\n \"description\": \"Total assessed tax value\"\n },\n \"estimated_monthly_payment\": {\n \"type\": \"string\",\n \"description\": \"Estimated mortgage payment\"\n }\n },\n \"description\": \"Financial data and recurring costs\"\n },\n {\n \"name\": \"tax_history\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Historical tax records\",\n \"item_structure\": {\n \"year\": {\n \"type\": \"number\",\n \"description\": \"Tax year\"\n },\n \"amount\": {\n \"type\": \"string\",\n \"description\": \"Tax amount paid\"\n },\n \"assessment\": {\n \"type\": \"string\",\n \"description\": \"Assessed value\"\n },\n \"change_percentage\": {\n \"type\": \"string\",\n \"description\": \"Year-over-year change\"\n }\n }\n },\n {\n \"name\": \"price_history\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"History of listing prices and sales\",\n \"item_structure\": {\n \"date\": {\n \"type\": \"string\",\n \"description\": \"Date of event\"\n },\n \"event\": {\n \"type\": \"string\",\n \"description\": \"Event type (Listed, Sold, Price Change)\"\n },\n \"price\": {\n \"type\": \"string\",\n \"description\": \"Price at time of event\"\n },\n \"source\": {\n \"type\": \"string\",\n \"description\": \"Source of data\"\n }\n }\n },\n {\n \"name\": \"agents\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Listing agents and brokerages\",\n \"item_structure\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Agent name\"\n },\n \"role\": {\n \"type\": \"string\",\n \"description\": \"Role (Listing Agent, Buying Agent)\"\n },\n \"email\": {\n \"type\": \"string\",\n \"description\": \"Contact email\"\n },\n \"phone\": {\n \"type\": \"string\",\n \"description\": \"Contact phone\"\n },\n \"agency\": {\n \"type\": \"string\",\n \"description\": \"Agency or Brokerage name\"\n },\n \"profile_url\": {\n \"type\": \"string\",\n \"description\": \"Link to agent profile\"\n }\n }\n },\n {\n \"name\": \"school_district\",\n \"type\": \"string\",\n \"description\": \"School district name\"\n },\n {\n \"name\": \"nearby_schools\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"List of nearby schools\",\n \"item_structure\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"School name\"\n },\n \"type\": {\n \"type\": \"string\",\n \"description\": \"Public or Private\"\n },\n \"grades\": {\n \"type\": \"string\",\n \"description\": \"Grades served\"\n },\n \"distance\": {\n \"type\": \"string\",\n \"description\": \"Distance from property\"\n }\n }\n },\n {\n \"name\": \"community\",\n \"type\": \"object\",\n \"structure\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Name of neighborhood or subdivision\"\n },\n \"features\": {\n \"type\": \"string\",\n \"description\": \"Community amenities (Park, Clubhouse)\"\n },\n \"security\": {\n \"type\": \"string\",\n \"description\": \"Community security features\"\n }\n },\n \"description\": \"Neighborhood and community features\"\n },\n {\n \"name\": \"area_statistics\",\n \"type\": \"object\",\n \"structure\": {\n \"people\": {\n \"type\": \"object\",\n \"structure\": {\n \"median_age\": {\n \"type\": \"object\",\n \"structure\": {\n \"zip\": \"number\",\n \"city\": \"number\",\n \"county\": \"number\",\n \"national\": \"number\"\n },\n \"description\": \"Median age in years (zip, city, county, national)\"\n },\n \"population\": {\n \"type\": \"object\",\n \"structure\": {\n \"zip\": \"number\",\n \"city\": \"number\",\n \"county\": \"number\",\n \"national\": \"number\"\n },\n \"description\": \"Population counts (zip, city, county, national)\"\n },\n \"average_income\": {\n \"type\": \"object\",\n \"structure\": {\n \"zip\": \"string\",\n \"city\": \"string\",\n \"county\": \"string\",\n \"national\": \"string\"\n },\n \"description\": \"Average income (zip, city, county, national)\"\n },\n \"population_density\": {\n \"type\": \"object\",\n \"structure\": {\n \"zip\": \"number\",\n \"city\": \"number\",\n \"county\": \"number\",\n \"national\": \"number\"\n },\n \"description\": \"People per sq mi (zip, city, county, national)\"\n },\n \"cost_of_living_index\": {\n \"type\": \"object\",\n \"structure\": {\n \"zip\": \"number\",\n \"city\": \"number\",\n \"county\": \"number\",\n \"national\": \"number\"\n },\n \"description\": \"Cost of living index, 100=national (zip, city, county, national)\"\n },\n \"people_per_household\": {\n \"type\": \"object\",\n \"structure\": {\n \"zip\": \"number\",\n \"city\": \"number\",\n \"county\": \"number\",\n \"national\": \"number\"\n },\n \"description\": \"Avg household size (zip, city, county, national)\"\n },\n \"median_household_income\": {\n \"type\": \"object\",\n \"structure\": {\n \"zip\": \"string\",\n \"city\": \"string\",\n \"county\": \"string\",\n \"national\": \"string\"\n },\n \"description\": \"Median income (zip, city, county, national)\"\n }\n },\n \"description\": \"Population and income statistics for the area\"\n },\n \"sun_exposure\": {\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Sun hours by month\"\n },\n \"annual_precipitation\": {\n \"type\": \"object\",\n \"structure\": {\n \"zip\": \"number\",\n \"city\": \"number\",\n \"county\": \"number\",\n \"national\": \"number\"\n },\n \"description\": \"Annual precipitation in inches (zip, city, county, national)\"\n }\n },\n \"description\": \"Demographics and environment stats\"\n },\n {\n \"name\": \"climate_risks\",\n \"type\": \"object\",\n \"structure\": {\n \"air_quality\": {\n \"type\": \"string\",\n \"description\": \"Air quality factor\"\n },\n \"fire_factor\": {\n \"type\": \"string\",\n \"description\": \"Wildfire risk level\"\n },\n \"heat_factor\": {\n \"type\": \"string\",\n \"description\": \"Heat risk level\"\n },\n \"wind_factor\": {\n \"type\": \"string\",\n \"description\": \"Wind risk level\"\n },\n \"flood_factor\": {\n \"type\": \"string\",\n \"description\": \"Flood risk level\"\n }\n },\n \"description\": \"Environmental risk factors\"\n },\n {\n \"name\": \"lifestyle\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Lifestyle scores and attributes\",\n \"item_structure\": {\n \"score\": {\n \"type\": \"number\",\n \"description\": \"Score value\"\n },\n \"category\": {\n \"type\": \"string\",\n \"description\": \"Category (e.g., Walkability, Quiet)\"\n },\n \"description\": {\n \"type\": \"string\",\n \"description\": \"Description of score\"\n }\n }\n },\n {\n \"name\": \"nearby_amenities\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Points of interest nearby\",\n \"item_structure\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Name of place\"\n },\n \"category\": {\n \"type\": \"string\",\n \"description\": \"Type (Grocery, Park, Hospital)\"\n },\n \"distance\": {\n \"type\": \"string\",\n \"description\": \"Distance from property\"\n }\n }\n },\n {\n \"name\": \"metadata\",\n \"type\": \"object\",\n \"structure\": {\n \"url\": {\n \"type\": \"string\",\n \"description\": \"Listing URL\"\n },\n \"views\": {\n \"type\": \"number\",\n \"description\": \"Number of views\"\n },\n \"source\": {\n \"type\": \"string\",\n \"description\": \"Data source name\"\n },\n \"favorites\": {\n \"type\": \"number\",\n \"description\": \"Number of favorites/saves\"\n }\n },\n \"description\": \"Listing metadata\"\n }\n ]\n}\n","restaurant":"\nPlease extract the data by following the schema:\n{\n \"fields\": [\n {\n \"name\": \"name\",\n \"type\": \"string\",\n \"description\": \"Restaurant name\"\n },\n {\n \"name\": \"rank\",\n \"type\": \"string\",\n \"description\": \"Ranking among restaurants in the area (e.g., #14 of 769 Restaurants in Kuta)\"\n },\n {\n \"name\": \"cuisine\",\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Cuisine types served\"\n },\n {\n \"name\": \"address\",\n \"type\": \"string\",\n \"description\": \"Full street address\"\n },\n {\n \"name\": \"map_link\",\n \"type\": \"string\",\n \"description\": \"Google Maps directions link\"\n },\n {\n \"name\": \"phone\",\n \"type\": \"string\",\n \"description\": \"Contact phone number\"\n },\n {\n \"name\": \"email\",\n \"type\": \"string\",\n \"description\": \"Contact email address\"\n },\n {\n \"name\": \"website\",\n \"type\": \"string\",\n \"description\": \"Official website URL\"\n },\n {\n \"name\": \"hours\",\n \"type\": \"object\",\n \"structure\": {\n \"Friday\": {\n \"type\": \"string\",\n \"description\": \"Opening hours\"\n },\n \"Monday\": {\n \"type\": \"string\",\n \"description\": \"Opening hours\"\n },\n \"Sunday\": {\n \"type\": \"string\",\n \"description\": \"Opening hours\"\n },\n \"Tuesday\": {\n \"type\": \"string\",\n \"description\": \"Opening hours\"\n },\n \"Saturday\": {\n \"type\": \"string\",\n \"description\": \"Opening hours\"\n },\n \"Thursday\": {\n \"type\": \"string\",\n \"description\": \"Opening hours\"\n },\n \"Wednesday\": {\n \"type\": \"string\",\n \"description\": \"Opening hours\"\n }\n },\n \"description\": \"Operating hours by day of week\"\n },\n {\n \"name\": \"features\",\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Restaurant features and amenities (e.g., dietary options, meal types, payment methods)\"\n },\n {\n \"name\": \"description\",\n \"type\": \"string\",\n \"description\": \"Restaurant description and overview\"\n },\n {\n \"name\": \"rating\",\n \"type\": \"number\",\n \"description\": \"Overall rating (0-5 scale)\"\n },\n {\n \"name\": \"rating_distribution\",\n \"type\": \"object\",\n \"structure\": {\n \"Food\": {\n \"type\": \"number\",\n \"description\": \"Food rating (0-5)\"\n },\n \"Value\": {\n \"type\": \"number\",\n \"description\": \"Value rating (0-5)\"\n },\n \"Service\": {\n \"type\": \"number\",\n \"description\": \"Service rating (0-5)\"\n },\n \"Atmosphere\": {\n \"type\": \"number\",\n \"description\": \"Atmosphere rating (0-5)\"\n }\n },\n \"description\": \"Ratings by category\"\n },\n {\n \"name\": \"reviews_count\",\n \"type\": \"number\",\n \"description\": \"Total number of reviews\"\n },\n {\n \"name\": \"reviews_distribution\",\n \"type\": \"object\",\n \"structure\": {\n \"Good\": {\n \"type\": \"number\",\n \"description\": \"Number of Good reviews\"\n },\n \"Poor\": {\n \"type\": \"number\",\n \"description\": \"Number of Poor reviews\"\n },\n \"Average\": {\n \"type\": \"number\",\n \"description\": \"Number of Average reviews\"\n },\n \"Terrible\": {\n \"type\": \"number\",\n \"description\": \"Number of Terrible reviews\"\n },\n \"Excellent\": {\n \"type\": \"number\",\n \"description\": \"Number of Excellent reviews\"\n }\n },\n \"description\": \"Review count by rating level\"\n },\n {\n \"name\": \"review_summary\",\n \"type\": \"string\",\n \"description\": \"AI-generated summary of guest reviews\"\n },\n {\n \"name\": \"review_summary_distribution\",\n \"type\": \"object\",\n \"structure\": {\n \"Food\": {\n \"type\": \"string\",\n \"description\": \"Food sentiment keyword\"\n },\n \"Value\": {\n \"type\": \"string\",\n \"description\": \"Value sentiment keyword\"\n },\n \"Service\": {\n \"type\": \"string\",\n \"description\": \"Service sentiment keyword\"\n },\n \"Location\": {\n \"type\": \"string\",\n \"description\": \"Location sentiment keyword\"\n },\n \"Atmosphere\": {\n \"type\": \"string\",\n \"description\": \"Atmosphere sentiment keyword\"\n }\n },\n \"description\": \"One-word sentiment summary by category\"\n },\n {\n \"name\": \"reviews\",\n \"type\": \"object\",\n \"structure\": {\n \"title\": {\n \"type\": \"string\",\n \"description\": \"Review title\"\n },\n \"rating\": {\n \"type\": \"string\",\n \"description\": \"\"\n },\n \"content\": {\n \"type\": \"string\",\n \"description\": \"\"\n },\n \"review_date\": {\n \"type\": \"number\",\n \"description\": \"For example Feb 5, 2026\"\n },\n \"reviewer_name\": {\n \"type\": \"string\",\n \"description\": \"\"\n },\n \"rating_distribution\": {\n \"type\": \"object\",\n \"structure\": {},\n \"description\": \"Key value rating distribution (for example value: 4, service: 3, etc.)\"\n }\n },\n \"description\": \"Restaurant reviews\"\n }\n ]\n}\n","socialMediaProfile":"\nPlease extract the data by following the schema:\n{\n \"fields\": [\n {\n \"name\": \"username\",\n \"type\": \"string\",\n \"description\": \"\"\n },\n {\n \"name\": \"display_name\",\n \"type\": \"string\",\n \"description\": \"Profile display name\"\n },\n {\n \"name\": \"type\",\n \"type\": \"string\",\n \"description\": \"Profile type (e.g. Internet Service Provider, Sport and recreation, community, etc.)\"\n },\n {\n \"name\": \"email\",\n \"type\": \"string\",\n \"description\": \"\"\n },\n {\n \"name\": \"phone\",\n \"type\": \"string\",\n \"description\": \"\"\n },\n {\n \"name\": \"phone\",\n \"type\": \"string\",\n \"description\": \"\"\n },\n {\n \"name\": \"address\",\n \"type\": \"object\",\n \"structure\": {\n \"city\": {\n \"type\": \"string\",\n \"description\": \"\"\n },\n \"street\": {\n \"type\": \"string\",\n \"description\": \"\"\n },\n \"country\": {\n \"type\": \"string\",\n \"description\": \"\"\n },\n \"district\": {\n \"type\": \"string\",\n \"description\": \"\"\n }\n },\n \"description\": \"\"\n },\n {\n \"name\": \"description\",\n \"type\": \"string\",\n \"description\": \"Profile bio or description\"\n },\n {\n \"name\": \"verified\",\n \"type\": \"string\",\n \"description\": \"true or false\"\n },\n {\n \"name\": \"followers\",\n \"type\": \"number\",\n \"description\": \"\"\n },\n {\n \"name\": \"following\",\n \"type\": \"number\",\n \"description\": \"\"\n },\n {\n \"name\": \"subscribers\",\n \"type\": \"number\",\n \"description\": \"\"\n },\n {\n \"name\": \"num_posts\",\n \"type\": \"number\",\n \"description\": \"\"\n },\n {\n \"name\": \"social_links\",\n \"type\": \"object\",\n \"structure\": {\n \"x\": {\n \"type\": \"string\",\n \"description\": \"\"\n },\n \"tiktok\": {\n \"type\": \"string\",\n \"description\": \"\"\n },\n \"threads\": {\n \"type\": \"string\",\n \"description\": \"\"\n },\n \"youtube\": {\n \"type\": \"string\",\n \"description\": \"\"\n },\n \"telegram\": {\n \"type\": \"string\",\n \"description\": \"\"\n },\n \"instagram\": {\n \"type\": \"string\",\n \"description\": \"\"\n }\n },\n \"description\": \"\"\n },\n {\n \"name\": \"website\",\n \"type\": \"string\",\n \"description\": \"\"\n },\n {\n \"name\": \"profile_picture\",\n \"type\": \"string\",\n \"description\": \"\"\n },\n {\n \"name\": \"recent_posts\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"\",\n \"item_structure\": {\n \"title\": {\n \"type\": \"string\",\n \"description\": \"\"\n },\n \"posted\": {\n \"type\": \"string\",\n \"description\": \"Can be date or for example 1 day ago, 4 months ago.\"\n },\n \"post_url\": {\n \"type\": \"string\",\n \"description\": \"Full url of the post\"\n },\n \"num_views\": {\n \"type\": \"string\",\n \"description\": \"\"\n },\n \"thumbnail\": {\n \"type\": \"string\",\n \"description\": \"Thumbnail URL\"\n },\n \"description\": {\n \"type\": \"string\",\n \"description\": \"\"\n }\n }\n }\n ]\n}\n","tourAttraction":"\nPlease extract the data by following the schema:\n{\n \"fields\": [\n {\n \"name\": \"title\",\n \"type\": \"string\",\n \"description\": \"Tour title as displayed on the page\"\n },\n {\n \"name\": \"about\",\n \"type\": \"string\",\n \"description\": \"Full tour description and overview\"\n },\n {\n \"name\": \"provider\",\n \"type\": \"object\",\n \"structure\": {\n \"logo\": {\n \"type\": \"string\",\n \"description\": \"Logo URL\"\n },\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Company name\"\n },\n \"contact\": {\n \"type\": \"object\",\n \"structure\": {\n \"email\": {\n \"type\": \"string\"\n },\n \"phone\": {\n \"type\": \"string\"\n },\n \"website\": {\n \"type\": \"string\"\n }\n }\n }\n },\n \"description\": \"Tour operator information\"\n },\n {\n \"name\": \"location\",\n \"type\": \"object\",\n \"structure\": {\n \"city\": {\n \"type\": \"string\"\n },\n \"country\": {\n \"type\": \"string\"\n },\n \"coordinates\": {\n \"type\": \"object\",\n \"structure\": {\n \"latitude\": {\n \"type\": \"number\"\n },\n \"longitude\": {\n \"type\": \"number\"\n }\n }\n },\n \"meeting_point\": {\n \"type\": \"string\"\n }\n },\n \"description\": \"Primary tour location\"\n },\n {\n \"name\": \"age_range\",\n \"type\": \"object\",\n \"structure\": {\n \"max_age\": {\n \"type\": \"number\"\n },\n \"min_age\": {\n \"type\": \"number\"\n }\n },\n \"description\": \"Age range for the tour\"\n },\n {\n \"name\": \"duration\",\n \"type\": \"string\",\n \"description\": \"Total tour duration (e.g., 3-4 hours)\"\n },\n {\n \"name\": \"start_time\",\n \"type\": \"string\",\n \"description\": \"Start time for the tour\"\n },\n {\n \"name\": \"mobile_ticket\",\n \"type\": \"boolean\",\n \"description\": \"Whether the tour requires a mobile ticket\"\n },\n {\n \"name\": \"languages\",\n \"type\": \"object\",\n \"structure\": {\n \"live_guide\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Languages of the live guide\"\n },\n \"audio_guide\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Languages of the audio guide\"\n },\n \"written_guide\": {\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Languages of the written guide\"\n }\n },\n \"description\": \"Languages of the tour\"\n },\n {\n \"name\": \"price\",\n \"type\": \"object\",\n \"structure\": {\n \"currency\": {\n \"type\": \"string\"\n },\n \"original_price\": {\n \"type\": \"string\"\n },\n \"starting_price\": {\n \"type\": \"string\"\n },\n \"discounted_price\": {\n \"type\": \"string\"\n },\n \"price_per_person\": {\n \"type\": \"boolean\"\n }\n },\n \"description\": \"Tour pricing information\"\n },\n {\n \"name\": \"availability\",\n \"type\": \"object\",\n \"structure\": {\n \"max_group_size\": {\n \"type\": \"number\"\n },\n \"available_dates\": {\n \"type\": \"array\",\n \"item_type\": \"string\"\n },\n \"free_cancellation\": {\n \"type\": \"boolean\"\n },\n \"instant_confirmation\": {\n \"type\": \"boolean\"\n },\n \"reserve_now_pay_later\": {\n \"type\": \"boolean\"\n }\n },\n \"description\": \"Availability and booking info\"\n },\n {\n \"name\": \"highlights\",\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Key selling points and highlights\"\n },\n {\n \"name\": \"included\",\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Items included in the tour\"\n },\n {\n \"name\": \"excluded\",\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Items not included\"\n },\n {\n \"name\": \"meeting_and_pickup\",\n \"type\": \"object\",\n \"structure\": {\n \"end_point\": {\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"item_structure\": {\n \"address\": {\n \"type\": \"string\",\n \"description\": \"Address of the end point\"\n },\n \"details\": {\n \"type\": \"string\",\n \"description\": \"Details of the end point address\"\n }\n }\n },\n \"start_point\": {\n \"type\": \"object\",\n \"structure\": {\n \"type\": {\n \"type\": \"string\",\n \"description\": \"Type of start point (hotel, airport, multiple, etc.)\"\n },\n \"address\": {\n \"type\": \"string\",\n \"description\": \"Address of the start point\"\n },\n \"details\": {\n \"type\": \"string\",\n \"description\": \"Details of the start point address\"\n },\n \"pickup_details\": {\n \"type\": \"string\",\n \"description\": \"Pickup details\"\n },\n \"pickup_offered\": {\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"item_structure\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Name of the pickup point\"\n },\n \"details\": {\n \"type\": \"string\",\n \"description\": \"Details of the pickup point\"\n }\n }\n }\n }\n }\n },\n \"description\": \"Meeting point and pickup information\"\n },\n {\n \"name\": \"itinerary\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Step-by-step itinerary\",\n \"item_structure\": {\n \"duration\": {\n \"type\": \"string\"\n },\n \"stop_name\": {\n \"type\": \"string\"\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"admission_included\": {\n \"type\": \"boolean\"\n }\n }\n },\n {\n \"name\": \"additional_info\",\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Important notes and additional details\"\n },\n {\n \"name\": \"accessibility\",\n \"type\": \"object\",\n \"structure\": {\n \"stroller_accessible\": {\n \"type\": \"boolean\"\n },\n \"near_public_transport\": {\n \"type\": \"boolean\"\n },\n \"wheelchair_accessible\": {\n \"type\": \"boolean\"\n }\n },\n \"description\": \"Accessibility information\"\n },\n {\n \"name\": \"review_stats\",\n \"type\": \"object\",\n \"structure\": {\n \"overall_score\": {\n \"type\": \"number\"\n },\n \"total_reviews\": {\n \"type\": \"number\"\n },\n \"rating_breakdown\": {\n \"type\": \"object\",\n \"structure\": {\n \"1_star\": {\n \"type\": \"number\"\n },\n \"2_star\": {\n \"type\": \"number\"\n },\n \"3_star\": {\n \"type\": \"number\"\n },\n \"4_star\": {\n \"type\": \"number\"\n },\n \"5_star\": {\n \"type\": \"number\"\n }\n }\n }\n },\n \"description\": \"Aggregated review statistics\"\n },\n {\n \"name\": \"reviews\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Individual traveler reviews\",\n \"item_structure\": {\n \"date\": {\n \"type\": \"string\"\n },\n \"title\": {\n \"type\": \"string\"\n },\n \"rating\": {\n \"type\": \"number\"\n },\n \"content\": {\n \"type\": \"string\"\n },\n \"country\": {\n \"type\": \"string\"\n },\n \"reviewer\": {\n \"type\": \"string\"\n },\n \"trip_type\": {\n \"type\": \"string\"\n }\n }\n },\n {\n \"name\": \"photos\",\n \"type\": \"array\",\n \"item_type\": \"string\",\n \"description\": \"Image URLs\"\n },\n {\n \"name\": \"faq\",\n \"type\": \"array\",\n \"item_type\": \"object\",\n \"description\": \"Frequently asked questions\",\n \"item_structure\": {\n \"answer\": {\n \"type\": \"string\"\n },\n \"question\": {\n \"type\": \"string\"\n }\n }\n },\n {\n \"name\": \"policies\",\n \"type\": \"object\",\n \"structure\": {\n \"child_policy\": {\n \"type\": \"string\"\n },\n \"refund_policy\": {\n \"type\": \"string\"\n },\n \"weather_policy\": {\n \"type\": \"string\"\n },\n \"cancellation_policy\": {\n \"type\": \"string\"\n }\n },\n \"description\": \"Tour policies\"\n }\n ]\n}\n"}
\ No newline at end of file
diff --git a/lib/crewai-tools/src/crewai_tools/tools/mrscraper/toolkit.py b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/toolkit.py
new file mode 100644
index 0000000000..48bce6e50a
--- /dev/null
+++ b/lib/crewai-tools/src/crewai_tools/tools/mrscraper/toolkit.py
@@ -0,0 +1,113 @@
+"""Convenience factory for independent MrScraper tool instances."""
+
+from collections.abc import Iterable
+
+from crewai.tools import BaseTool
+
+from crewai_tools.tools.mrscraper.account import MrScraperGetAccountInfoTool
+from crewai_tools.tools.mrscraper.base import MrScraperBaseTool, resolve_api_token
+from crewai_tools.tools.mrscraper.client import MrScraperClient
+from crewai_tools.tools.mrscraper.discovery import (
+ MrScraperCrawlWebsiteUrlsTool,
+ MrScraperSearchGoogleSerpTool,
+)
+from crewai_tools.tools.mrscraper.extraction import (
+ MrScraperExtractListingsTool,
+ MrScraperExtractPageByPromptTool,
+ MrScraperExtractStructuredDataTool,
+ MrScraperFetchRenderedHtmlTool,
+)
+from crewai_tools.tools.mrscraper.results import (
+ MrScraperGetLatestResultsTool,
+ MrScraperGetResultDetailTool,
+ MrScraperGetResultsTool,
+)
+from crewai_tools.tools.mrscraper.scraper_creation import (
+ MrScraperCreateListingScraperTool,
+ MrScraperCreatePromptScraperTool,
+ MrScraperCreateWebsiteCrawlScraperTool,
+)
+from crewai_tools.tools.mrscraper.scraper_runs import (
+ MrScraperRunExistingScraperBatchTool,
+ MrScraperRunExistingScraperTool,
+)
+
+
+ToolClass = type[MrScraperBaseTool]
+
+_GROUPS: dict[str, tuple[ToolClass, ...]] = {
+ "account": (MrScraperGetAccountInfoTool,),
+ "discovery": (
+ MrScraperCrawlWebsiteUrlsTool,
+ MrScraperSearchGoogleSerpTool,
+ ),
+ "extraction": (
+ MrScraperExtractPageByPromptTool,
+ MrScraperExtractListingsTool,
+ MrScraperExtractStructuredDataTool,
+ MrScraperFetchRenderedHtmlTool,
+ ),
+ "results": (
+ MrScraperGetResultsTool,
+ MrScraperGetLatestResultsTool,
+ MrScraperGetResultDetailTool,
+ ),
+ "scraper creation": (
+ MrScraperCreatePromptScraperTool,
+ MrScraperCreateListingScraperTool,
+ MrScraperCreateWebsiteCrawlScraperTool,
+ ),
+ "scraper runs": (
+ MrScraperRunExistingScraperTool,
+ MrScraperRunExistingScraperBatchTool,
+ ),
+}
+
+
+def create_mrscraper_toolkit(
+ *,
+ groups: Iterable[str] | None = None,
+ tool_names: Iterable[str] | None = None,
+ api_token: str | None = None,
+) -> list[BaseTool]:
+ """Create all 15 MrScraper tools or a selected group/name subset.
+
+ Args:
+ groups: Optional case-insensitive group names: Account, Discovery,
+ Extraction, Results, Scraper Creation, or Scraper Runs.
+ tool_names: Optional exact public tool names to select.
+ api_token: Optional constructor-only credential override. The value remains
+ private and is excluded from schemas, serialization, reprs, and errors.
+
+ Returns:
+ New independent tool instances sharing one configured HTTP client.
+
+ Raises:
+ ValueError: If selection is ambiguous or contains an unknown group/name.
+ """
+ if groups is not None and tool_names is not None:
+ raise ValueError("Select MrScraper tools by groups or tool_names, not both")
+
+ all_classes = tuple(tool for group in _GROUPS.values() for tool in group)
+ selected: tuple[ToolClass, ...]
+ if groups is not None:
+ normalized = list(dict.fromkeys(group.strip().lower() for group in groups))
+ unknown = sorted(set(normalized) - _GROUPS.keys())
+ if unknown:
+ raise ValueError(f"Unknown MrScraper toolkit groups: {', '.join(unknown)}")
+ selected = tuple(tool for group in normalized for tool in _GROUPS[group])
+ elif tool_names is not None:
+ by_name = {tool.model_fields["name"].default: tool for tool in all_classes}
+ requested = list(tool_names)
+ unknown = sorted(set(requested) - by_name.keys())
+ if unknown:
+ raise ValueError(f"Unknown MrScraper tool names: {', '.join(unknown)}")
+ selected = tuple(by_name[name] for name in requested)
+ else:
+ selected = all_classes
+
+ client = MrScraperClient(resolve_api_token(api_token))
+ return [tool_class(client=client) for tool_class in selected]
+
+
+__all__ = ["create_mrscraper_toolkit"]
diff --git a/lib/crewai-tools/tests/tools/mrscraper/test_mrscraper_tools.py b/lib/crewai-tools/tests/tools/mrscraper/test_mrscraper_tools.py
new file mode 100644
index 0000000000..3ade6eceb5
--- /dev/null
+++ b/lib/crewai-tools/tests/tools/mrscraper/test_mrscraper_tools.py
@@ -0,0 +1,655 @@
+"""Contract tests for the native MrScraper integration."""
+
+import hashlib
+import json
+from pathlib import Path
+from typing import Any
+
+from crewai import Agent
+from crewai.tools import BaseTool
+from crewai_tools import (
+ MrScraperCrawlWebsiteUrlsTool,
+ MrScraperCreateListingScraperTool,
+ MrScraperCreatePromptScraperTool,
+ MrScraperCreateWebsiteCrawlScraperTool,
+ MrScraperExtractListingsTool,
+ MrScraperExtractPageByPromptTool,
+ MrScraperExtractStructuredDataTool,
+ MrScraperFetchRenderedHtmlTool,
+ MrScraperGetAccountInfoTool,
+ MrScraperGetLatestResultsTool,
+ MrScraperGetResultDetailTool,
+ MrScraperGetResultsTool,
+ MrScraperRunExistingScraperBatchTool,
+ MrScraperRunExistingScraperTool,
+ MrScraperSearchGoogleSerpTool,
+ create_mrscraper_toolkit,
+)
+from crewai_tools.generate_tool_specs import ToolSpecExtractor
+from crewai_tools.tools.mrscraper.client import MrScraperClient
+from crewai_tools.tools.mrscraper.extraction import load_structured_data_prompts
+from crewai_tools.tools.mrscraper.payloads import (
+ append_output_schema,
+ general_payload,
+ listing_payload,
+ map_payload,
+)
+from crewai_tools.tools.mrscraper.schemas import (
+ FetchRenderedHtmlInput,
+ GetResultsInput,
+ ListingScraperInput,
+ MapScraperInput,
+ RunExistingScraperBatchInput,
+ RunExistingScraperInput,
+ SearchGoogleSerpInput,
+)
+from pydantic import BaseModel, ValidationError
+import pytest
+import requests
+
+
+FAKE_TOKEN = "conspicuously-fake-mrscraper-token"
+
+TOOL_CLASSES = (
+ MrScraperGetAccountInfoTool,
+ MrScraperCrawlWebsiteUrlsTool,
+ MrScraperSearchGoogleSerpTool,
+ MrScraperExtractPageByPromptTool,
+ MrScraperExtractListingsTool,
+ MrScraperExtractStructuredDataTool,
+ MrScraperFetchRenderedHtmlTool,
+ MrScraperGetResultsTool,
+ MrScraperGetLatestResultsTool,
+ MrScraperGetResultDetailTool,
+ MrScraperCreatePromptScraperTool,
+ MrScraperCreateListingScraperTool,
+ MrScraperCreateWebsiteCrawlScraperTool,
+ MrScraperRunExistingScraperTool,
+ MrScraperRunExistingScraperBatchTool,
+)
+
+TOOL_NAMES = (
+ "mrscraper_get_account_info",
+ "mrscraper_crawl_website_urls",
+ "mrscraper_search_google_serp",
+ "mrscraper_extract_page_by_prompt",
+ "mrscraper_extract_listings",
+ "mrscraper_extract_structured_data",
+ "mrscraper_fetch_rendered_html",
+ "mrscraper_get_results",
+ "mrscraper_get_latest_results",
+ "mrscraper_get_result_detail",
+ "mrscraper_create_prompt_scraper",
+ "mrscraper_create_listing_scraper",
+ "mrscraper_create_website_crawl_scraper",
+ "mrscraper_run_existing_scraper",
+ "mrscraper_run_existing_scraper_batch",
+)
+
+
+class FakeResponse:
+ def __init__(
+ self,
+ value: Any = None,
+ *,
+ text: str | None = None,
+ status_code: int = 200,
+ content_type: str = "application/json",
+ ) -> None:
+ """Initialize a deterministic response double."""
+ self.value = value
+ self.text = text if text is not None else json.dumps(value)
+ self.status_code = status_code
+ self.headers = {"Content-Type": content_type}
+
+ def json(self) -> Any:
+ """Return the configured JSON value."""
+ return self.value
+
+
+class FakeSession:
+ def __init__(
+ self,
+ response: FakeResponse | None = None,
+ error: requests.RequestException | None = None,
+ ) -> None:
+ """Initialize a session double with a response or transport error."""
+ self.response = response or FakeResponse({"ok": True})
+ self.error = error
+ self.calls: list[dict[str, Any]] = []
+
+ def request(self, method: str, url: str, **kwargs: Any) -> FakeResponse:
+ """Record a request before returning or raising the configured result."""
+ self.calls.append({"method": method, "url": url, **kwargs})
+ if self.error is not None:
+ raise self.error
+ return self.response
+
+
+def make_client(
+ response: FakeResponse | None = None,
+ error: requests.RequestException | None = None,
+) -> tuple[MrScraperClient, FakeSession]:
+ """Build a client and expose its deterministic session double."""
+ session = FakeSession(response=response, error=error)
+ return MrScraperClient(FAKE_TOKEN, session=session), session # type: ignore[arg-type]
+
+
+def test_all_tools_are_public_independent_base_tools() -> None:
+ """Expose every integration operation as a distinct BaseTool."""
+ client, _ = make_client()
+ tools = [tool_class(client=client) for tool_class in TOOL_CLASSES]
+
+ assert len(tools) == 15
+ assert all(isinstance(tool, BaseTool) for tool in tools)
+ assert tuple(tool.name for tool in tools) == TOOL_NAMES
+ assert len({tool.description for tool in tools}) == 15
+ assert all(tool.description.strip() for tool in tools)
+ assert all(tool.args_schema is not BaseTool._ArgsSchemaPlaceholder for tool in tools)
+
+
+def test_toolkit_returns_all_groups_names_and_fresh_state(monkeypatch: pytest.MonkeyPatch) -> None:
+ """Select fresh tool instances by group or public tool name."""
+ monkeypatch.setenv("MRSCRAPER_API_TOKEN", FAKE_TOKEN)
+ first = create_mrscraper_toolkit()
+ second = create_mrscraper_toolkit()
+
+ assert len(first) == 15
+ assert [tool.name for tool in first] == list(TOOL_NAMES)
+ assert all(left is not right for left, right in zip(first, second, strict=True))
+ assert [tool.name for tool in create_mrscraper_toolkit(groups=["Results"])] == [
+ "mrscraper_get_results",
+ "mrscraper_get_latest_results",
+ "mrscraper_get_result_detail",
+ ]
+ assert len(create_mrscraper_toolkit(groups=["Discovery", "Extraction"])) == 6
+ selected = create_mrscraper_toolkit(
+ tool_names=["mrscraper_get_account_info", "mrscraper_get_result_detail"]
+ )
+ assert [tool.name for tool in selected] == [
+ "mrscraper_get_account_info",
+ "mrscraper_get_result_detail",
+ ]
+ with pytest.raises(ValueError, match="groups or tool_names"):
+ create_mrscraper_toolkit(groups=["Account"], tool_names=[TOOL_NAMES[0]])
+ with pytest.raises(ValueError, match="Unknown MrScraper toolkit groups"):
+ create_mrscraper_toolkit(groups=["unknown"])
+
+
+def test_agent_receives_15_independent_tools(monkeypatch: pytest.MonkeyPatch) -> None:
+ """Attach all independent MrScraper tools to an Agent."""
+ monkeypatch.setenv("MRSCRAPER_API_TOKEN", FAKE_TOKEN)
+ tools = create_mrscraper_toolkit()
+ agent = Agent(
+ role="MrScraper contract test",
+ goal="Verify independent tool discovery",
+ backstory="A deterministic test agent",
+ tools=tools,
+ )
+
+ assert len(agent.tools) == 15
+ assert [tool.name for tool in agent.tools] == list(TOOL_NAMES)
+ assert "operation" not in {
+ field for tool in agent.tools for field in tool.args_schema.model_fields
+ }
+
+
+def test_schema_required_defaults_enums_constraints_and_descriptions() -> None:
+ """Publish strict, documented schemas with the expected defaults."""
+ search_schema = SearchGoogleSerpInput.model_json_schema()
+ assert search_schema["required"] == ["query"]
+ assert search_schema["properties"]["page"]["default"] == 1
+ assert search_schema["properties"]["page"]["minimum"] == 1
+ assert search_schema["properties"]["format"]["enum"] == ["json", "html"]
+
+ rendered = FetchRenderedHtmlInput.model_json_schema()["properties"]
+ assert rendered["max_retries"]["minimum"] == 0
+ assert {item.get("minimum") for item in rendered["token_cap"]["anyOf"]} == {
+ 1,
+ None,
+ }
+ assert ["full", "top"] in [
+ item.get("enum") for item in rendered["screenshot_mode"]["anyOf"]
+ ]
+
+ listing = ListingScraperInput.model_json_schema()
+ assert listing["required"] == ["url"]
+ assert listing["properties"]["max_pages"]["default"] == 1
+ assert listing["properties"]["max_pages"]["minimum"] == 1
+
+ for tool_class in TOOL_CLASSES:
+ schema = tool_class.model_fields["args_schema"].default.model_json_schema()
+ assert schema["type"] == "object"
+ for field in schema.get("properties", {}).values():
+ assert field.get("description")
+
+
+@pytest.mark.parametrize(
+ ("schema", "values"),
+ [
+ (SearchGoogleSerpInput, {"query": "x", "page": True}),
+ (SearchGoogleSerpInput, {"query": "x", "region": "USA"}),
+ (MapScraperInput, {"url": "https://example.com", "limit": False}),
+ (ListingScraperInput, {"url": "https://example.com", "max_pages": 1.5}),
+ (FetchRenderedHtmlInput, {"url": "https://example.com", "max_retries": -1}),
+ (RunExistingScraperBatchInput, {"scraper_type": "ai", "scraper_id": "id", "urls": []}),
+ (RunExistingScraperBatchInput, {"scraper_type": "ai", "scraper_id": "id", "urls": [" "]}),
+ ],
+)
+def test_strict_schema_validation(schema: Any, values: dict[str, Any]) -> None:
+ """Reject coercion, invalid bounds, and empty URL batches."""
+ with pytest.raises(ValidationError):
+ schema.model_validate(values)
+
+
+def test_credentials_are_required_metadata_only_and_secret_safe(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.delenv("MRSCRAPER_API_TOKEN", raising=False)
+ with pytest.raises(ValueError, match="MRSCRAPER_API_TOKEN is required") as exc:
+ MrScraperGetAccountInfoTool()
+ assert FAKE_TOKEN not in str(exc.value)
+
+ client, _ = make_client()
+ for tool_class in TOOL_CLASSES:
+ tool = tool_class(client=client)
+ assert [(item.name, item.description, item.required) for item in tool.env_vars] == [
+ ("MRSCRAPER_API_TOKEN", "MrScraper API token", True)
+ ]
+ schema_text = json.dumps(tool.args_schema.model_json_schema())
+ assert "MRSCRAPER_API_TOKEN" not in schema_text
+ assert FAKE_TOKEN not in schema_text
+ assert FAKE_TOKEN not in repr(tool)
+ assert FAKE_TOKEN not in tool.model_dump_json()
+
+
+@pytest.mark.parametrize(
+ ("value", "expected"),
+ [
+ ({"café": False, "zero": 0}, '{"café":false,"zero":0}'),
+ ([1, False, 0], "[1,false,0]"),
+ (False, "false"),
+ (0, "0"),
+ (None, "null"),
+ ],
+)
+def test_client_preserves_json_shapes_and_primary_auth(value: Any, expected: str) -> None:
+ client, session = make_client(FakeResponse(value))
+ result = client.request("GET", "primary", "/api/v1/test")
+
+ assert result == expected
+ call = session.calls[0]
+ assert call["url"] == "https://api.app.mrscraper.com/api/v1/test"
+ assert call["headers"]["x-api-token"] == FAKE_TOKEN
+ assert call["headers"]["Accept"] == "application/json"
+ assert call["timeout"] == (10, 660)
+
+
+def test_serp_host_bearer_payload_and_html_preservation() -> None:
+ html = "\n
exact & unquoted
"
+ client, session = make_client(
+ FakeResponse(text=html, content_type="text/html; charset=utf-8")
+ )
+ tool = MrScraperSearchGoogleSerpTool(client=client)
+
+ result = tool.run(query="hotels", format="html", render_js=False)
+
+ assert result == html
+ call = session.calls[0]
+ assert call["url"] == (
+ "https://sync.scraper.mrscraper.com/api/google/serp/v2/sync"
+ )
+ assert call["headers"]["Authorization"] == f"Bearer {FAKE_TOKEN}"
+ assert "x-api-token" not in call["headers"]
+ assert call["json"]["renderJs"] is False
+
+
+def test_rendered_page_query_body_split_and_boolean_text() -> None:
+ client, session = make_client(FakeResponse(text="