-
Notifications
You must be signed in to change notification settings - Fork 24
Expand file tree
/
Copy pathserver.py
More file actions
566 lines (463 loc) · 19.3 KB
/
server.py
File metadata and controls
566 lines (463 loc) · 19.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
#!/usr/bin/env python3
"""
MCP server for ScapeGraph API integration.
This server exposes methods to use ScapeGraph's AI-powered web scraping services:
- markdownify: Convert any webpage into clean, formatted markdown
- smartscraper: Extract structured data from any webpage using AI
- searchscraper: Perform AI-powered web searches with structured results
- smartcrawler_initiate: Initiate intelligent multi-page web crawling with AI extraction or markdown conversion
- smartcrawler_fetch_results: Retrieve results from asynchronous crawling operations
"""
import os
import json
from typing import Any, Dict, Optional, List, Union
import httpx
from fastmcp import FastMCP
class ScapeGraphClient:
"""Client for interacting with the ScapeGraph API."""
BASE_URL = "https://api.scrapegraphai.com/v1"
def __init__(self, api_key: str):
"""
Initialize the ScapeGraph API client.
Args:
api_key: API key for ScapeGraph API
"""
self.api_key = api_key
self.headers = {
"SGAI-APIKEY": api_key,
"Content-Type": "application/json"
}
self.client = httpx.Client(timeout=httpx.Timeout(120.0))
def markdownify(self, website_url: str) -> Dict[str, Any]:
"""
Convert a webpage into clean, formatted markdown.
Args:
website_url: URL of the webpage to convert
Returns:
Dictionary containing the markdown result
"""
url = f"{self.BASE_URL}/markdownify"
data = {
"website_url": website_url
}
response = self.client.post(url, headers=self.headers, json=data)
if response.status_code != 200:
error_msg = f"Error {response.status_code}: {response.text}"
raise Exception(error_msg)
return response.json()
def smartscraper(self, user_prompt: str, website_url: str, number_of_scrolls: int = None, markdown_only: bool = None) -> Dict[str, Any]:
"""
Extract structured data from a webpage using AI.
Args:
user_prompt: Instructions for what data to extract
website_url: URL of the webpage to scrape
number_of_scrolls: Number of infinite scrolls to perform (optional)
markdown_only: Whether to return only markdown content without AI processing (optional)
Returns:
Dictionary containing the extracted data or markdown content
"""
url = f"{self.BASE_URL}/smartscraper"
data = {
"user_prompt": user_prompt,
"website_url": website_url
}
# Add number_of_scrolls to the request if provided
if number_of_scrolls is not None:
data["number_of_scrolls"] = number_of_scrolls
# Add markdown_only to the request if provided
if markdown_only is not None:
data["markdown_only"] = markdown_only
response = self.client.post(url, headers=self.headers, json=data)
if response.status_code != 200:
error_msg = f"Error {response.status_code}: {response.text}"
raise Exception(error_msg)
return response.json()
def searchscraper(self, user_prompt: str, num_results: int = None, number_of_scrolls: int = None) -> Dict[str, Any]:
"""
Perform AI-powered web searches with structured results.
Args:
user_prompt: Search query or instructions
num_results: Number of websites to search (optional, default: 3 websites = 30 credits)
number_of_scrolls: Number of infinite scrolls to perform on each website (optional)
Returns:
Dictionary containing search results and reference URLs
"""
url = f"{self.BASE_URL}/searchscraper"
data = {
"user_prompt": user_prompt
}
# Add num_results to the request if provided
if num_results is not None:
data["num_results"] = num_results
# Add number_of_scrolls to the request if provided
if number_of_scrolls is not None:
data["number_of_scrolls"] = number_of_scrolls
response = self.client.post(url, headers=self.headers, json=data)
if response.status_code != 200:
error_msg = f"Error {response.status_code}: {response.text}"
raise Exception(error_msg)
return response.json()
def scrape(self, website_url: str, render_heavy_js: Optional[bool] = None) -> Dict[str, Any]:
"""
Basic scrape endpoint to fetch page content.
Args:
website_url: URL to scrape
render_heavy_js: Whether to render heavy JS (optional)
Returns:
Dictionary containing the scraped result
"""
url = f"{self.BASE_URL}/scrape"
payload: Dict[str, Any] = {"website_url": website_url}
if render_heavy_js is not None:
payload["render_heavy_js"] = render_heavy_js
response = self.client.post(url, headers=self.headers, json=payload)
response.raise_for_status()
return response.json()
def sitemap(self, website_url: str) -> Dict[str, Any]:
"""
Extract sitemap for a given website.
Args:
website_url: Base website URL
Returns:
Dictionary containing sitemap URLs/structure
"""
url = f"{self.BASE_URL}/sitemap"
payload: Dict[str, Any] = {"website_url": website_url}
response = self.client.post(url, headers=self.headers, json=payload)
response.raise_for_status()
return response.json()
def agentic_scrapper(
self,
url: str,
user_prompt: Optional[str] = None,
output_schema: Optional[Dict[str, Any]] = None,
steps: Optional[List[str]] = None,
ai_extraction: Optional[bool] = None,
persistent_session: Optional[bool] = None,
timeout_seconds: Optional[float] = None,
) -> Dict[str, Any]:
"""
Run the Agentic Scraper workflow (no live session/browser interaction).
Args:
url: Target website URL
user_prompt: Instructions for what to do/extract (optional)
output_schema: Desired structured output schema (optional)
steps: High-level steps/instructions for the agent (optional)
ai_extraction: Whether to enable AI extraction mode (optional)
persistent_session: Whether to keep session alive between steps (optional)
timeout_seconds: Per-request timeout override in seconds (optional)
"""
endpoint = f"{self.BASE_URL}/agentic-scrapper"
payload: Dict[str, Any] = {"url": url}
if user_prompt is not None:
payload["user_prompt"] = user_prompt
if output_schema is not None:
payload["output_schema"] = output_schema
if steps is not None:
payload["steps"] = steps
if ai_extraction is not None:
payload["ai_extraction"] = ai_extraction
if persistent_session is not None:
payload["persistent_session"] = persistent_session
if timeout_seconds is not None:
response = self.client.post(endpoint, headers=self.headers, json=payload, timeout=timeout_seconds)
else:
response = self.client.post(endpoint, headers=self.headers, json=payload)
response.raise_for_status()
return response.json()
def smartcrawler_initiate(
self,
url: str,
prompt: str = None,
extraction_mode: str = "ai",
depth: int = None,
max_pages: int = None,
same_domain_only: bool = None
) -> Dict[str, Any]:
"""
Initiate a SmartCrawler request for multi-page web crawling.
SmartCrawler supports two modes:
- AI Extraction Mode (10 credits per page): Extracts structured data based on your prompt
- Markdown Conversion Mode (2 credits per page): Converts pages to clean markdown
Smartcrawler takes some time to process the request and returns the request id.
Use smartcrawler_fetch_results to get the results of the request.
You have to keep polling the smartcrawler_fetch_results until the request is complete.
The request is complete when the status is "completed".
Args:
url: Starting URL to crawl
prompt: AI prompt for data extraction (required for AI mode)
extraction_mode: "ai" for AI extraction or "markdown" for markdown conversion (default: "ai")
depth: Maximum link traversal depth (optional)
max_pages: Maximum number of pages to crawl (optional)
same_domain_only: Whether to crawl only within the same domain (optional)
Returns:
Dictionary containing the request ID for async processing
"""
endpoint = f"{self.BASE_URL}/crawl"
data = {
"url": url
}
# Handle extraction mode
if extraction_mode == "markdown":
data["markdown_only"] = True
elif extraction_mode == "ai":
if prompt is None:
raise ValueError("prompt is required when extraction_mode is 'ai'")
data["prompt"] = prompt
else:
raise ValueError(f"Invalid extraction_mode: {extraction_mode}. Must be 'ai' or 'markdown'")
if depth is not None:
data["depth"] = depth
if max_pages is not None:
data["max_pages"] = max_pages
if same_domain_only is not None:
data["same_domain_only"] = same_domain_only
response = self.client.post(endpoint, headers=self.headers, json=data)
if response.status_code != 200:
error_msg = f"Error {response.status_code}: {response.text}"
raise Exception(error_msg)
return response.json()
def smartcrawler_fetch_results(self, request_id: str) -> Dict[str, Any]:
"""
Fetch the results of a SmartCrawler operation.
Args:
request_id: The request ID returned by smartcrawler_initiate
Returns:
Dictionary containing the crawled data (structured extraction or markdown)
and metadata about processed pages
Note:
It takes some time to process the request and returns the results.
Meanwhile it returns the status of the request.
You have to keep polling the smartcrawler_fetch_results until the request is complete.
The request is complete when the status is "completed". and you get results
Keep polling the smartcrawler_fetch_results until the request is complete.
"""
endpoint = f"{self.BASE_URL}/crawl/{request_id}"
response = self.client.get(endpoint, headers=self.headers)
if response.status_code != 200:
error_msg = f"Error {response.status_code}: {response.text}"
raise Exception(error_msg)
return response.json()
def close(self) -> None:
"""Close the HTTP client."""
self.client.close()
# Create MCP server
mcp = FastMCP("ScapeGraph API MCP Server")
# Default API key (will be overridden in main or by direct assignment)
default_api_key = os.environ.get("SGAI_API_KEY")
scrapegraph_client = ScapeGraphClient(default_api_key) if default_api_key else None
# Add tool for markdownify
@mcp.tool()
def markdownify(website_url: str) -> Dict[str, Any]:
"""
Convert a webpage into clean, formatted markdown.
Args:
website_url: URL of the webpage to convert
Returns:
Dictionary containing the markdown result
"""
if scrapegraph_client is None:
return {"error": "ScapeGraph client not initialized. Please provide an API key."}
try:
return scrapegraph_client.markdownify(website_url)
except Exception as e:
return {"error": str(e)}
# Add tool for smartscraper
@mcp.tool()
def smartscraper(
user_prompt: str,
website_url: str,
number_of_scrolls: int = None,
markdown_only: bool = None
) -> Dict[str, Any]:
"""
Extract structured data from a webpage using AI.
Args:
user_prompt: Instructions for what data to extract
website_url: URL of the webpage to scrape
number_of_scrolls: Number of infinite scrolls to perform (optional)
markdown_only: Whether to return only markdown content without AI processing (optional)
Returns:
Dictionary containing the extracted data or markdown content
"""
if scrapegraph_client is None:
return {"error": "ScapeGraph client not initialized. Please provide an API key."}
try:
return scrapegraph_client.smartscraper(user_prompt, website_url, number_of_scrolls, markdown_only)
except Exception as e:
return {"error": str(e)}
# Add tool for searchscraper
@mcp.tool()
def searchscraper(
user_prompt: str,
num_results: int = None,
number_of_scrolls: int = None
) -> Dict[str, Any]:
"""
Perform AI-powered web searches with structured results.
Args:
user_prompt: Search query or instructions
num_results: Number of websites to search (optional, default: 3 websites = 30 credits)
number_of_scrolls: Number of infinite scrolls to perform on each website (optional)
Returns:
Dictionary containing search results and reference URLs
"""
if scrapegraph_client is None:
return {"error": "ScapeGraph client not initialized. Please provide an API key."}
try:
return scrapegraph_client.searchscraper(user_prompt, num_results, number_of_scrolls)
except Exception as e:
return {"error": str(e)}
# Add tool for SmartCrawler initiation
@mcp.tool()
def smartcrawler_initiate(
url: str,
prompt: str = None,
extraction_mode: str = "ai",
depth: int = None,
max_pages: int = None,
same_domain_only: bool = None
) -> Dict[str, Any]:
"""
Initiate a SmartCrawler request for intelligent multi-page web crawling.
SmartCrawler supports two modes:
- AI Extraction Mode (10 credits per page): Extracts structured data based on your prompt
- Markdown Conversion Mode (2 credits per page): Converts pages to clean markdown
Args:
url: Starting URL to crawl
prompt: AI prompt for data extraction (required for AI mode)
extraction_mode: "ai" for AI extraction or "markdown" for markdown conversion (default: "ai")
depth: Maximum link traversal depth (optional)
max_pages: Maximum number of pages to crawl (optional)
same_domain_only: Whether to crawl only within the same domain (optional)
Returns:
Dictionary containing the request ID for async processing
"""
if scrapegraph_client is None:
return {"error": "ScapeGraph client not initialized. Please provide an API key."}
try:
return scrapegraph_client.smartcrawler_initiate(
url=url,
prompt=prompt,
extraction_mode=extraction_mode,
depth=depth,
max_pages=max_pages,
same_domain_only=same_domain_only
)
except Exception as e:
return {"error": str(e)}
# Add tool for fetching SmartCrawler results
@mcp.tool()
def smartcrawler_fetch_results(request_id: str) -> Dict[str, Any]:
"""
Fetch the results of a SmartCrawler operation.
Args:
request_id: The request ID returned by smartcrawler_initiate
Returns:
Dictionary containing the crawled data (structured extraction or markdown)
and metadata about processed pages
"""
if scrapegraph_client is None:
return {"error": "ScapeGraph client not initialized. Please provide an API key."}
try:
return scrapegraph_client.smartcrawler_fetch_results(request_id)
except Exception as e:
return {"error": str(e)}
# Add tool for basic scrape
@mcp.tool()
def scrape(website_url: str, render_heavy_js: Optional[bool] = None) -> Dict[str, Any]:
"""
Fetch page content for a URL.
Args:
website_url: URL to scrape
render_heavy_js: Whether to render heavy JS (optional)
"""
if scrapegraph_client is None:
return {"error": "ScapeGraph client not initialized. Please provide an API key."}
try:
return scrapegraph_client.scrape(website_url=website_url, render_heavy_js=render_heavy_js)
except httpx.HTTPError as http_err:
return {"error": str(http_err)}
except ValueError as val_err:
return {"error": str(val_err)}
# Add tool for sitemap extraction
@mcp.tool()
def sitemap(website_url: str) -> Dict[str, Any]:
"""
Extract sitemap for a website.
Args:
website_url: Base website URL
"""
if scrapegraph_client is None:
return {"error": "ScapeGraph client not initialized. Please provide an API key."}
try:
return scrapegraph_client.sitemap(website_url=website_url)
except httpx.HTTPError as http_err:
return {"error": str(http_err)}
except ValueError as val_err:
return {"error": str(val_err)}
# Add tool for Agentic Scraper (no live session/browser interaction)
@mcp.tool()
def agentic_scrapper(
url: str,
user_prompt: Optional[str] = None,
output_schema: Optional[Union[str, Dict[str, Any]]] = None,
steps: Optional[Union[str, List[str]]] = None,
ai_extraction: Optional[bool] = None,
persistent_session: Optional[bool] = None,
timeout_seconds: Optional[float] = None,
) -> Dict[str, Any]:
"""
Run the Agentic Scraper workflow. Accepts flexible input forms for steps and schema.
"""
if scrapegraph_client is None:
return {"error": "ScapeGraph client not initialized. Please provide an API key."}
# Normalize inputs
normalized_steps: Optional[List[str]] = None
if isinstance(steps, list):
normalized_steps = steps
elif isinstance(steps, str):
parsed_steps: Optional[Any] = None
try:
parsed_steps = json.loads(steps)
except json.JSONDecodeError:
parsed_steps = None
if isinstance(parsed_steps, list):
normalized_steps = parsed_steps
else:
normalized_steps = [steps]
normalized_schema: Optional[Dict[str, Any]] = None
if isinstance(output_schema, dict):
normalized_schema = output_schema
elif isinstance(output_schema, str):
try:
parsed_schema = json.loads(output_schema)
if isinstance(parsed_schema, dict):
normalized_schema = parsed_schema
else:
return {"error": "output_schema must be a JSON object"}
except json.JSONDecodeError as e:
return {"error": f"Invalid JSON for output_schema: {str(e)}"}
try:
return scrapegraph_client.agentic_scrapper(
url=url,
user_prompt=user_prompt,
output_schema=normalized_schema,
steps=normalized_steps,
ai_extraction=ai_extraction,
persistent_session=persistent_session,
timeout_seconds=timeout_seconds,
)
except httpx.TimeoutException as timeout_err:
return {"error": f"Request timed out: {str(timeout_err)}"}
except httpx.HTTPError as http_err:
return {"error": str(http_err)}
except ValueError as val_err:
return {"error": str(val_err)}
def main() -> None:
"""Run the ScapeGraph MCP server."""
print("Starting ScapeGraph MCP server!")
# Run the server
mcp.run(transport="stdio")
if __name__ == "__main__":
main()