├── .python-version ├── images └── firecrawl-mcp-examples-github-banner.png ├── pyproject.toml ├── .gitignore ├── README.md ├── server.py └── uv.lock /.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /images/firecrawl-mcp-examples-github-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexfazio/firecrawl-mcp-example/main/images/firecrawl-mcp-examples-github-banner.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "firecrawl-mcp-example" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [ 8 | "httpx>=0.28.1", 9 | "mcp[cli]>=1.4.1", 10 | "python-dotenv>=1.0.0", 11 | ] 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore the mcp_help folder entirely 2 | mcp_help/ 3 | 4 | # Python 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | *.so 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # Virtual Environment 27 | venv/ 28 | ENV/ 29 | .venv/ 30 | 31 | # Environment variables 32 | .env 33 | 34 | # IDE files 35 | .idea/ 36 | .vscode/ 37 | *.swp 38 | *.swo 39 | 40 | # OS specific files 41 | .DS_Store 42 | .DS_Store? 43 | ._* 44 | .Spotlight-V100 45 | .Trashes 46 | ehthumbs.db 47 | Thumbs.db 48 | 49 | # Logs 50 | *.log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Firecrawl Quickstarts Logo 5 | 6 | 7 | 8 | # Firecrawl MCP Examples 9 | 10 | A collection of Model Context Protocol (MCP) servers that provide tools for Claude and other LLMs to interact with various data sources using Firecrawl API. 11 | 12 | ## Examples 13 | 14 | ### Hacker News MCP Server 15 | 16 | A server that provides tools for: 17 | 18 | 1. Accessing Hacker News content and discussions 19 | 2. Scraping websites using the Firecrawl API 20 | 3. Searching for Hacker News discussions on any topic 21 | 22 | Features: 23 | - `get_hnews_item`: Get details about a specific Hacker News post or comment 24 | - `get_hnews_popular_discussions`: Get the top 30 discussions on Hacker News 25 | - `firecrawl_scrape_url`: Extract clean, readable content from any URL 26 | - `search_hnews`: Search for Hacker News discussions about a specific topic 27 | 28 | ## Setup 29 | 30 | ### Requirements 31 | 32 | - Python 3.10+ 33 | - [UV package manager](https://github.com/astral-sh/uv) 34 | - Firecrawl API key (stored in `.env` file) 35 | 36 | ### Installation 37 | 38 | 1. Clone the repository 39 | 2. Install dependencies: 40 | ```bash 41 | uv venv 42 | source .venv/bin/activate # On Windows: .venv\Scripts\activate 43 | uv install -e . 44 | ``` 45 | 3. Create a `.env` file with your Firecrawl API key: 46 | ``` 47 | FIRECRAWL_API_KEY=your-api-key-here 48 | ``` 49 | 50 | ### Usage with Claude Desktop 51 | 52 | 1. Configure Claude Desktop to use this MCP server (see Claude Desktop documentation) 53 | 2. Start the server: 54 | ```bash 55 | uv run server.py 56 | ``` 57 | 3. In Claude Desktop, you'll have access to Hacker News tools and web scraping capabilities 58 | 59 | ## License 60 | 61 | MIT License 62 | 63 | [![Watch the video](https://i.imgur.com/E5hfFIz.png)](https://x.com/alxfazio/status/1901690377364476400) -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from typing import Optional, Dict, Any, List, Union 4 | import httpx 5 | from datetime import datetime 6 | import time 7 | import asyncio 8 | from mcp.server.fastmcp import FastMCP 9 | from dotenv import load_dotenv 10 | 11 | # ==================== SERVER INITIALIZATION START ==================== 12 | # Load environment variables from .env file 13 | load_dotenv() 14 | 15 | # Get the Firecrawl API key from environment variables 16 | FIRECRAWL_API_KEY = os.getenv("FIRECRAWL_API_KEY") 17 | 18 | # Initialize the FastMCP server with a unique name 19 | mcp = FastMCP("hn-firecrawl-service") 20 | 21 | # Base URL for the Hacker News API 22 | HN_API_BASE = "https://hacker-news.firebaseio.com/v0" 23 | # ==================== SERVER INITIALIZATION END ==================== 24 | 25 | # ==================== HELPER FUNCTIONS START ==================== 26 | 27 | # ==================== API CORE HELPERS ==================== 28 | 29 | async def make_hn_request(endpoint: str) -> Optional[Union[Dict[str, Any], List[int], int]]: 30 | """Make a request to the Hacker News API with proper error handling. 31 | 32 | Args: 33 | endpoint: The API endpoint to query (without the base URL) 34 | 35 | Returns: 36 | The JSON response data, or None if the request failed 37 | """ 38 | url = f"{HN_API_BASE}/{endpoint}" 39 | 40 | async with httpx.AsyncClient() as client: 41 | try: 42 | response = await client.get(url, timeout=10.0) 43 | response.raise_for_status() 44 | return response.json() 45 | except httpx.RequestError: 46 | return None 47 | except httpx.HTTPStatusError: 48 | return None 49 | except Exception: 50 | return None 51 | 52 | async def get_item(item_id: int) -> Optional[Dict[str, Any]]: 53 | """Fetch an item from the Hacker News API by its ID. 54 | 55 | Args: 56 | item_id: The ID of the item to fetch 57 | 58 | Returns: 59 | The item data, or None if not found 60 | """ 61 | result = await make_hn_request(f"item/{item_id}.json") 62 | if isinstance(result, dict): 63 | return result 64 | return None 65 | 66 | # ==================== FIRECRAWL HELPERS ==================== 67 | 68 | async def firecrawl_scrape_md(url: str) -> str: 69 | """Scrape a URL and return the content in Markdown format using Firecrawl. 70 | 71 | Args: 72 | url: The URL to scrape 73 | """ 74 | if not FIRECRAWL_API_KEY: 75 | return "Error: Firecrawl API key not found in environment variables" 76 | 77 | # Using the correct endpoint from the API spec 78 | firecrawl_url = "https://api.firecrawl.dev/v1/scrape" 79 | headers = { 80 | "Authorization": f"Bearer {FIRECRAWL_API_KEY}", 81 | "Content-Type": "application/json" 82 | } 83 | 84 | # Using the correct payload structure from the API spec 85 | payload = { 86 | "url": url, 87 | "formats": ["markdown"] 88 | } 89 | 90 | try: 91 | async with httpx.AsyncClient() as client: 92 | response = await client.post( 93 | firecrawl_url, 94 | headers=headers, 95 | json=payload, 96 | timeout=60.0 97 | ) 98 | response.raise_for_status() 99 | 100 | result = response.json() 101 | 102 | # According to the API spec, the response structure is: 103 | # { "success": true, "data": { "markdown": "..." } } 104 | if result.get("success") and "data" in result and "markdown" in result["data"]: 105 | return result["data"]["markdown"] 106 | else: 107 | return f"Error scraping {url}: No markdown content returned. Response: {json.dumps(result)}" 108 | 109 | except httpx.HTTPStatusError as e: 110 | return f"HTTP error while scraping {url}: {str(e)}" 111 | except httpx.RequestError as e: 112 | return f"Request error while scraping {url}: {str(e)}" 113 | except Exception as e: 114 | return f"Unexpected error while scraping {url}: {str(e)}" 115 | 116 | # ==================== HACKER NEWS HELPERS ==================== 117 | 118 | async def hnews_get_post_title(post_data: Dict[str, Any]) -> str: 119 | """Extract the title from a Hacker News post.""" 120 | return post_data.get("title", "No title available") 121 | 122 | async def hnews_get_post_id(post_data: Dict[str, Any]) -> int: 123 | """Extract the ID from a Hacker News post.""" 124 | return post_data.get("id", 0) 125 | 126 | async def hnews_get_post_author(post_data: Dict[str, Any]) -> str: 127 | """Extract the author from a Hacker News post.""" 128 | return post_data.get("by", "Anonymous") 129 | 130 | async def hnews_get_post_score(post_data: Dict[str, Any]) -> int: 131 | """Extract the score from a Hacker News post.""" 132 | return post_data.get("score", 0) 133 | 134 | async def hnews_get_post_time(post_data: Dict[str, Any]) -> str: 135 | """Extract the time from a Hacker News post and format it as a relative time.""" 136 | timestamp = post_data.get("time", 0) 137 | current_time = int(time.time()) 138 | diff = current_time - timestamp 139 | 140 | if diff < 60: 141 | return "just now" 142 | elif diff < 3600: 143 | minutes = diff // 60 144 | return f"{minutes} minute{'s' if minutes > 1 else ''} ago" 145 | elif diff < 86400: 146 | hours = diff // 3600 147 | return f"{hours} hour{'s' if hours > 1 else ''} ago" 148 | else: 149 | days = diff // 86400 150 | return f"{days} day{'s' if days > 1 else ''} ago" 151 | 152 | async def hnews_get_post_time_absolute(post_data: Dict[str, Any]) -> str: 153 | """Extract the time from a Hacker News post and format it as an absolute time.""" 154 | timestamp = post_data.get("time", 0) 155 | dt = datetime.fromtimestamp(timestamp) 156 | return dt.strftime("%Y-%m-%d %H:%M:%S") 157 | 158 | async def hnews_get_post_type(post_data: Dict[str, Any]) -> str: 159 | """Extract the type from a Hacker News post.""" 160 | return post_data.get("type", "unknown") 161 | 162 | async def hnews_get_post_content_text(post_data: Dict[str, Any]) -> str: 163 | """Extract the text content from a Hacker News post.""" 164 | return post_data.get("text", "No text content available") 165 | 166 | async def hnews_get_post_discussion_url(post_data: Dict[str, Any]) -> str: 167 | """Generate the discussion URL for a Hacker News post.""" 168 | post_id = post_data.get("id", 0) 169 | return f"https://news.ycombinator.com/item?id={post_id}" 170 | 171 | async def hnews_get_post_external_article_url(post_data: Dict[str, Any]) -> str: 172 | """Extract the external article URL from a Hacker News post.""" 173 | return post_data.get("url", "No external URL available") 174 | 175 | async def hnews_get_post_comment_count(post_data: Dict[str, Any]) -> int: 176 | """Extract the comment count from a Hacker News post.""" 177 | # Use descendants if available (total count including nested comments) 178 | # Fall back to kids length (only direct comments) 179 | return post_data.get("descendants", len(post_data.get("kids", []))) 180 | 181 | async def hnews_get_post_metadata(post_data: Dict[str, Any]) -> str: 182 | """Format comprehensive metadata about a Hacker News post.""" 183 | title = await hnews_get_post_title(post_data) 184 | author = await hnews_get_post_author(post_data) 185 | score = await hnews_get_post_score(post_data) 186 | time = await hnews_get_post_time_absolute(post_data) 187 | comment_count = await hnews_get_post_comment_count(post_data) 188 | 189 | return f"Title: {title}\nAuthor: {author}\nScore: {score}\nTime: {time}\nComments: {comment_count}" 190 | 191 | # ==================== HACKER NEWS TOPLIST HELPERS ==================== 192 | 193 | async def hnews_get_toplist_articles() -> List[int]: 194 | """Fetch the current top stories from Hacker News.""" 195 | result = await make_hn_request("topstories.json") 196 | if isinstance(result, list): 197 | return result[:30] # Return top 30 stories for efficiency 198 | return [] 199 | 200 | async def hnews_get_toplist_article_id(toplist_item: Dict[str, Any]) -> int: 201 | """Extract the ID from a toplist article.""" 202 | return toplist_item.get("id", 0) 203 | 204 | async def hnews_get_toplist_article_title(toplist_item: Dict[str, Any]) -> str: 205 | """Extract the title from a toplist article.""" 206 | return toplist_item.get("title", "No title available") 207 | 208 | async def hnews_get_toplist_article_rank(toplist_item: Dict[str, Any], index: int) -> int: 209 | """Get the rank of an article in the toplist.""" 210 | return index + 1 211 | 212 | async def hnews_get_toplist_article_score(toplist_item: Dict[str, Any]) -> int: 213 | """Extract the score from a toplist article.""" 214 | return toplist_item.get("score", 0) 215 | 216 | async def hnews_get_toplist_article_author(toplist_item: Dict[str, Any]) -> str: 217 | """Extract the author from a toplist article.""" 218 | return toplist_item.get("by", "Anonymous") 219 | 220 | async def hnews_get_toplist_article_age(toplist_item: Dict[str, Any]) -> str: 221 | """Calculate the age of a toplist article.""" 222 | # Reuse the post time function which gives a relative time 223 | return await hnews_get_post_time(toplist_item) 224 | 225 | async def hnews_get_toplist_article_type(toplist_item: Dict[str, Any]) -> str: 226 | """Extract the type from a toplist article.""" 227 | return toplist_item.get("type", "unknown") 228 | 229 | async def hnews_get_toplist_external_article_url(toplist_item: Dict[str, Any]) -> str: 230 | """Extract the external article URL from a toplist article.""" 231 | return toplist_item.get("url", "No external URL available") 232 | 233 | async def hnews_get_toplist_discussion_url(toplist_item: Dict[str, Any]) -> str: 234 | """Generate the discussion URL for a toplist article.""" 235 | post_id = toplist_item.get("id", 0) 236 | return f"https://news.ycombinator.com/item?id={post_id}" 237 | 238 | async def hnews_get_toplist_comment_count(toplist_item: Dict[str, Any]) -> int: 239 | """Extract the comment count from a toplist article.""" 240 | return len(toplist_item.get("kids", [])) 241 | 242 | # ==================== GOOGLE SEARCH HELPERS ==================== 243 | 244 | async def search_google_for_hnews(query: str) -> List[Dict[str, str]]: 245 | """Helper function to search Google for Hacker News content. 246 | 247 | Args: 248 | query: The search query 249 | """ 250 | # Format the query for a Google search limited to Hacker News 251 | formatted_query = query.replace(" ", "+") 252 | search_url = f"https://www.google.com/search?q=site:news.ycombinator.com+{formatted_query}" 253 | 254 | # Use Firecrawl to get the search results page 255 | markdown_content = await firecrawl_scrape_md(search_url) 256 | 257 | # Check if there's an error message from Firecrawl 258 | if markdown_content.startswith("Error:") or markdown_content.startswith("HTTP error"): 259 | print(f"Firecrawl error: {markdown_content}") 260 | return [] 261 | 262 | # Parse the results using a more robust approach 263 | results = [] 264 | 265 | # Split content into lines for processing 266 | lines = markdown_content.split("\n") 267 | 268 | i = 0 269 | while i < len(lines): 270 | line = lines[i].strip() 271 | 272 | # Look for links which are formatted as [text](url) in markdown 273 | if "news.ycombinator.com" in line and ("[" in line or "http" in line): 274 | result = {} 275 | 276 | # Extract URL - handle both markdown links [text](url) and plain URLs 277 | if "(" in line and ")" in line and "[" in line and "]" in line: 278 | # Markdown format: [title](url) 279 | url_start = line.find("(") + 1 280 | url_end = line.find(")", url_start) 281 | if url_start > 0 and url_end > url_start: 282 | url = line[url_start:url_end].strip() 283 | if "news.ycombinator.com" in url: 284 | result["url"] = url 285 | 286 | # Extract title from markdown link 287 | title_start = line.find("[") + 1 288 | title_end = line.find("]", title_start) 289 | if title_start > 0 and title_end > title_start: 290 | result["title"] = line[title_start:title_end].strip() 291 | else: 292 | # Try to find a plain URL 293 | words = line.split() 294 | for word in words: 295 | if word.startswith("http") and "news.ycombinator.com" in word: 296 | result["url"] = word.strip() 297 | break 298 | 299 | # If we have a URL but no title, look in surrounding lines for title 300 | if result.get("url") and not result.get("title"): 301 | # Look at current line first 302 | potential_title = line.strip() 303 | if "http" in potential_title: 304 | # Remove the URL part for cleaner title 305 | url_index = potential_title.find("http") 306 | if url_index > 0: 307 | potential_title = potential_title[:url_index].strip() 308 | 309 | # If current line doesn't have a good title, check previous and next lines 310 | if not potential_title or len(potential_title) < 5: 311 | if i > 0: 312 | prev_line = lines[i-1].strip() 313 | if prev_line and "http" not in prev_line and len(prev_line) > 5: 314 | potential_title = prev_line 315 | if (not potential_title or len(potential_title) < 5) and i < len(lines)-1: 316 | next_line = lines[i+1].strip() 317 | if next_line and "http" not in next_line and len(next_line) > 5: 318 | potential_title = next_line 319 | 320 | if potential_title and len(potential_title) > 5: 321 | result["title"] = potential_title 322 | 323 | # If we still don't have a title, use a placeholder 324 | if result.get("url") and not result.get("title"): 325 | result["title"] = "Hacker News Discussion" 326 | 327 | # Look for description in next few lines 328 | description_lines = [] 329 | j = i + 1 330 | while j < min(i + 5, len(lines)) and not result.get("description"): 331 | next_line = lines[j].strip() 332 | # Skip empty lines or lines that look like URLs 333 | if next_line and "http" not in next_line and not next_line.startswith("..."): 334 | description_lines.append(next_line) 335 | j += 1 336 | 337 | if description_lines: 338 | result["description"] = " ".join(description_lines) 339 | else: 340 | result["description"] = "No description available" 341 | 342 | # Add to results if we have the minimum required data 343 | if result.get("url") and result.get("title"): 344 | # Clean up the URL if needed (remove trailing punctuation) 345 | if result["url"][-1] in '.,:;)]}': 346 | result["url"] = result["url"][:-1] 347 | 348 | # Make sure it's a complete URL 349 | if not result["url"].startswith("http"): 350 | result["url"] = "https://" + result["url"] 351 | 352 | results.append(result) 353 | 354 | i += 1 355 | 356 | # If we couldn't find structured results, try to extract any HN URLs as a fallback 357 | if not results: 358 | for line in lines: 359 | if "news.ycombinator.com/item?id=" in line: 360 | # Find the URL 361 | start_idx = line.find("news.ycombinator.com") 362 | if start_idx > 0: 363 | # Look for the start of the URL 364 | url_start = line.rfind("http", 0, start_idx) 365 | if url_start == -1: # No http found, try with www 366 | url_start = line.rfind("www", 0, start_idx) 367 | if url_start == -1: # Still not found, just use the domain 368 | url_start = start_idx 369 | url = "https://" + line[url_start:] 370 | else: 371 | url = line[url_start:] 372 | 373 | # Find the end of the URL 374 | url_end = len(url) 375 | for char in [' ', ')', ']', '"', "'"]: 376 | pos = url.find(char) 377 | if pos != -1 and pos < url_end: 378 | url_end = pos 379 | 380 | url = url[:url_end] 381 | 382 | results.append({ 383 | "url": url, 384 | "title": "Hacker News Discussion", 385 | "description": "Found through Google search" 386 | }) 387 | 388 | return results 389 | 390 | # ==================== HELPER FUNCTIONS END ==================== 391 | 392 | # ==================== MCP TOOLS START ==================== 393 | @mcp.tool() 394 | async def get_hnews_item(item_id: int) -> str: 395 | """Retrieve detailed information about a specific Hacker News item. 396 | 397 | This tool fetches comprehensive details about a Hacker News post or comment 398 | by its ID, including metadata, content, and relevant URLs. 399 | 400 | Args: 401 | item_id: The numeric identifier of the Hacker News item (e.g., 34159862) 402 | """ 403 | # Fetch the item data from Hacker News API 404 | item_data = await get_item(item_id) 405 | 406 | if not item_data: 407 | return f"Could not find Hacker News item with ID {item_id}" 408 | 409 | # Extract item properties using helper functions 410 | title = await hnews_get_post_title(item_data) 411 | author = await hnews_get_post_author(item_data) 412 | score = await hnews_get_post_score(item_data) 413 | post_time = await hnews_get_post_time_absolute(item_data) 414 | relative_time = await hnews_get_post_time(item_data) 415 | item_type = await hnews_get_post_type(item_data) 416 | comment_count = await hnews_get_post_comment_count(item_data) 417 | content = await hnews_get_post_content_text(item_data) 418 | 419 | # Get URLs 420 | article_url = await hnews_get_post_external_article_url(item_data) 421 | discussion_url = await hnews_get_post_discussion_url(item_data) 422 | 423 | # Format response in a structured way 424 | response = f"# {title}\n\n" 425 | response += f"**Type:** {item_type}\n" 426 | response += f"**Author:** {author}\n" 427 | response += f"**Posted:** {post_time} ({relative_time})\n" 428 | 429 | if score > 0: 430 | response += f"**Score:** {score} points\n" 431 | 432 | if comment_count > 0: 433 | response += f"**Comments:** {comment_count}\n" 434 | 435 | response += "\n**Links:**\n" 436 | response += f"- Discussion: {discussion_url}\n" 437 | 438 | if article_url != "No external URL available": 439 | response += f"- Article: {article_url}\n" 440 | 441 | if content != "No text content available": 442 | response += f"\n**Content:**\n{content}\n" 443 | 444 | # Add children info if available 445 | if "kids" in item_data and item_data["kids"]: 446 | response += f"\n**Has {len(item_data['kids'])} direct replies**\n" 447 | 448 | return response 449 | 450 | @mcp.tool() 451 | async def get_hnews_popular_discussions() -> str: 452 | """Retrieve today's top discussions on Hacker News. 453 | 454 | This tool fetches the current top 30 stories from Hacker News, 455 | sorted by score. For each story, it provides the title, author, 456 | score, comment count, post time, and relevant URLs. 457 | """ 458 | # Get the top stories IDs 459 | top_ids = await hnews_get_toplist_articles() 460 | 461 | if not top_ids: 462 | return "Could not fetch popular discussions from Hacker News" 463 | 464 | # Limit to top 30 as specified 465 | top_ids = top_ids[:30] 466 | 467 | # Fetch details for each story 468 | discussions = [] 469 | for i, story_id in enumerate(top_ids): 470 | story_data = await get_item(story_id) 471 | if story_data: 472 | # Use helper functions to extract all required information 473 | rank = i + 1 474 | title = await hnews_get_toplist_article_title(story_data) 475 | score = await hnews_get_toplist_article_score(story_data) 476 | author = await hnews_get_toplist_article_author(story_data) 477 | age = await hnews_get_toplist_article_age(story_data) 478 | post_type = await hnews_get_toplist_article_type(story_data) 479 | comments = await hnews_get_toplist_comment_count(story_data) 480 | 481 | # Get URLs 482 | discussion_url = await hnews_get_toplist_discussion_url(story_data) 483 | article_url = await hnews_get_toplist_external_article_url(story_data) 484 | 485 | # Get content as a summary 486 | content = await hnews_get_post_content_text(story_data) 487 | if content != "No text content available": 488 | # Limit content to a reasonable summary length 489 | if len(content) > 200: 490 | content = content[:197] + "..." 491 | summary = f"\n Summary: {content}" 492 | else: 493 | summary = "" 494 | 495 | # Format each discussion with all required information 496 | discussion_info = f"#{rank}: {title}\n" 497 | discussion_info += f" Type: {post_type} | Score: {score} points | By: {author} | Posted: {age}\n" 498 | discussion_info += f" Comments: {comments}\n" 499 | 500 | # Add URLs 501 | discussion_info += f" Discussion: {discussion_url}\n" 502 | if article_url != "No external URL available": 503 | discussion_info += f" Article: {article_url}" 504 | 505 | # Add summary if available 506 | discussion_info += summary 507 | 508 | discussions.append(discussion_info) 509 | 510 | if not discussions: 511 | return "No discussions found" 512 | 513 | return "# TOP 30 HACKER NEWS DISCUSSIONS\n\n" + "\n\n".join(discussions) 514 | 515 | @mcp.tool() 516 | async def firecrawl_scrape_url(url: str) -> str: 517 | """Extract clean, readable content from any web page. 518 | 519 | This tool scrapes the specified URL and returns the content in a clean, 520 | readable Markdown format. It handles JavaScript-rendered sites and complex 521 | layouts, extracting just the main content. 522 | 523 | Args: 524 | url: The complete web address to scrape (e.g., "https://example.com/page") 525 | """ 526 | markdown_content = await firecrawl_scrape_md(url) 527 | 528 | # Check if there was an error in scraping 529 | if markdown_content.startswith("Error:") or markdown_content.startswith("HTTP error"): 530 | return f"Failed to scrape {url}: {markdown_content}" 531 | 532 | # Limit the response length if needed 533 | if len(markdown_content) > 8000: 534 | markdown_content = markdown_content[:8000] + "...\n\n[Content truncated due to length]" 535 | 536 | return f"# Content from {url}\n\n{markdown_content}" 537 | 538 | @mcp.tool() 539 | async def search_hnews(query: str) -> str: 540 | """Search for Hacker News discussions matching specific keywords. 541 | 542 | This tool performs a targeted search for Hacker News content using Google 543 | and returns relevant discussions with their titles, URLs, and brief descriptions. 544 | Results are sorted by relevance to your query. 545 | 546 | Args: 547 | query: The search terms to find in Hacker News discussions (e.g., "Python web frameworks", "startup funding") 548 | """ 549 | if not query.strip(): 550 | return "Error: Search query cannot be empty." 551 | 552 | # Search for HN content via Google 553 | search_results = await search_google_for_hnews(query) 554 | 555 | # Format the results 556 | if not search_results: 557 | return f"No Hacker News discussions found for query: '{query}'" 558 | 559 | formatted_results = [] 560 | for i, result in enumerate(search_results[:10]): # Limit to top 10 results 561 | title = result.get("title", "Untitled Discussion") 562 | url = result.get("url", "") 563 | description = result.get("description", "No description available") 564 | 565 | # Format each result with clear structure and markdown 566 | formatted_results.append( 567 | f"### {i+1}. {title}\n" 568 | f"**Link**: [{url}]({url})\n" 569 | f"**Description**: {description}\n" 570 | ) 571 | 572 | header = f"# SEARCH RESULTS FOR '{query}' ON HACKER NEWS\n\n" 573 | footer = "\n\n*Results retrieved via Google site-specific search*" 574 | 575 | return header + "\n".join(formatted_results) + footer 576 | 577 | # ==================== MCP TOOLS END ==================== 578 | 579 | # ==================== SERVER EXECUTION START ==================== 580 | if __name__ == "__main__": 581 | # Initialize and run the server with stdio transport 582 | mcp.run(transport='stdio') 583 | # ==================== SERVER EXECUTION END ==================== -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.10" 3 | 4 | [[package]] 5 | name = "annotated-types" 6 | version = "0.7.0" 7 | source = { registry = "https://pypi.org/simple" } 8 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 9 | wheels = [ 10 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, 11 | ] 12 | 13 | [[package]] 14 | name = "anyio" 15 | version = "4.8.0" 16 | source = { registry = "https://pypi.org/simple" } 17 | dependencies = [ 18 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 19 | { name = "idna" }, 20 | { name = "sniffio" }, 21 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 22 | ] 23 | sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } 24 | wheels = [ 25 | { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, 26 | ] 27 | 28 | [[package]] 29 | name = "certifi" 30 | version = "2025.1.31" 31 | source = { registry = "https://pypi.org/simple" } 32 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } 33 | wheels = [ 34 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, 35 | ] 36 | 37 | [[package]] 38 | name = "click" 39 | version = "8.1.8" 40 | source = { registry = "https://pypi.org/simple" } 41 | dependencies = [ 42 | { name = "colorama", marker = "platform_system == 'Windows'" }, 43 | ] 44 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } 45 | wheels = [ 46 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, 47 | ] 48 | 49 | [[package]] 50 | name = "colorama" 51 | version = "0.4.6" 52 | source = { registry = "https://pypi.org/simple" } 53 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 54 | wheels = [ 55 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 56 | ] 57 | 58 | [[package]] 59 | name = "exceptiongroup" 60 | version = "1.2.2" 61 | source = { registry = "https://pypi.org/simple" } 62 | sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } 63 | wheels = [ 64 | { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, 65 | ] 66 | 67 | [[package]] 68 | name = "firecrawl-mcp-example" 69 | version = "0.1.0" 70 | source = { virtual = "." } 71 | dependencies = [ 72 | { name = "httpx" }, 73 | { name = "mcp", extra = ["cli"] }, 74 | { name = "python-dotenv" }, 75 | ] 76 | 77 | [package.metadata] 78 | requires-dist = [ 79 | { name = "httpx", specifier = ">=0.28.1" }, 80 | { name = "mcp", extras = ["cli"], specifier = ">=1.4.1" }, 81 | { name = "python-dotenv", specifier = ">=1.0.0" }, 82 | ] 83 | 84 | [[package]] 85 | name = "h11" 86 | version = "0.14.0" 87 | source = { registry = "https://pypi.org/simple" } 88 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 89 | wheels = [ 90 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 91 | ] 92 | 93 | [[package]] 94 | name = "httpcore" 95 | version = "1.0.7" 96 | source = { registry = "https://pypi.org/simple" } 97 | dependencies = [ 98 | { name = "certifi" }, 99 | { name = "h11" }, 100 | ] 101 | sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } 102 | wheels = [ 103 | { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, 104 | ] 105 | 106 | [[package]] 107 | name = "httpx" 108 | version = "0.28.1" 109 | source = { registry = "https://pypi.org/simple" } 110 | dependencies = [ 111 | { name = "anyio" }, 112 | { name = "certifi" }, 113 | { name = "httpcore" }, 114 | { name = "idna" }, 115 | ] 116 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } 117 | wheels = [ 118 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, 119 | ] 120 | 121 | [[package]] 122 | name = "httpx-sse" 123 | version = "0.4.0" 124 | source = { registry = "https://pypi.org/simple" } 125 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } 126 | wheels = [ 127 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, 128 | ] 129 | 130 | [[package]] 131 | name = "idna" 132 | version = "3.10" 133 | source = { registry = "https://pypi.org/simple" } 134 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 135 | wheels = [ 136 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 137 | ] 138 | 139 | [[package]] 140 | name = "markdown-it-py" 141 | version = "3.0.0" 142 | source = { registry = "https://pypi.org/simple" } 143 | dependencies = [ 144 | { name = "mdurl" }, 145 | ] 146 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } 147 | wheels = [ 148 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, 149 | ] 150 | 151 | [[package]] 152 | name = "mcp" 153 | version = "1.4.1" 154 | source = { registry = "https://pypi.org/simple" } 155 | dependencies = [ 156 | { name = "anyio" }, 157 | { name = "httpx" }, 158 | { name = "httpx-sse" }, 159 | { name = "pydantic" }, 160 | { name = "pydantic-settings" }, 161 | { name = "sse-starlette" }, 162 | { name = "starlette" }, 163 | { name = "uvicorn" }, 164 | ] 165 | sdist = { url = "https://files.pythonhosted.org/packages/50/cc/5c5bb19f1a0f8f89a95e25cb608b0b07009e81fd4b031e519335404e1422/mcp-1.4.1.tar.gz", hash = "sha256:b9655d2de6313f9d55a7d1df62b3c3fe27a530100cc85bf23729145b0dba4c7a", size = 154942 } 166 | wheels = [ 167 | { url = "https://files.pythonhosted.org/packages/e8/0e/885f156ade60108e67bf044fada5269da68e29d758a10b0c513f4d85dd76/mcp-1.4.1-py3-none-any.whl", hash = "sha256:a7716b1ec1c054e76f49806f7d96113b99fc1166fc9244c2c6f19867cb75b593", size = 72448 }, 168 | ] 169 | 170 | [package.optional-dependencies] 171 | cli = [ 172 | { name = "python-dotenv" }, 173 | { name = "typer" }, 174 | ] 175 | 176 | [[package]] 177 | name = "mdurl" 178 | version = "0.1.2" 179 | source = { registry = "https://pypi.org/simple" } 180 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } 181 | wheels = [ 182 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, 183 | ] 184 | 185 | [[package]] 186 | name = "pydantic" 187 | version = "2.10.6" 188 | source = { registry = "https://pypi.org/simple" } 189 | dependencies = [ 190 | { name = "annotated-types" }, 191 | { name = "pydantic-core" }, 192 | { name = "typing-extensions" }, 193 | ] 194 | sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } 195 | wheels = [ 196 | { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, 197 | ] 198 | 199 | [[package]] 200 | name = "pydantic-core" 201 | version = "2.27.2" 202 | source = { registry = "https://pypi.org/simple" } 203 | dependencies = [ 204 | { name = "typing-extensions" }, 205 | ] 206 | sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } 207 | wheels = [ 208 | { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, 209 | { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, 210 | { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, 211 | { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, 212 | { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, 213 | { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, 214 | { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, 215 | { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, 216 | { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, 217 | { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, 218 | { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, 219 | { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, 220 | { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, 221 | { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, 222 | { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, 223 | { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, 224 | { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, 225 | { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, 226 | { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, 227 | { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, 228 | { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, 229 | { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, 230 | { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, 231 | { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, 232 | { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, 233 | { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, 234 | { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, 235 | { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, 236 | { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, 237 | { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, 238 | { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, 239 | { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, 240 | { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, 241 | { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, 242 | { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, 243 | { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, 244 | { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, 245 | { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, 246 | { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, 247 | { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, 248 | { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, 249 | { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, 250 | { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, 251 | { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, 252 | { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, 253 | { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, 254 | { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, 255 | { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, 256 | { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, 257 | { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, 258 | { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, 259 | { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, 260 | { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, 261 | { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, 262 | { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, 263 | { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, 264 | { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, 265 | { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, 266 | { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, 267 | { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, 268 | { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, 269 | { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, 270 | { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, 271 | { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, 272 | ] 273 | 274 | [[package]] 275 | name = "pydantic-settings" 276 | version = "2.8.1" 277 | source = { registry = "https://pypi.org/simple" } 278 | dependencies = [ 279 | { name = "pydantic" }, 280 | { name = "python-dotenv" }, 281 | ] 282 | sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } 283 | wheels = [ 284 | { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, 285 | ] 286 | 287 | [[package]] 288 | name = "pygments" 289 | version = "2.19.1" 290 | source = { registry = "https://pypi.org/simple" } 291 | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } 292 | wheels = [ 293 | { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, 294 | ] 295 | 296 | [[package]] 297 | name = "python-dotenv" 298 | version = "1.0.1" 299 | source = { registry = "https://pypi.org/simple" } 300 | sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } 301 | wheels = [ 302 | { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, 303 | ] 304 | 305 | [[package]] 306 | name = "rich" 307 | version = "13.9.4" 308 | source = { registry = "https://pypi.org/simple" } 309 | dependencies = [ 310 | { name = "markdown-it-py" }, 311 | { name = "pygments" }, 312 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 313 | ] 314 | sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } 315 | wheels = [ 316 | { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, 317 | ] 318 | 319 | [[package]] 320 | name = "shellingham" 321 | version = "1.5.4" 322 | source = { registry = "https://pypi.org/simple" } 323 | sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } 324 | wheels = [ 325 | { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, 326 | ] 327 | 328 | [[package]] 329 | name = "sniffio" 330 | version = "1.3.1" 331 | source = { registry = "https://pypi.org/simple" } 332 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 333 | wheels = [ 334 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 335 | ] 336 | 337 | [[package]] 338 | name = "sse-starlette" 339 | version = "2.2.1" 340 | source = { registry = "https://pypi.org/simple" } 341 | dependencies = [ 342 | { name = "anyio" }, 343 | { name = "starlette" }, 344 | ] 345 | sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } 346 | wheels = [ 347 | { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, 348 | ] 349 | 350 | [[package]] 351 | name = "starlette" 352 | version = "0.46.1" 353 | source = { registry = "https://pypi.org/simple" } 354 | dependencies = [ 355 | { name = "anyio" }, 356 | ] 357 | sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } 358 | wheels = [ 359 | { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, 360 | ] 361 | 362 | [[package]] 363 | name = "typer" 364 | version = "0.15.2" 365 | source = { registry = "https://pypi.org/simple" } 366 | dependencies = [ 367 | { name = "click" }, 368 | { name = "rich" }, 369 | { name = "shellingham" }, 370 | { name = "typing-extensions" }, 371 | ] 372 | sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 } 373 | wheels = [ 374 | { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, 375 | ] 376 | 377 | [[package]] 378 | name = "typing-extensions" 379 | version = "4.12.2" 380 | source = { registry = "https://pypi.org/simple" } 381 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 382 | wheels = [ 383 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 384 | ] 385 | 386 | [[package]] 387 | name = "uvicorn" 388 | version = "0.34.0" 389 | source = { registry = "https://pypi.org/simple" } 390 | dependencies = [ 391 | { name = "click" }, 392 | { name = "h11" }, 393 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 394 | ] 395 | sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } 396 | wheels = [ 397 | { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, 398 | ] 399 | --------------------------------------------------------------------------------