Skip to main content
Glama

Flutter MCP

by adamsmaka
server.py125 kB
#!/usr/bin/env python3 """Flutter MCP Server - Real-time Flutter/Dart documentation for AI assistants""" import asyncio import json import re from typing import Optional, Dict, List, Any, Tuple from datetime import datetime import time from mcp.server.fastmcp import FastMCP import httpx # Redis removed - using SQLite cache instead from bs4 import BeautifulSoup import structlog from structlog.contextvars import bind_contextvars from rich.console import Console # Import our custom logging utilities from .logging_utils import format_cache_stats, print_server_header # Initialize structured logging # IMPORTANT: For MCP servers, logs must go to stderr, not stdout # stdout is reserved for the JSON-RPC protocol import sys import logging # Configure structlog with enhanced formatting structlog.configure( processors=[ structlog.contextvars.merge_contextvars, structlog.processors.add_log_level, structlog.processors.StackInfoRenderer(), structlog.dev.set_exc_info, structlog.processors.TimeStamper(fmt="%H:%M:%S", utc=False), # Our custom processor comes before the renderer! format_cache_stats, # Use ConsoleRenderer for beautiful colored output structlog.dev.ConsoleRenderer( colors=True, exception_formatter=structlog.dev.plain_traceback, ), ], wrapper_class=structlog.make_filtering_bound_logger(logging.INFO), context_class=dict, logger_factory=structlog.PrintLoggerFactory(file=sys.stderr), cache_logger_on_first_use=True, ) logger = structlog.get_logger() # Rich console for direct output console = Console(stderr=True) # Initialize FastMCP server mcp = FastMCP("Flutter Docs Server") # Import our SQLite-based cache from .cache import get_cache # Import error handling utilities from .error_handling import ( NetworkError, DocumentationNotFoundError, RateLimitError, with_retry, safe_http_get, format_error_response, CircuitBreaker ) # Legacy version parser functionality now integrated in resolve_identifier() # Import truncation utilities from .truncation import truncate_flutter_docs, create_truncator, DocumentTruncator # Import token management from .token_manager import TokenManager # Initialize cache manager cache_manager = get_cache() logger.info("cache_initialized", cache_type="sqlite", path=cache_manager.db_path) # Initialize token manager token_manager = TokenManager() class RateLimiter: """Rate limiter for respectful web scraping (2 requests/second)""" def __init__(self, calls_per_second: float = 2.0): self.semaphore = asyncio.Semaphore(1) self.min_interval = 1.0 / calls_per_second self.last_call = 0 async def acquire(self): async with self.semaphore: current_time = time.time() elapsed = current_time - self.last_call if elapsed < self.min_interval: await asyncio.sleep(self.min_interval - elapsed) self.last_call = time.time() # Global rate limiter instance rate_limiter = RateLimiter() # ============================================================================ # Helper Functions for Tool Consolidation # ============================================================================ def resolve_identifier(identifier: str) -> Tuple[str, str, Optional[str]]: """ Resolve an identifier to determine its type and clean form. Args: identifier: The identifier to resolve (e.g., "Container", "material.AppBar", "dart:async.Future", "provider", "dio:^5.0.0") Returns: Tuple of (type, clean_id, library) where: - type: "flutter_class", "dart_class", "pub_package", or "unknown" - clean_id: Cleaned identifier without prefixes or version constraints - library: Library name for classes, None for packages """ # Check for version constraint (indicates package) if ':' in identifier and not identifier.startswith('dart:'): # It's a package with version constraint package_name = identifier.split(':')[0] return ("pub_package", package_name, None) # Check for Dart API pattern (dart:library.Class) if identifier.startswith('dart:'): match = re.match(r'dart:(\w+)\.(\w+)', identifier) if match: library = f"dart:{match.group(1)}" class_name = match.group(2) return ("dart_class", class_name, library) else: # Just dart:library without class return ("dart_class", identifier, None) # Check for Flutter library.class pattern flutter_libs = ['widgets', 'material', 'cupertino', 'painting', 'animation', 'rendering', 'services', 'gestures', 'foundation'] for lib in flutter_libs: if identifier.startswith(f"{lib}."): class_name = identifier.split('.', 1)[1] return ("flutter_class", class_name, lib) # Check if it's a known Flutter widget (common ones) common_widgets = ['Container', 'Row', 'Column', 'Text', 'Scaffold', 'AppBar', 'ListView', 'GridView', 'Stack', 'Card', 'IconButton'] if identifier in common_widgets: return ("flutter_class", identifier, "widgets") # Check if it looks like a package name (lowercase, may contain underscores) if identifier.islower() or '_' in identifier: return ("pub_package", identifier, None) # Default to unknown return ("unknown", identifier, None) def filter_by_topic(content: str, topic: str, doc_type: str) -> str: """ Extract specific sections from documentation based on topic. Args: content: Full documentation content topic: Topic to filter by (e.g., "constructors", "methods", "properties", "examples", "dependencies", "usage") doc_type: Type of documentation ("flutter_class", "dart_class", "pub_package") Returns: Filtered content containing only the requested topic """ if not content: return "No content available" topic_lower = topic.lower() if doc_type in ["flutter_class", "dart_class"]: # For class documentation, extract specific sections lines = content.split('\n') in_section = False section_content = [] section_headers = { "constructors": ["## Constructors", "### Constructors"], "methods": ["## Methods", "### Methods"], "properties": ["## Properties", "### Properties"], "examples": ["## Code Examples", "### Examples", "## Examples"], "description": ["## Description", "### Description"], } if topic_lower in section_headers: headers = section_headers[topic_lower] for i, line in enumerate(lines): if any(header in line for header in headers): in_section = True section_content.append(line) elif in_section and line.startswith('##'): # Reached next major section break elif in_section: section_content.append(line) if section_content: return '\n'.join(section_content) else: return f"No {topic} section found in documentation" else: return f"Unknown topic '{topic}' for class documentation" elif doc_type == "pub_package": # For package documentation, different sections if topic_lower == "dependencies": # Extract dependencies from the content deps_match = re.search(r'"dependencies":\s*\[(.*?)\]', content, re.DOTALL) if deps_match: deps = deps_match.group(1) return f"Dependencies: {deps}" return "No dependencies information found" elif topic_lower == "usage": # Try to extract usage/getting started section from README if "readme" in content.lower(): # Look for usage patterns in README patterns = [r'## Usage.*?(?=##|\Z)', r'## Getting Started.*?(?=##|\Z)', r'## Quick Start.*?(?=##|\Z)', r'## Installation.*?(?=##|\Z)'] for pattern in patterns: match = re.search(pattern, content, re.DOTALL | re.IGNORECASE) if match: return match.group(0).strip() return "No usage information found" elif topic_lower == "examples": # Extract code examples from README code_blocks = re.findall(r'```(?:dart|flutter)?\n(.*?)\n```', content, re.DOTALL) if code_blocks: examples = [] for i, code in enumerate(code_blocks[:5]): # Limit to 5 examples examples.append(f"Example {i+1}:\n```dart\n{code}\n```") return '\n\n'.join(examples) return "No code examples found" # Default: return full content if topic not recognized return content def to_unified_id(doc_type: str, identifier: str, library: str = None) -> str: """ Convert documentation reference to unified ID format. Args: doc_type: Type of documentation ("flutter_class", "dart_class", "pub_package") identifier: The identifier (class name or package name) library: Optional library name for classes Returns: Unified ID string (e.g., "flutter:material.AppBar", "dart:async.Future", "package:dio") """ if doc_type == "flutter_class": if library: return f"flutter:{library}.{identifier}" else: return f"flutter:widgets.{identifier}" # Default to widgets elif doc_type == "dart_class": if library: return f"{library}.{identifier}" else: return f"dart:core.{identifier}" # Default to core elif doc_type == "pub_package": return f"package:{identifier}" else: return identifier def from_unified_id(unified_id: str) -> Tuple[str, str, Optional[str]]: """ Parse unified ID format back to components. Args: unified_id: Unified ID string (e.g., "flutter:material.AppBar") Returns: Tuple of (type, identifier, library) """ if unified_id.startswith("flutter:"): parts = unified_id[8:].split('.', 1) # Remove "flutter:" prefix if len(parts) == 2: return ("flutter_class", parts[1], parts[0]) else: return ("flutter_class", parts[0], "widgets") elif unified_id.startswith("dart:"): match = re.match(r'(dart:\w+)\.(\w+)', unified_id) if match: return ("dart_class", match.group(2), match.group(1)) else: return ("dart_class", unified_id, None) elif unified_id.startswith("package:"): return ("pub_package", unified_id[8:], None) else: return ("unknown", unified_id, None) def estimate_doc_size(content: str) -> str: """ Estimate documentation size category based on content length. Args: content: Documentation content Returns: Size category: "small", "medium", or "large" """ if not content: return "small" # Rough token estimation (1 token ≈ 4 characters) estimated_tokens = len(content) / 4 if estimated_tokens < 1000: return "small" elif estimated_tokens < 4000: return "medium" else: return "large" def rank_results(results: List[Dict[str, Any]], query: str) -> List[Dict[str, Any]]: """ Rank search results based on relevance to query. Args: results: List of search results query: Original search query Returns: Sorted list of results with updated relevance scores """ query_lower = query.lower() query_words = set(query_lower.split()) for result in results: # Start with existing relevance score if present score = result.get("relevance", 0.5) # Boost for exact title match title = result.get("title", "").lower() if query_lower == title: score += 0.5 elif query_lower in title: score += 0.3 # Boost for word matches in title title_words = set(title.split()) word_overlap = len(query_words & title_words) / len(query_words) if query_words else 0 score += word_overlap * 0.2 # Consider description matches description = result.get("description", "").lower() if query_lower in description: score += 0.1 # Boost for type preferences if "state" in query_lower and result.get("type") == "concept": score += 0.2 elif "package" in query_lower and result.get("type") == "pub_package": score += 0.2 elif any(word in query_lower for word in ["widget", "class"]) and result.get("type") == "flutter_class": score += 0.2 # Cap score at 1.0 result["relevance"] = min(score, 1.0) # Sort by relevance score (descending) return sorted(results, key=lambda x: x.get("relevance", 0), reverse=True) # Circuit breakers for external services flutter_docs_circuit = CircuitBreaker( failure_threshold=5, recovery_timeout=60.0, expected_exception=(NetworkError, httpx.HTTPStatusError) ) pub_dev_circuit = CircuitBreaker( failure_threshold=5, recovery_timeout=60.0, expected_exception=(NetworkError, httpx.HTTPStatusError) ) # Cache TTL strategy (in seconds) CACHE_DURATIONS = { "flutter_api": 86400, # 24 hours for stable APIs "dart_api": 86400, # 24 hours for Dart APIs "pub_package": 43200, # 12 hours for packages (may update more frequently) "cookbook": 604800, # 7 days for examples "stackoverflow": 3600, # 1 hour for community content } def get_cache_key(doc_type: str, identifier: str, version: str = None) -> str: """Generate cache keys for different documentation types""" if version: # Normalize version string for cache key version = version.replace(' ', '_').replace('>=', 'gte').replace('<=', 'lte').replace('^', 'caret') return f"{doc_type}:{identifier}:{version}" return f"{doc_type}:{identifier}" def clean_text(element) -> str: """Clean and extract text from BeautifulSoup element""" if not element: return "" text = element.get_text(strip=True) # Remove excessive whitespace text = re.sub(r'\s+', ' ', text) return text.strip() def format_constructors(constructors: List) -> str: """Format constructor information for AI consumption""" if not constructors: return "No constructors found" result = [] for constructor in constructors: name = constructor.find('h3') signature = constructor.find('pre') desc = constructor.find('p') if name: result.append(f"### {clean_text(name)}") if signature: result.append(f"```dart\n{clean_text(signature)}\n```") if desc: result.append(clean_text(desc)) result.append("") return "\n".join(result) def format_properties(properties: List) -> str: """Format property information""" if not properties: return "No properties found" result = [] for prop_list in properties: items = prop_list.find_all('dt') for item in items: prop_name = clean_text(item) prop_desc = item.find_next_sibling('dd') if prop_name: result.append(f"- **{prop_name}**: {clean_text(prop_desc) if prop_desc else 'No description'}") return "\n".join(result) def format_methods(methods: List) -> str: """Format method information""" if not methods: return "No methods found" result = [] for method in methods: name = method.find('h3') signature = method.find('pre') desc = method.find('p') if name: result.append(f"### {clean_text(name)}") if signature: result.append(f"```dart\n{clean_text(signature)}\n```") if desc: result.append(clean_text(desc)) result.append("") return "\n".join(result) def extract_code_examples(soup: BeautifulSoup) -> str: """Extract code examples from documentation""" examples = soup.find_all('pre', class_='language-dart') if not examples: examples = soup.find_all('pre') # Fallback to any pre tags if not examples: return "No code examples found" result = [] for i, example in enumerate(examples[:5]): # Limit to 5 examples code = clean_text(example) if code: result.append(f"#### Example {i+1}:\n```dart\n{code}\n```\n") return "\n".join(result) async def process_documentation(html: str, class_name: str, tokens: int = None) -> Dict[str, Any]: """Context7-style documentation processing pipeline with smart truncation and token counting. Returns a dict containing: - content: The processed markdown content - token_count: Final token count after any truncation - original_tokens: Original token count before truncation - truncated: Boolean indicating if content was truncated - truncation_note: Optional note about truncation """ soup = BeautifulSoup(html, 'html.parser') # Remove navigation, scripts, styles, etc. for element in soup.find_all(['script', 'style', 'nav', 'header', 'footer']): element.decompose() # 1. Parse - Extract key sections description = soup.find('section', class_='desc') constructors = soup.find_all('section', class_='constructor') properties = soup.find_all('dl', class_='properties') methods = soup.find_all('section', class_='method') # 2. Enrich - Format for AI consumption markdown = f"""# {class_name} ## Description {clean_text(description) if description else 'No description available'} ## Constructors {format_constructors(constructors)} ## Properties {format_properties(properties)} ## Methods {format_methods(methods)} ## Code Examples {extract_code_examples(soup)} """ # Count tokens before truncation original_tokens = token_manager.count_tokens(markdown) truncated = False truncation_note = None # 3. Truncate if needed if tokens and original_tokens > tokens: markdown = truncate_flutter_docs( markdown, class_name, max_tokens=tokens, strategy="balanced" ) truncated = True truncation_note = f"Documentation truncated from {original_tokens} to approximately {tokens} tokens" # Count final tokens final_tokens = token_manager.count_tokens(markdown) return { "content": markdown, "token_count": final_tokens, "original_tokens": original_tokens if truncated else final_tokens, "truncated": truncated, "truncation_note": truncation_note } def resolve_flutter_url(query: str) -> Optional[str]: """Intelligently resolve documentation URLs from queries""" # Common Flutter class patterns patterns = { r"^(\w+)$": "https://api.flutter.dev/flutter/widgets/{0}-class.html", r"^widgets\.(\w+)$": "https://api.flutter.dev/flutter/widgets/{0}-class.html", r"^material\.(\w+)$": "https://api.flutter.dev/flutter/material/{0}-class.html", r"^cupertino\.(\w+)$": "https://api.flutter.dev/flutter/cupertino/{0}-class.html", r"^painting\.(\w+)$": "https://api.flutter.dev/flutter/painting/{0}-class.html", r"^animation\.(\w+)$": "https://api.flutter.dev/flutter/animation/{0}-class.html", r"^rendering\.(\w+)$": "https://api.flutter.dev/flutter/rendering/{0}-class.html", r"^services\.(\w+)$": "https://api.flutter.dev/flutter/services/{0}-class.html", r"^gestures\.(\w+)$": "https://api.flutter.dev/flutter/gestures/{0}-class.html", r"^foundation\.(\w+)$": "https://api.flutter.dev/flutter/foundation/{0}-class.html", # Dart core libraries r"^dart:core\.(\w+)$": "https://api.dart.dev/stable/dart-core/{0}-class.html", r"^dart:async\.(\w+)$": "https://api.dart.dev/stable/dart-async/{0}-class.html", r"^dart:collection\.(\w+)$": "https://api.dart.dev/stable/dart-collection/{0}-class.html", r"^dart:convert\.(\w+)$": "https://api.dart.dev/stable/dart-convert/{0}-class.html", r"^dart:io\.(\w+)$": "https://api.dart.dev/stable/dart-io/{0}-class.html", r"^dart:math\.(\w+)$": "https://api.dart.dev/stable/dart-math/{0}-class.html", r"^dart:typed_data\.(\w+)$": "https://api.dart.dev/stable/dart-typed_data/{0}-class.html", r"^dart:ui\.(\w+)$": "https://api.dart.dev/stable/dart-ui/{0}-class.html", } for pattern, url_template in patterns.items(): if match := re.match(pattern, query, re.IGNORECASE): return url_template.format(*match.groups()) return None @mcp.tool() async def get_flutter_docs( class_name: str, library: str = "widgets", tokens: int = 8000 ) -> Dict[str, Any]: """ Get Flutter class documentation on-demand with optional smart truncation. **DEPRECATED**: This tool is deprecated. Please use flutter_docs() instead. The new tool provides better query resolution and unified interface. Args: class_name: Name of the Flutter class (e.g., "Container", "Scaffold") library: Flutter library (e.g., "widgets", "material", "cupertino") tokens: Maximum token limit for truncation (default: 8000, min: 500) Returns: Dictionary with documentation content or error message """ bind_contextvars(tool="get_flutter_docs", class_name=class_name, library=library) logger.warning("deprecated_tool_usage", tool="get_flutter_docs", replacement="flutter_docs") # Validate tokens parameter if tokens < 500: return {"error": "tokens parameter must be at least 500"} # Call the new flutter_docs tool identifier = f"{library}.{class_name}" if library != "widgets" else class_name result = await flutter_docs(identifier, max_tokens=tokens) # Transform back to old format if result.get("error"): return { "error": result["error"], "suggestion": result.get("suggestion", "") } else: return { "source": result.get("source", "live"), "class": result.get("class", class_name), "library": result.get("library", library), "content": result.get("content", ""), "fetched_at": datetime.utcnow().isoformat(), "truncated": result.get("truncated", False) } async def _get_flutter_docs_impl( class_name: str, library: str = "widgets", tokens: int = None ) -> Dict[str, Any]: """ Internal implementation of get_flutter_docs functionality. """ # Check cache first cache_key = get_cache_key("flutter_api", f"{library}:{class_name}") # Check cache cached_data = cache_manager.get(cache_key) if cached_data: logger.info("cache_hit") return cached_data # Rate-limited fetch from Flutter docs await rate_limiter.acquire() # Determine URL based on library type if library.startswith("dart:"): # Convert dart:core to dart-core format for Dart API dart_lib = library.replace("dart:", "dart-") url = f"https://api.dart.dev/stable/{dart_lib}/{class_name}-class.html" else: # Flutter libraries use api.flutter.dev url = f"https://api.flutter.dev/flutter/{library}/{class_name}-class.html" logger.info("fetching_docs", url=url) try: async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client: response = await client.get( url, headers={ "User-Agent": "Flutter-MCP-Docs/1.0 (github.com/flutter-mcp/flutter-mcp)" } ) response.raise_for_status() # Process HTML - Context7 style pipeline with truncation doc_result = await process_documentation(response.text, class_name, tokens) # Cache the result with token metadata result = { "source": "live", "class": class_name, "library": library, "content": doc_result["content"], "fetched_at": datetime.utcnow().isoformat(), "truncated": doc_result["truncated"], "token_count": doc_result["token_count"], "original_tokens": doc_result["original_tokens"], "truncation_note": doc_result["truncation_note"] } cache_manager.set(cache_key, result, CACHE_DURATIONS["flutter_api"], token_count=doc_result["token_count"]) logger.info("docs_fetched_success", content_length=len(doc_result["content"]), token_count=doc_result["token_count"], truncated=doc_result["truncated"]) return result except httpx.HTTPStatusError as e: logger.error("http_error", status_code=e.response.status_code) return { "error": f"HTTP {e.response.status_code}: Documentation not found for {library}.{class_name}", "suggestion": "Check the class name and library. Common libraries: widgets, material, cupertino" } except Exception as e: logger.error("fetch_error", error=str(e)) return { "error": f"Failed to fetch documentation: {str(e)}", "url": url } @mcp.tool() async def search_flutter_docs(query: str, tokens: int = 5000) -> Dict[str, Any]: """ Search across Flutter/Dart documentation sources with fuzzy matching. **DEPRECATED**: This tool is deprecated. Please use flutter_search() instead. The new tool provides better filtering and more structured results. Searches Flutter API docs, Dart API docs, and pub.dev packages. Returns top 5-10 most relevant results with brief descriptions. Args: query: Search query (e.g., "state management", "Container", "navigation", "http requests") tokens: Maximum token limit for response (default: 5000, min: 500) Returns: Search results with relevance scores and brief descriptions """ bind_contextvars(tool="search_flutter_docs", query=query) logger.warning("deprecated_tool_usage", tool="search_flutter_docs", replacement="flutter_search") # Validate tokens parameter if tokens < 500: return {"error": "tokens parameter must be at least 500"} # Call new flutter_search tool result = await flutter_search(query, limit=10) # Transform back to old format return { "query": result["query"], "results": result["results"], "total": result.get("total_results", result.get("returned_results", 0)), "timestamp": result.get("timestamp", datetime.utcnow().isoformat()), "suggestions": result.get("suggestions", []) } async def _search_flutter_docs_impl( query: str, limit: int = 10, types: List[str] = None ) -> Dict[str, Any]: """ Internal implementation of search functionality. """ logger.info("searching_docs") results = [] query_lower = query.lower() # Check cache for search results cache_key = get_cache_key("search_results", query_lower) cached_data = cache_manager.get(cache_key) if cached_data: logger.info("search_cache_hit") return cached_data # 1. Try direct URL resolution first (exact matches) if url := resolve_flutter_url(query): logger.info("url_resolved", url=url) # Extract class name and library from URL if "flutter/widgets" in url: library = "widgets" elif "flutter/material" in url: library = "material" elif "flutter/cupertino" in url: library = "cupertino" else: library = "unknown" class_match = re.search(r'/([^/]+)-class\.html$', url) if class_match: class_name = class_match.group(1) doc = await _get_flutter_docs_impl(class_name, library) if "error" not in doc: results.append({ "type": "flutter_class", "relevance": 1.0, "title": f"{class_name} ({library})", "description": f"Flutter {library} widget/class", "url": url, "content_preview": doc.get("content", "")[:200] + "..." }) # 2. Check common Flutter widgets and classes common_flutter_items = [ # State management related ("StatefulWidget", "widgets", "Base class for widgets that have mutable state"), ("StatelessWidget", "widgets", "Base class for widgets that don't require mutable state"), ("State", "widgets", "Logic and internal state for a StatefulWidget"), ("InheritedWidget", "widgets", "Base class for widgets that propagate information down the tree"), ("Provider", "widgets", "A widget that provides a value to its descendants"), ("ValueListenableBuilder", "widgets", "Rebuilds when ValueListenable changes"), ("NotificationListener", "widgets", "Listens for Notifications bubbling up"), # Layout widgets ("Container", "widgets", "A convenience widget that combines common painting, positioning, and sizing"), ("Row", "widgets", "Displays children in a horizontal array"), ("Column", "widgets", "Displays children in a vertical array"), ("Stack", "widgets", "Positions children relative to the box edges"), ("Scaffold", "material", "Basic material design visual layout structure"), ("Expanded", "widgets", "Expands a child to fill available space in Row/Column"), ("Flexible", "widgets", "Controls how a child flexes in Row/Column"), ("Wrap", "widgets", "Displays children in multiple runs"), ("Flow", "widgets", "Positions children using transformation matrices"), ("Table", "widgets", "Displays children in a table layout"), ("Align", "widgets", "Aligns a child within itself"), ("Center", "widgets", "Centers a child within itself"), ("Positioned", "widgets", "Positions a child in a Stack"), ("FittedBox", "widgets", "Scales and positions child within itself"), ("AspectRatio", "widgets", "Constrains child to specific aspect ratio"), ("ConstrainedBox", "widgets", "Imposes additional constraints on child"), ("SizedBox", "widgets", "Box with a specified size"), ("FractionallySizedBox", "widgets", "Sizes child to fraction of total space"), ("LimitedBox", "widgets", "Limits child size when unconstrained"), ("Offstage", "widgets", "Lays out child as if visible but paints nothing"), ("LayoutBuilder", "widgets", "Builds widget tree based on parent constraints"), # Navigation ("Navigator", "widgets", "Manages a stack of Route objects"), ("Route", "widgets", "An abstraction for an entry managed by a Navigator"), ("MaterialPageRoute", "material", "A modal route that replaces the entire screen"), ("NavigationBar", "material", "Material 3 navigation bar"), ("NavigationRail", "material", "Material navigation rail"), ("BottomNavigationBar", "material", "Bottom navigation bar"), ("Drawer", "material", "Material design drawer"), ("TabBar", "material", "Material design tabs"), ("TabBarView", "material", "Page view for TabBar"), ("WillPopScope", "widgets", "Intercepts back button press"), ("BackButton", "material", "Material design back button"), # Input widgets ("TextField", "material", "A material design text field"), ("TextFormField", "material", "A FormField that contains a TextField"), ("Form", "widgets", "Container for form fields"), ("GestureDetector", "widgets", "Detects gestures on widgets"), ("InkWell", "material", "Rectangular area that responds to touch with ripple"), ("Dismissible", "widgets", "Can be dismissed by dragging"), ("Draggable", "widgets", "Can be dragged to DragTarget"), ("LongPressDraggable", "widgets", "Draggable triggered by long press"), ("DragTarget", "widgets", "Receives data from Draggable"), ("DropdownButton", "material", "Material design dropdown button"), ("Slider", "material", "Material design slider"), ("Switch", "material", "Material design switch"), ("Checkbox", "material", "Material design checkbox"), ("Radio", "material", "Material design radio button"), ("DatePicker", "material", "Material design date picker"), ("TimePicker", "material", "Material design time picker"), # Lists & Grids ("ListView", "widgets", "Scrollable list of widgets"), ("GridView", "widgets", "Scrollable 2D array of widgets"), ("CustomScrollView", "widgets", "ScrollView with slivers"), ("SingleChildScrollView", "widgets", "Box with single scrollable child"), ("PageView", "widgets", "Scrollable list that works page by page"), ("ReorderableListView", "material", "List where items can be reordered"), ("RefreshIndicator", "material", "Material design pull-to-refresh"), # Common material widgets ("AppBar", "material", "A material design app bar"), ("Card", "material", "A material design card"), ("ListTile", "material", "A single fixed-height row for lists"), ("IconButton", "material", "A material design icon button"), ("ElevatedButton", "material", "A material design elevated button"), ("FloatingActionButton", "material", "A material design floating action button"), ("Chip", "material", "Material design chip"), ("ChoiceChip", "material", "Material design choice chip"), ("FilterChip", "material", "Material design filter chip"), ("ActionChip", "material", "Material design action chip"), ("CircularProgressIndicator", "material", "Material circular progress"), ("LinearProgressIndicator", "material", "Material linear progress"), ("SnackBar", "material", "Material design snackbar"), ("BottomSheet", "material", "Material design bottom sheet"), ("ExpansionPanel", "material", "Material expansion panel"), ("Stepper", "material", "Material design stepper"), ("DataTable", "material", "Material design data table"), # Visual Effects ("Opacity", "widgets", "Makes child partially transparent"), ("Transform", "widgets", "Applies transformation before painting"), ("RotatedBox", "widgets", "Rotates child by integral quarters"), ("ClipRect", "widgets", "Clips child to rectangle"), ("ClipRRect", "widgets", "Clips child to rounded rectangle"), ("ClipOval", "widgets", "Clips child to oval"), ("ClipPath", "widgets", "Clips child to path"), ("DecoratedBox", "widgets", "Paints decoration around child"), ("BackdropFilter", "widgets", "Applies filter to existing painted content"), # Animation ("AnimatedBuilder", "widgets", "A widget that rebuilds when animation changes"), ("AnimationController", "animation", "Controls an animation"), ("Hero", "widgets", "Marks a child for hero animations"), ("AnimatedContainer", "widgets", "Animated version of Container"), ("AnimatedOpacity", "widgets", "Animated version of Opacity"), ("AnimatedPositioned", "widgets", "Animated version of Positioned"), ("AnimatedDefaultTextStyle", "widgets", "Animated version of DefaultTextStyle"), ("AnimatedAlign", "widgets", "Animated version of Align"), ("AnimatedPadding", "widgets", "Animated version of Padding"), ("AnimatedSize", "widgets", "Animates its size to match child"), ("AnimatedCrossFade", "widgets", "Cross-fades between two children"), ("AnimatedSwitcher", "widgets", "Animates when switching between children"), # Async widgets ("FutureBuilder", "widgets", "Builds based on interaction with a Future"), ("StreamBuilder", "widgets", "Builds based on interaction with a Stream"), # Utility widgets ("MediaQuery", "widgets", "Establishes media query subtree"), ("Theme", "material", "Applies theme to descendant widgets"), ("DefaultTextStyle", "widgets", "Default text style for descendants"), ("Semantics", "widgets", "Annotates widget tree with semantic descriptions"), ("MergeSemantics", "widgets", "Merges semantics of descendants"), ("ExcludeSemantics", "widgets", "Drops semantics of descendants"), ] # Score Flutter items based on query match for class_name, library, description in common_flutter_items: relevance = calculate_relevance(query_lower, class_name.lower(), description.lower()) if relevance > 0.3: # Threshold for inclusion results.append({ "type": "flutter_class", "relevance": relevance, "title": f"{class_name} ({library})", "description": description, "class_name": class_name, "library": library }) # 3. Check common Dart core classes common_dart_items = [ ("List", "dart:core", "An indexable collection of objects with a length"), ("Map", "dart:core", "A collection of key/value pairs"), ("Set", "dart:core", "A collection of objects with no duplicate elements"), ("String", "dart:core", "A sequence of UTF-16 code units"), ("Future", "dart:async", "Represents a computation that completes with a value or error"), ("Stream", "dart:async", "A source of asynchronous data events"), ("Duration", "dart:core", "A span of time"), ("DateTime", "dart:core", "An instant in time"), ("RegExp", "dart:core", "A regular expression pattern"), ("Iterable", "dart:core", "A collection of values that can be accessed sequentially"), ] for class_name, library, description in common_dart_items: relevance = calculate_relevance(query_lower, class_name.lower(), description.lower()) if relevance > 0.3: results.append({ "type": "dart_class", "relevance": relevance, "title": f"{class_name} ({library})", "description": description, "class_name": class_name, "library": library }) # 4. Search popular pub.dev packages popular_packages = [ # State Management ("provider", "State management library that makes it easy to connect business logic to widgets"), ("riverpod", "A reactive caching and data-binding framework"), ("bloc", "State management library implementing the BLoC design pattern"), ("get", "Open source state management, navigation and utilities"), ("mobx", "Reactive state management library"), ("redux", "Predictable state container"), ("stacked", "MVVM architecture solution"), ("get_it", "Service locator for dependency injection"), # Networking ("dio", "Powerful HTTP client for Dart with interceptors and FormData"), ("http", "A composable, multi-platform, Future-based API for HTTP requests"), ("retrofit", "Type-safe HTTP client generator"), ("chopper", "HTTP client with built-in JsonConverter"), ("graphql_flutter", "GraphQL client for Flutter"), ("socket_io_client", "Socket.IO client"), ("web_socket_channel", "WebSocket connections"), # Storage & Database ("shared_preferences", "Flutter plugin for reading and writing simple key-value pairs"), ("sqflite", "SQLite plugin for Flutter with support for iOS, Android and MacOS"), ("hive", "Lightweight and blazing fast key-value database written in pure Dart"), ("isar", "Fast cross-platform database"), ("objectbox", "High-performance NoSQL database"), ("drift", "Reactive persistence library"), ("floor", "SQLite abstraction with Room-like API"), # Firebase ("firebase_core", "Flutter plugin to use Firebase Core API"), ("firebase_auth", "Flutter plugin for Firebase Auth"), ("firebase_database", "Flutter plugin for Firebase Realtime Database"), ("cloud_firestore", "Flutter plugin for Cloud Firestore"), ("firebase_messaging", "Push notifications via FCM"), ("firebase_storage", "Flutter plugin for Firebase Cloud Storage"), ("firebase_analytics", "Flutter plugin for Google Analytics for Firebase"), # UI/UX Libraries ("flutter_bloc", "Flutter widgets that make it easy to implement BLoC design pattern"), ("animations", "Beautiful pre-built animations for Flutter"), ("flutter_svg", "SVG rendering and widget library for Flutter"), ("cached_network_image", "Flutter library to load and cache network images"), ("flutter_slidable", "Slidable list item actions"), ("shimmer", "Shimmer loading effect"), ("liquid_swipe", "Liquid swipe page transitions"), ("flutter_staggered_grid_view", "Staggered grid layouts"), ("carousel_slider", "Carousel widget"), ("photo_view", "Zoomable image widget"), ("flutter_spinkit", "Loading indicators collection"), ("lottie", "Render After Effects animations"), ("rive", "Interactive animations"), # Platform Integration ("url_launcher", "Flutter plugin for launching URLs"), ("path_provider", "Flutter plugin for getting commonly used locations on the filesystem"), ("image_picker", "Flutter plugin for selecting images from image library or camera"), ("connectivity_plus", "Flutter plugin for discovering network connectivity"), ("permission_handler", "Permission plugin for Flutter"), ("geolocator", "Flutter geolocation plugin for Android and iOS"), ("google_fonts", "Flutter package to use fonts from fonts.google.com"), ("flutter_local_notifications", "Local notifications"), ("share_plus", "Share content to other apps"), ("file_picker", "Native file picker"), ("open_file", "Open files with default apps"), # Navigation ("go_router", "A declarative routing package for Flutter"), ("auto_route", "Code generation for type-safe route navigation"), ("beamer", "Handle your application routing"), ("fluro", "Flutter routing library"), # Developer Tools ("logger", "Beautiful logging utility"), ("pretty_dio_logger", "Dio interceptor for logging"), ("flutter_dotenv", "Load environment variables"), ("device_info_plus", "Device information"), ("package_info_plus", "App package information"), ("equatable", "Simplify equality comparisons"), ("freezed", "Code generation for immutable classes"), ("json_serializable", "Automatically generate code for JSON"), ("build_runner", "Build system for Dart code generation"), ] for package_name, description in popular_packages: relevance = calculate_relevance(query_lower, package_name.lower(), description.lower()) if relevance > 0.3: results.append({ "type": "pub_package", "relevance": relevance, "title": f"{package_name} (pub.dev)", "description": description, "package_name": package_name }) # 5. Concept-based search (for queries like "state management", "navigation", etc.) concepts = { "state management": [ ("setState", "The simplest way to manage state in Flutter"), ("InheritedWidget", "Share data across the widget tree"), ("provider", "Popular state management package"), ("riverpod", "Improved provider with compile-time safety"), ("bloc", "Business Logic Component pattern"), ("get", "Lightweight state management solution"), ("mobx", "Reactive state management"), ("redux", "Predictable state container"), ("ValueNotifier", "Simple observable pattern"), ("ChangeNotifier", "Observable object for multiple listeners"), ], "navigation": [ ("Navigator", "Stack-based navigation in Flutter"), ("go_router", "Declarative routing package"), ("auto_route", "Code generation for routes"), ("Named routes", "Navigation using route names"), ("Deep linking", "Handle URLs in your app"), ("WillPopScope", "Intercept back navigation"), ("NavigatorObserver", "Observe navigation events"), ("Hero animations", "Animate widgets between routes"), ("Modal routes", "Full-screen modal pages"), ("BottomSheet navigation", "Navigate with bottom sheets"), ], "http": [ ("http", "Official Dart HTTP package"), ("dio", "Advanced HTTP client with interceptors"), ("retrofit", "Type-safe HTTP client generator"), ("chopper", "HTTP client with built-in JsonConverter"), ("GraphQL", "Query language for APIs"), ("REST API", "RESTful web services"), ("WebSocket", "Real-time bidirectional communication"), ("gRPC", "High performance RPC framework"), ], "database": [ ("sqflite", "SQLite for Flutter"), ("hive", "NoSQL database for Flutter"), ("drift", "Reactive persistence library"), ("objectbox", "Fast NoSQL database"), ("shared_preferences", "Simple key-value storage"), ("isar", "Fast cross-platform database"), ("floor", "SQLite abstraction"), ("sembast", "NoSQL persistent store"), ("Firebase Realtime Database", "Cloud-hosted NoSQL database"), ("Cloud Firestore", "Scalable NoSQL cloud database"), ], "animation": [ ("AnimationController", "Control animations"), ("AnimatedBuilder", "Build animations efficiently"), ("Hero", "Shared element transitions"), ("animations", "Pre-built animation package"), ("rive", "Interactive animations"), ("lottie", "After Effects animations"), ("AnimatedContainer", "Implicit animations"), ("TweenAnimationBuilder", "Simple custom animations"), ("Curves", "Animation easing functions"), ("Physics-based animations", "Spring and friction animations"), ], "architecture": [ ("BLoC Pattern", "Business Logic Component pattern for state management"), ("MVVM", "Model-View-ViewModel architecture pattern"), ("Clean Architecture", "Domain-driven design with clear separation"), ("Repository Pattern", "Abstraction layer for data sources"), ("Provider Pattern", "Dependency injection and state management"), ("GetX Pattern", "Reactive state management with GetX"), ("MVC", "Model-View-Controller pattern in Flutter"), ("Redux", "Predictable state container pattern"), ("Riverpod Architecture", "Modern reactive caching framework"), ("Domain Driven Design", "DDD principles in Flutter"), ("Hexagonal Architecture", "Ports and adapters pattern"), ("Feature-based structure", "Organize code by features"), ], "testing": [ ("Widget Testing", "Testing Flutter widgets in isolation"), ("Integration Testing", "End-to-end testing of Flutter apps"), ("Unit Testing", "Testing Dart code logic"), ("Golden Testing", "Visual regression testing"), ("Mockito", "Mocking framework for Dart"), ("flutter_test", "Flutter testing framework"), ("test", "Dart testing package"), ("integration_test", "Flutter integration testing"), ("mocktail", "Type-safe mocking library"), ("Test Coverage", "Measuring test completeness"), ("TDD", "Test-driven development"), ("BDD", "Behavior-driven development"), ], "performance": [ ("Performance Profiling", "Analyzing app performance"), ("Widget Inspector", "Debugging widget trees"), ("Timeline View", "Performance timeline analysis"), ("Memory Profiling", "Analyzing memory usage"), ("Shader Compilation", "Reducing shader jank"), ("Build Optimization", "Optimizing build methods"), ("Lazy Loading", "Loading content on demand"), ("Image Caching", "Efficient image loading"), ("Code Splitting", "Reducing initial bundle size"), ("Tree Shaking", "Removing unused code"), ("Const Constructors", "Compile-time optimizations"), ("RepaintBoundary", "Isolate expensive paints"), ], "platform": [ ("Platform Channels", "Communication with native code"), ("Method Channel", "Invoking platform-specific APIs"), ("Event Channel", "Streaming data from platform"), ("iOS Integration", "Flutter iOS-specific features"), ("Android Integration", "Flutter Android-specific features"), ("Web Support", "Flutter web-specific features"), ("Desktop Support", "Flutter desktop applications"), ("Embedding Flutter", "Adding Flutter to existing apps"), ("Platform Views", "Embedding native views"), ("FFI", "Foreign Function Interface"), ("Plugin Development", "Creating Flutter plugins"), ("Platform-specific UI", "Adaptive UI patterns"), ], "debugging": [ ("Flutter Inspector", "Visual debugging tool"), ("Logging", "Debug logging in Flutter"), ("Breakpoints", "Using breakpoints in Flutter"), ("DevTools", "Flutter DevTools suite"), ("Error Handling", "Handling errors in Flutter"), ("Crash Reporting", "Capturing and reporting crashes"), ("Debug Mode", "Flutter debug mode features"), ("Assert Statements", "Debug-only checks"), ("Stack Traces", "Understanding error traces"), ("Network Debugging", "Inspecting network requests"), ("Layout Explorer", "Visualize layout constraints"), ("Performance Overlay", "On-device performance metrics"), ], "forms": [ ("Form", "Container for form fields"), ("TextFormField", "Text input with validation"), ("FormField", "Base class for form fields"), ("Form Validation", "Validating user input"), ("Input Decoration", "Styling form fields"), ("Focus Management", "Managing input focus"), ("Keyboard Actions", "Custom keyboard actions"), ("Input Formatters", "Format input as typed"), ("Form State", "Managing form state"), ("Custom Form Fields", "Creating custom inputs"), ], "theming": [ ("ThemeData", "Application theme configuration"), ("Material Theme", "Material Design theming"), ("Dark Mode", "Supporting dark theme"), ("Custom Themes", "Creating custom themes"), ("Theme Extensions", "Extending theme data"), ("Color Schemes", "Material 3 color system"), ("Typography", "Text theming"), ("Dynamic Theming", "Runtime theme changes"), ("Platform Theming", "Platform-specific themes"), ], } # Check if query matches any concept for concept, items in concepts.items(): if concept in query_lower or any(word in concept for word in query_lower.split()): for item_name, item_desc in items: results.append({ "type": "concept", "relevance": 0.8, "title": item_name, "description": item_desc, "concept": concept }) # Apply type filtering if specified if types: filtered_results = [] for result in results: result_type = result.get("type", "") # Map result types to filter types if "flutter" in types and result_type == "flutter_class": filtered_results.append(result) elif "dart" in types and result_type == "dart_class": filtered_results.append(result) elif "package" in types and result_type == "pub_package": filtered_results.append(result) elif "concept" in types and result_type == "concept": filtered_results.append(result) results = filtered_results # Sort results by relevance results.sort(key=lambda x: x["relevance"], reverse=True) # Apply limit results = results[:limit] # Fetch actual documentation for top results if needed enriched_results = [] for result in results: if result["type"] == "flutter_class" and "class_name" in result: # Only fetch full docs for top 3 Flutter classes if len(enriched_results) < 3: try: doc = await _get_flutter_docs_impl(result["class_name"], result["library"]) if not doc.get("error"): result["documentation_available"] = True result["content_preview"] = doc.get("content", "")[:300] + "..." else: result["documentation_available"] = False result["error_info"] = doc.get("error_type", "unknown") except Exception as e: logger.warning("search_enrichment_error", error=str(e), class_name=result.get("class_name")) result["documentation_available"] = False result["error_info"] = "enrichment_failed" elif result["type"] == "pub_package" and "package_name" in result: # Add pub.dev URL result["url"] = f"https://pub.dev/packages/{result['package_name']}" result["documentation_available"] = True enriched_results.append(result) # Prepare final response response = { "query": query, "results": enriched_results, "total": len(enriched_results), "timestamp": datetime.utcnow().isoformat(), "suggestions": generate_search_suggestions(query_lower, enriched_results) } # Cache the search results for 1 hour cache_manager.set(cache_key, response, 3600) return response def calculate_relevance(query: str, title: str, description: str) -> float: """Calculate relevance score based on fuzzy matching.""" score = 0.0 # Exact match in title if query == title: score += 1.0 # Partial match in title elif query in title: score += 0.8 # Word match in title elif any(word in title for word in query.split()): score += 0.6 # Match in description if query in description: score += 0.4 elif any(word in description for word in query.split() if len(word) > 3): score += 0.2 # Fuzzy match using character overlap title_overlap = len(set(query) & set(title)) / len(set(query) | set(title)) if title else 0 desc_overlap = len(set(query) & set(description)) / len(set(query) | set(description)) if description else 0 score += (title_overlap * 0.3 + desc_overlap * 0.1) return min(score, 1.0) def generate_search_suggestions(query: str, results: List[Dict]) -> List[str]: """Generate helpful search suggestions based on query and results.""" suggestions = [] if not results: suggestions.append(f"Try searching for specific widget names like 'Container' or 'Scaffold'") suggestions.append(f"Use package names from pub.dev like 'provider' or 'dio'") suggestions.append(f"Search for concepts like 'state management' or 'navigation'") elif len(results) < 3: suggestions.append(f"For more results, try broader terms or related concepts") if any(r["type"] == "flutter_class" for r in results): suggestions.append(f"You can also search for specific libraries like 'material.AppBar'") return suggestions @mcp.tool() async def flutter_docs( identifier: str, topic: Optional[str] = None, tokens: int = 10000 ) -> Dict[str, Any]: """ Unified tool to get Flutter/Dart documentation with smart identifier resolution. Automatically detects the type of identifier and fetches appropriate documentation. Supports Flutter classes, Dart classes, and pub.dev packages. Args: identifier: The identifier to look up. Examples: - "Container" (Flutter widget) - "material.AppBar" (library-qualified Flutter class) - "dart:async.Future" (Dart API) - "provider" (pub.dev package) - "pub:dio" (explicit pub.dev package) - "flutter:Container" (explicit Flutter class) topic: Optional topic filter. For classes: "constructors", "methods", "properties", "examples". For packages: "getting-started", "examples", "api", "installation" tokens: Maximum tokens for response (default: 10000, min: 1000) Returns: Dictionary with documentation content, type, and metadata """ bind_contextvars(tool="flutter_docs", identifier=identifier, topic=topic) logger.info("resolving_identifier", identifier=identifier) # Validate tokens parameter if tokens < 1000: return {"error": "tokens parameter must be at least 1000"} # Parse identifier to determine type identifier_lower = identifier.lower() doc_type = None library = None class_name = None package_name = None # Check for explicit prefixes if identifier.startswith("pub:"): doc_type = "pub_package" package_name = identifier[4:] elif identifier.startswith("flutter:"): doc_type = "flutter_class" class_name = identifier[8:] library = "widgets" # Default to widgets elif identifier.startswith("dart:"): doc_type = "dart_class" # Parse dart:library.Class format parts = identifier.split(".") if len(parts) == 2: library = parts[0] class_name = parts[1] else: class_name = identifier[5:] library = "dart:core" elif "." in identifier: # Library-qualified name (e.g., material.AppBar) parts = identifier.split(".", 1) library = parts[0] class_name = parts[1] if library.startswith("dart:"): doc_type = "dart_class" else: doc_type = "flutter_class" else: # Auto-detect type by trying different sources # First check if it's a known Flutter class flutter_libs = ["widgets", "material", "cupertino", "painting", "animation", "rendering", "services", "gestures", "foundation"] # Try to find in common Flutter widgets for lib in flutter_libs: test_url = f"https://api.flutter.dev/flutter/{lib}/{identifier}-class.html" if identifier.lower() in ["container", "scaffold", "appbar", "column", "row", "text", "button", "listview", "gridview", "stack"]: doc_type = "flutter_class" class_name = identifier library = "widgets" if identifier.lower() in ["container", "column", "row", "text", "stack"] else "material" break if not doc_type: # Could be a package or unknown Flutter class # We'll try both and see what works doc_type = "auto" class_name = identifier package_name = identifier # Based on detected type, fetch documentation result = { "identifier": identifier, "type": doc_type, "topic": topic, "max_tokens": tokens } if doc_type == "flutter_class" or (doc_type == "auto" and class_name): # Try Flutter documentation first flutter_doc = await get_flutter_docs(class_name, library or "widgets", tokens=tokens) if "error" not in flutter_doc: # Successfully found Flutter documentation content = flutter_doc.get("content", "") # Apply topic filtering if requested if topic: content = filter_documentation_by_topic(content, topic, "flutter_class") # Recount tokens after filtering filtered_tokens = token_manager.count_tokens(content) # If filtering reduced content below token limit, no need for further truncation if tokens and filtered_tokens > tokens: content = truncate_flutter_docs(content, class_name, tokens, strategy="balanced") final_tokens = token_manager.count_tokens(content) else: final_tokens = filtered_tokens else: # Use the token count from get_flutter_docs if no filtering final_tokens = flutter_doc.get("token_count", token_manager.count_tokens(content)) result.update({ "type": "flutter_class", "class": class_name, "library": flutter_doc.get("library"), "content": content, "source": flutter_doc.get("source"), "truncated": flutter_doc.get("truncated", False) or topic is not None, "token_count": final_tokens, "original_tokens": flutter_doc.get("original_tokens", final_tokens), "truncation_note": flutter_doc.get("truncation_note") }) return result elif doc_type != "auto": # Explicit Flutter class not found return { "identifier": identifier, "type": "flutter_class", "error": flutter_doc.get("error"), "suggestion": flutter_doc.get("suggestion") } if doc_type == "dart_class": # Try Dart documentation dart_doc = await get_flutter_docs(class_name, library, tokens=tokens) if "error" not in dart_doc: content = dart_doc.get("content", "") # Apply topic filtering if requested if topic: content = filter_documentation_by_topic(content, topic, "dart_class") # Recount tokens after filtering filtered_tokens = token_manager.count_tokens(content) # If filtering reduced content below token limit, no need for further truncation if tokens and filtered_tokens > tokens: content = truncate_flutter_docs(content, class_name, tokens, strategy="balanced") final_tokens = token_manager.count_tokens(content) else: final_tokens = filtered_tokens else: # Use the token count from get_flutter_docs if no filtering final_tokens = dart_doc.get("token_count", token_manager.count_tokens(content)) result.update({ "type": "dart_class", "class": class_name, "library": library, "content": content, "source": dart_doc.get("source"), "truncated": dart_doc.get("truncated", False) or topic is not None, "token_count": final_tokens, "original_tokens": dart_doc.get("original_tokens", final_tokens), "truncation_note": dart_doc.get("truncation_note") }) return result else: return { "identifier": identifier, "type": "dart_class", "error": dart_doc.get("error"), "suggestion": "Check the class name and library. Example: dart:async.Future" } if doc_type == "pub_package" or doc_type == "auto": # Try pub.dev package package_doc = await _get_pub_package_info_impl(package_name) if "error" not in package_doc: # Successfully found package # Format content based on topic if topic: content = format_package_content_by_topic(package_doc, topic) else: content = format_package_content(package_doc) # Count original tokens original_tokens = token_manager.count_tokens(content) truncated = False truncation_note = None # Apply token truncation if needed if tokens and original_tokens > tokens: truncator = create_truncator(tokens) content = truncator.truncate(content) truncated = True truncation_note = f"Documentation truncated from {original_tokens} to approximately {tokens} tokens" # Count final tokens final_tokens = token_manager.count_tokens(content) result.update({ "type": "pub_package", "package": package_name, "version": package_doc.get("version"), "content": content, "source": package_doc.get("source"), "metadata": { "description": package_doc.get("description"), "homepage": package_doc.get("homepage"), "repository": package_doc.get("repository"), "likes": package_doc.get("likes"), "pub_points": package_doc.get("pub_points"), "platforms": package_doc.get("platforms") }, "truncated": truncated or topic is not None, "token_count": final_tokens, "original_tokens": original_tokens if truncated else final_tokens, "truncation_note": truncation_note }) return result elif doc_type == "pub_package": # Explicit package not found return { "identifier": identifier, "type": "pub_package", "error": package_doc.get("error"), "suggestion": "Check the package name on pub.dev" } # If auto-detection failed to find anything if doc_type == "auto": # Try search as last resort search_results = await search_flutter_docs(identifier) if search_results.get("results"): top_result = search_results["results"][0] return { "identifier": identifier, "type": "search_suggestion", "error": f"Could not find exact match for '{identifier}'", "suggestion": f"Did you mean '{top_result['title']}'?", "search_results": search_results["results"][:3] } else: return { "identifier": identifier, "type": "not_found", "error": f"No documentation found for '{identifier}'", "suggestion": "Try using explicit prefixes like 'pub:', 'flutter:', or 'dart:'" } # Should not reach here return { "identifier": identifier, "type": "error", "error": "Failed to resolve identifier" } def filter_documentation_by_topic(content: str, topic: str, doc_type: str) -> str: """Filter documentation content by topic""" topic_lower = topic.lower() if doc_type in ["flutter_class", "dart_class"]: # Class documentation topics lines = content.split('\n') filtered_lines = [] current_section = None include_section = False for line in lines: # Detect section headers if line.startswith('## '): section_name = line[3:].lower() current_section = section_name # Determine if we should include this section if topic_lower == "constructors" and "constructor" in section_name: include_section = True elif topic_lower == "methods" and "method" in section_name: include_section = True elif topic_lower == "properties" and "propert" in section_name: include_section = True elif topic_lower == "examples" and ("example" in section_name or "code" in section_name): include_section = True else: include_section = False # Always include the class name and description if line.startswith('# ') or (current_section == "description" and not line.startswith('## ')): filtered_lines.append(line) elif include_section: filtered_lines.append(line) return '\n'.join(filtered_lines) return content def format_package_content(package_doc: Dict[str, Any]) -> str: """Format package documentation into readable content""" content = [] # Header content.append(f"# {package_doc['name']} v{package_doc['version']}") content.append("") # Description content.append("## Description") content.append(package_doc.get('description', 'No description available')) content.append("") # Metadata content.append("## Package Information") content.append(f"- **Version**: {package_doc['version']}") content.append(f"- **Published**: {package_doc.get('updated', 'Unknown')}") content.append(f"- **Publisher**: {package_doc.get('publisher', 'Unknown')}") content.append(f"- **Platforms**: {', '.join(package_doc.get('platforms', []))}") content.append(f"- **Likes**: {package_doc.get('likes', 0)}") content.append(f"- **Pub Points**: {package_doc.get('pub_points', 0)}") content.append(f"- **Popularity**: {package_doc.get('popularity', 0)}") content.append("") # Links if package_doc.get('homepage') or package_doc.get('repository'): content.append("## Links") if package_doc.get('homepage'): content.append(f"- **Homepage**: {package_doc['homepage']}") if package_doc.get('repository'): content.append(f"- **Repository**: {package_doc['repository']}") if package_doc.get('documentation'): content.append(f"- **Documentation**: {package_doc['documentation']}") content.append("") # Dependencies if package_doc.get('dependencies'): content.append("## Dependencies") for dep in package_doc['dependencies']: content.append(f"- {dep}") content.append("") # Environment if package_doc.get('environment'): content.append("## Environment") for key, value in package_doc['environment'].items(): content.append(f"- **{key}**: {value}") content.append("") # README if package_doc.get('readme'): content.append("## README") content.append(package_doc['readme']) return '\n'.join(content) def format_package_content_by_topic(package_doc: Dict[str, Any], topic: str) -> str: """Format package documentation filtered by topic""" topic_lower = topic.lower() content = [] # Always include header content.append(f"# {package_doc['name']} v{package_doc['version']}") content.append("") if topic_lower == "installation": content.append("## Installation") content.append("") content.append("Add this to your package's `pubspec.yaml` file:") content.append("") content.append("```yaml") content.append("dependencies:") content.append(f" {package_doc['name']}: ^{package_doc['version']}") content.append("```") content.append("") content.append("Then run:") content.append("```bash") content.append("flutter pub get") content.append("```") # Include environment requirements if package_doc.get('environment'): content.append("") content.append("### Requirements") for key, value in package_doc['environment'].items(): content.append(f"- **{key}**: {value}") elif topic_lower == "getting-started": content.append("## Getting Started") content.append("") content.append(package_doc.get('description', 'No description available')) content.append("") # Extract getting started section from README if available if package_doc.get('readme'): readme_lower = package_doc['readme'].lower() # Look for getting started section start_idx = readme_lower.find("getting started") if start_idx == -1: start_idx = readme_lower.find("quick start") if start_idx == -1: start_idx = readme_lower.find("usage") if start_idx != -1: # Extract section readme_section = package_doc['readme'][start_idx:] # Find next section header next_section = readme_section.find("\n## ") if next_section != -1: readme_section = readme_section[:next_section] content.append(readme_section) elif topic_lower == "examples": content.append("## Examples") content.append("") # Extract examples from README if package_doc.get('readme'): readme = package_doc['readme'] # Find code blocks code_blocks = re.findall(r'```[\w]*\n(.*?)\n```', readme, re.DOTALL) if code_blocks: for i, code in enumerate(code_blocks[:5]): # Limit to 5 examples content.append(f"### Example {i+1}") content.append("```dart") content.append(code) content.append("```") content.append("") else: content.append("No code examples found in documentation.") elif topic_lower == "api": content.append("## API Reference") content.append("") content.append(f"Full API documentation: https://pub.dev/documentation/{package_doc['name']}/latest/") content.append("") # Include basic package info content.append("### Package Information") content.append(f"- **Version**: {package_doc['version']}") content.append(f"- **Platforms**: {', '.join(package_doc.get('platforms', []))}") if package_doc.get('dependencies'): content.append("") content.append("### Dependencies") for dep in package_doc['dependencies']: content.append(f"- {dep}") else: # Default to full content for unknown topics return format_package_content(package_doc) return '\n'.join(content) @mcp.tool() async def process_flutter_mentions(text: str, tokens: int = 4000) -> Dict[str, Any]: """ Parse text for @flutter_mcp mentions and return relevant documentation. NOTE: This tool is maintained for backward compatibility. For new integrations, consider using the unified tools directly: - flutter_docs: For Flutter/Dart classes and pub.dev packages - flutter_search: For searching Flutter/Dart documentation Supports patterns like: - @flutter_mcp provider (pub.dev package - latest version) - @flutter_mcp provider:^6.0.0 (specific version constraint) - @flutter_mcp riverpod:2.5.1 (exact version) - @flutter_mcp dio:>=5.0.0 <6.0.0 (version range) - @flutter_mcp bloc:latest (latest version keyword) - @flutter_mcp material.AppBar (Flutter class) - @flutter_mcp dart:async.Future (Dart API) - @flutter_mcp Container (widget) Args: text: Text containing @flutter_mcp mentions tokens: Maximum token limit for each mention's documentation (default: 4000, min: 500) Returns: Dictionary with parsed mentions and their documentation """ bind_contextvars(tool="process_flutter_mentions", text_length=len(text)) # Validate tokens parameter if tokens < 500: return {"error": "tokens parameter must be at least 500"} # Updated pattern to match @flutter_mcp mentions with version constraints # Now supports version constraints like :^6.0.0, :>=5.0.0 <6.0.0, etc. pattern = r'@flutter_mcp\s+([a-zA-Z0-9_.:]+(?:\s*[<>=^]+\s*[0-9.+\-\w]+(?:\s*[<>=]+\s*[0-9.+\-\w]+)?)?)' mentions = re.findall(pattern, text) if not mentions: return { "mentions_found": 0, "message": "No @flutter_mcp mentions found in text", "results": [] } logger.info("mentions_found", count=len(mentions)) results = [] # Process each mention using the unified flutter_docs tool for mention in mentions: logger.info("processing_mention", mention=mention) try: # Parse version constraints if present if ':' in mention and not mention.startswith('dart:'): # Package with version constraint parts = mention.split(':', 1) identifier = parts[0] version_spec = parts[1] # For packages with version constraints, use get_pub_package_info if version_spec and version_spec != 'latest': # Extract actual version if it's a simple version number version = None if re.match(r'^\d+\.\d+\.\d+$', version_spec.strip()): version = version_spec.strip() # Get package with specific version doc_result = await get_pub_package_info(identifier, version=version) if "error" not in doc_result: results.append({ "mention": mention, "type": "pub_package", "documentation": doc_result }) if version_spec and version_spec != version: results[-1]["documentation"]["version_constraint"] = version_spec else: results.append({ "mention": mention, "type": "package_version_error", "error": doc_result["error"] }) else: # Latest version requested doc_result = await flutter_docs(identifier, max_tokens=tokens) else: # Use unified flutter_docs for all other cases doc_result = await flutter_docs(mention, max_tokens=tokens) # Process the result from flutter_docs if "error" not in doc_result: # Determine type based on result doc_type = doc_result.get("type", "unknown") if doc_type == "flutter_class": results.append({ "mention": mention, "type": "flutter_class", "documentation": doc_result }) elif doc_type == "dart_class": results.append({ "mention": mention, "type": "dart_api", "documentation": doc_result }) elif doc_type == "pub_package": results.append({ "mention": mention, "type": "pub_package", "documentation": doc_result }) else: # Fallback for auto-detected types results.append({ "mention": mention, "type": doc_result.get("type", "flutter_widget"), "documentation": doc_result }) else: # Try search as fallback search_result = await flutter_search(mention, limit=1) if search_result.get("results"): results.append({ "mention": mention, "type": search_result["results"][0].get("type", "flutter_widget"), "documentation": search_result["results"][0] }) else: results.append({ "mention": mention, "type": "not_found", "error": f"No documentation found for '{mention}'" }) except Exception as e: logger.error("mention_processing_error", mention=mention, error=str(e)) results.append({ "mention": mention, "type": "error", "error": f"Error processing mention: {str(e)}" }) # Format results - keep the same format for backward compatibility formatted_results = [] for result in results: if "error" in result: formatted_results.append({ "mention": result["mention"], "type": result["type"], "error": result["error"] }) else: doc = result["documentation"] if result["type"] == "pub_package": # Format package info formatted_result = { "mention": result["mention"], "type": "pub_package", "name": doc.get("name", ""), "version": doc.get("version", ""), "description": doc.get("description", ""), "documentation_url": doc.get("documentation", ""), "dependencies": doc.get("dependencies", []), "likes": doc.get("likes", 0), "pub_points": doc.get("pub_points", 0) } # Add version constraint info if present if "version_constraint" in doc: formatted_result["version_constraint"] = doc["version_constraint"] if "resolved_version" in doc: formatted_result["resolved_version"] = doc["resolved_version"] formatted_results.append(formatted_result) else: # Format Flutter/Dart documentation formatted_results.append({ "mention": result["mention"], "type": result["type"], "class": doc.get("class", doc.get("identifier", "")), "library": doc.get("library", ""), "content": doc.get("content", ""), "source": doc.get("source", "live") }) return { "mentions_found": len(mentions), "unique_mentions": len(set(mentions)), "results": formatted_results, "timestamp": datetime.utcnow().isoformat(), "note": "This tool is maintained for backward compatibility. Consider using flutter_docs or flutter_search directly." } def clean_readme_markdown(readme_content: str) -> str: """Clean and format README markdown for AI consumption""" if not readme_content: return "No README available" # Remove HTML comments readme_content = re.sub(r'<!--.*?-->', '', readme_content, flags=re.DOTALL) # Remove excessive blank lines readme_content = re.sub(r'\n{3,}', '\n\n', readme_content) # Remove badges/shields (common in READMEs but not useful for AI) readme_content = re.sub(r'!\[.*?\]\(.*?shields\.io.*?\)', '', readme_content) readme_content = re.sub(r'!\[.*?\]\(.*?badge.*?\)', '', readme_content) # Clean up any remaining formatting issues readme_content = readme_content.strip() return readme_content @mcp.tool() async def get_pub_package_info(package_name: str, version: Optional[str] = None, tokens: int = 6000) -> Dict[str, Any]: """ Get package information from pub.dev including README content. **DEPRECATED**: This tool is deprecated. Please use flutter_docs() instead with the "pub:" prefix (e.g., flutter_docs("pub:provider")). Args: package_name: Name of the pub.dev package (e.g., "provider", "bloc", "dio") version: Optional specific version to fetch (e.g., "6.0.5", "2.5.1") tokens: Maximum token limit for response (default: 6000, min: 500) Returns: Package information including version, description, metadata, and README """ bind_contextvars(tool="get_pub_package_info", package=package_name, version=version) logger.warning("deprecated_tool_usage", tool="get_pub_package_info", replacement="flutter_docs") # Validate tokens parameter if tokens < 500: return {"error": "tokens parameter must be at least 500"} # Call new flutter_docs tool identifier = f"pub:{package_name}" if version: identifier += f":{version}" result = await flutter_docs(identifier, max_tokens=tokens) # Transform back to old format if result.get("error"): return { "error": result["error"] } else: metadata = result.get("metadata", {}) return { "source": result.get("source", "live"), "name": result.get("package", package_name), "version": result.get("version", "latest"), "description": metadata.get("description", ""), "homepage": metadata.get("homepage", ""), "repository": metadata.get("repository", ""), "documentation": f"https://pub.dev/packages/{package_name}", "dependencies": [], # Not included in new format "readme": result.get("content", ""), "pub_points": metadata.get("pub_points", 0), "likes": metadata.get("likes", 0), "fetched_at": datetime.utcnow().isoformat() } async def _get_pub_package_info_impl(package_name: str, version: Optional[str] = None) -> Dict[str, Any]: """ Internal implementation of get_pub_package_info functionality. """ # Check cache first cache_key = get_cache_key("pub_package", package_name, version) # Check cache cached_data = cache_manager.get(cache_key) if cached_data: logger.info("cache_hit") return cached_data # Rate limit before fetching await rate_limiter.acquire() # Fetch from pub.dev API url = f"https://pub.dev/api/packages/{package_name}" logger.info("fetching_package", url=url) try: async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client: # Fetch package info response = await client.get( url, headers={ "User-Agent": "Flutter-MCP-Docs/1.0 (github.com/flutter-mcp/flutter-mcp)" } ) response.raise_for_status() data = response.json() # If specific version requested, find it in versions list if version: version_data = None for v in data.get("versions", []): if v.get("version") == version: version_data = v break if not version_data: return { "error": f"Version '{version}' not found for package '{package_name}'", "available_versions": [v.get("version") for v in data.get("versions", [])][:10] # Show first 10 } pubspec = version_data.get("pubspec", {}) actual_version = version_data.get("version", version) published_date = version_data.get("published", "") else: # Use latest version latest = data.get("latest", {}) pubspec = latest.get("pubspec", {}) actual_version = latest.get("version", "unknown") published_date = latest.get("published", "") result = { "source": "live", "name": package_name, "version": actual_version, "description": pubspec.get("description", "No description available"), "homepage": pubspec.get("homepage", ""), "repository": pubspec.get("repository", ""), "documentation": pubspec.get("documentation", f"https://pub.dev/packages/{package_name}"), "dependencies": list(pubspec.get("dependencies", {}).keys()), "dev_dependencies": list(pubspec.get("dev_dependencies", {}).keys()), "environment": pubspec.get("environment", {}), "platforms": data.get("platforms", []), "updated": published_date, "publisher": data.get("publisher", ""), "likes": data.get("likeCount", 0), "pub_points": data.get("pubPoints", 0), "popularity": data.get("popularityScore", 0) } # Fetch README content from package page # For specific versions, pub.dev uses /versions/{version} path if version: readme_url = f"https://pub.dev/packages/{package_name}/versions/{actual_version}" else: readme_url = f"https://pub.dev/packages/{package_name}" logger.info("fetching_readme", url=readme_url) try: # Rate limit before second request await rate_limiter.acquire() readme_response = await client.get( readme_url, headers={ "User-Agent": "Flutter-MCP-Docs/1.0 (github.com/flutter-mcp/flutter-mcp)" } ) readme_response.raise_for_status() # Parse page HTML to extract README soup = BeautifulSoup(readme_response.text, 'html.parser') # Find the README content - pub.dev uses a section with specific classes readme_div = soup.find('section', class_='detail-tab-readme-content') if not readme_div: # Try finding any section with markdown-body class readme_div = soup.find('section', class_='markdown-body') if not readme_div: # Try finding div with markdown-body readme_div = soup.find('div', class_='markdown-body') if readme_div: # Extract text content and preserve basic markdown structure # Convert common HTML elements back to markdown for br in readme_div.find_all('br'): br.replace_with('\n') for p in readme_div.find_all('p'): p.insert_after('\n\n') for h1 in readme_div.find_all('h1'): h1.insert_before('# ') h1.insert_after('\n\n') for h2 in readme_div.find_all('h2'): h2.insert_before('## ') h2.insert_after('\n\n') for h3 in readme_div.find_all('h3'): h3.insert_before('### ') h3.insert_after('\n\n') for code in readme_div.find_all('code'): if code.parent.name != 'pre': code.insert_before('`') code.insert_after('`') for pre in readme_div.find_all('pre'): code_block = pre.find('code') if code_block: lang_class = code_block.get('class', []) lang = '' for cls in lang_class if isinstance(lang_class, list) else [lang_class]: if cls and cls.startswith('language-'): lang = cls.replace('language-', '') break pre.insert_before(f'\n```{lang}\n') pre.insert_after('\n```\n') readme_text = readme_div.get_text() result["readme"] = clean_readme_markdown(readme_text) else: result["readme"] = "README parsing failed - content structure not recognized" except httpx.HTTPStatusError as e: logger.warning("readme_fetch_failed", status_code=e.response.status_code) result["readme"] = f"README not available (HTTP {e.response.status_code})" except Exception as e: logger.warning("readme_fetch_error", error=str(e)) result["readme"] = f"Failed to fetch README: {str(e)}" # Cache for 12 hours cache_manager.set(cache_key, result, CACHE_DURATIONS["pub_package"]) logger.info("package_fetched_success", has_readme="readme" in result) return result except httpx.HTTPStatusError as e: logger.error("http_error", status_code=e.response.status_code) return { "error": f"Package '{package_name}' not found on pub.dev", "status_code": e.response.status_code } except Exception as e: logger.error("fetch_error", error=str(e)) return { "error": f"Failed to fetch package information: {str(e)}" } @mcp.tool() async def flutter_search(query: str, limit: int = 10, tokens: int = 5000) -> Dict[str, Any]: """ Search across multiple Flutter/Dart documentation sources with unified results. Searches Flutter classes, Dart classes, pub packages, and concepts in parallel. Returns structured results with relevance scoring and documentation hints. Args: query: Search query (e.g., "state management", "Container", "http") limit: Maximum number of results to return (default: 10, max: 25) tokens: Maximum token limit for response (default: 5000, min: 500) Returns: Unified search results with type classification and relevance scores """ bind_contextvars(tool="flutter_search", query=query, limit=limit) logger.info("unified_search_started") # Validate tokens parameter if tokens < 500: return {"error": "tokens parameter must be at least 500"} # Validate limit limit = min(max(limit, 1), 25) # Check cache for search results cache_key = get_cache_key("unified_search", f"{query}:{limit}") cached_data = cache_manager.get(cache_key) if cached_data: logger.info("unified_search_cache_hit") return cached_data # Prepare search tasks for parallel execution search_tasks = [] results = [] query_lower = query.lower() # Define search functions for parallel execution async def search_flutter_classes(): """Search Flutter widget/class documentation""" flutter_results = [] # Check if query is a direct Flutter class reference if url := resolve_flutter_url(query): # Extract class and library info from resolved URL library = "widgets" # Default if "flutter/material" in url: library = "material" elif "flutter/cupertino" in url: library = "cupertino" elif "flutter/animation" in url: library = "animation" elif "flutter/painting" in url: library = "painting" elif "flutter/rendering" in url: library = "rendering" elif "flutter/services" in url: library = "services" elif "flutter/gestures" in url: library = "gestures" elif "flutter/foundation" in url: library = "foundation" class_match = re.search(r'/([^/]+)-class\.html$', url) if class_match: class_name = class_match.group(1) flutter_results.append({ "id": f"flutter:{library}:{class_name}", "type": "flutter_class", "relevance": 1.0, "title": class_name, "library": library, "description": f"Flutter {library} class", "doc_size": "large", "url": url }) # Search common Flutter classes flutter_classes = [ # State management ("StatefulWidget", "widgets", "Base class for widgets that have mutable state", ["state", "stateful", "widget"]), ("StatelessWidget", "widgets", "Base class for widgets that don't require mutable state", ["state", "stateless", "widget"]), ("State", "widgets", "Logic and internal state for a StatefulWidget", ["state", "lifecycle"]), ("InheritedWidget", "widgets", "Base class for widgets that propagate information down the tree", ["inherited", "propagate", "state"]), ("ValueListenableBuilder", "widgets", "Rebuilds when ValueListenable changes", ["value", "listenable", "builder", "state"]), # Layout widgets ("Container", "widgets", "A convenience widget that combines common painting, positioning, and sizing", ["container", "box", "layout"]), ("Row", "widgets", "Displays children in a horizontal array", ["row", "horizontal", "layout"]), ("Column", "widgets", "Displays children in a vertical array", ["column", "vertical", "layout"]), ("Stack", "widgets", "Positions children relative to the box edges", ["stack", "overlay", "position"]), ("Scaffold", "material", "Basic material design visual layout structure", ["scaffold", "material", "layout", "structure"]), # Navigation ("Navigator", "widgets", "Manages a stack of Route objects", ["navigator", "navigation", "route"]), ("MaterialPageRoute", "material", "A modal route that replaces the entire screen", ["route", "navigation", "page"]), # Input widgets ("TextField", "material", "A material design text field", ["text", "input", "field", "form"]), ("GestureDetector", "widgets", "Detects gestures on widgets", ["gesture", "touch", "tap", "click"]), # Lists ("ListView", "widgets", "Scrollable list of widgets", ["list", "scroll", "view"]), ("GridView", "widgets", "Scrollable 2D array of widgets", ["grid", "scroll", "view"]), # Visual ("AppBar", "material", "A material design app bar", ["app", "bar", "header", "material"]), ("Card", "material", "A material design card", ["card", "material"]), # Async ("FutureBuilder", "widgets", "Builds based on interaction with a Future", ["future", "async", "builder"]), ("StreamBuilder", "widgets", "Builds based on interaction with a Stream", ["stream", "async", "builder"]), ] for class_name, library, description, keywords in flutter_classes: # Calculate relevance based on query match relevance = 0.0 # Direct match if query_lower == class_name.lower(): relevance = 1.0 elif query_lower in class_name.lower(): relevance = 0.8 elif class_name.lower() in query_lower: relevance = 0.7 # Keyword match if relevance < 0.3: for keyword in keywords: if keyword in query_lower or query_lower in keyword: relevance = max(relevance, 0.5) break # Description match if relevance < 0.3 and query_lower in description.lower(): relevance = 0.4 if relevance > 0.3: flutter_results.append({ "id": f"flutter:{library}:{class_name}", "type": "flutter_class", "relevance": relevance, "title": class_name, "library": library, "description": description, "doc_size": "large" }) return flutter_results async def search_dart_classes(): """Search Dart core library documentation""" dart_results = [] dart_classes = [ ("List", "dart:core", "An indexable collection of objects with a length", ["list", "array", "collection"]), ("Map", "dart:core", "A collection of key/value pairs", ["map", "dictionary", "hash", "key", "value"]), ("Set", "dart:core", "A collection of objects with no duplicate elements", ["set", "unique", "collection"]), ("String", "dart:core", "A sequence of UTF-16 code units", ["string", "text"]), ("Future", "dart:async", "Represents a computation that completes with a value or error", ["future", "async", "promise"]), ("Stream", "dart:async", "A source of asynchronous data events", ["stream", "async", "event"]), ("Duration", "dart:core", "A span of time", ["duration", "time", "span"]), ("DateTime", "dart:core", "An instant in time", ["date", "time", "datetime"]), ("RegExp", "dart:core", "A regular expression pattern", ["regex", "regexp", "pattern"]), ("Iterable", "dart:core", "A collection of values that can be accessed sequentially", ["iterable", "collection", "sequence"]), ] for class_name, library, description, keywords in dart_classes: relevance = 0.0 # Direct match if query_lower == class_name.lower(): relevance = 1.0 elif query_lower in class_name.lower(): relevance = 0.8 elif class_name.lower() in query_lower: relevance = 0.7 # Keyword match if relevance < 0.3: for keyword in keywords: if keyword in query_lower or query_lower in keyword: relevance = max(relevance, 0.5) break # Description match if relevance < 0.3 and query_lower in description.lower(): relevance = 0.4 if relevance > 0.3: dart_results.append({ "id": f"dart:{library.replace('dart:', '')}:{class_name}", "type": "dart_class", "relevance": relevance, "title": class_name, "library": library, "description": description, "doc_size": "medium" }) return dart_results async def search_pub_packages(): """Search pub.dev packages""" package_results = [] # Define popular packages with categories packages = [ # State Management ("provider", "State management library that makes it easy to connect business logic to widgets", ["state", "management", "provider"], "state_management"), ("riverpod", "A reactive caching and data-binding framework", ["state", "management", "riverpod", "reactive"], "state_management"), ("bloc", "State management library implementing the BLoC design pattern", ["state", "management", "bloc", "pattern"], "state_management"), ("get", "Open source state management, navigation and utilities", ["state", "management", "get", "navigation"], "state_management"), # Networking ("dio", "Powerful HTTP client for Dart with interceptors and FormData", ["http", "network", "dio", "api"], "networking"), ("http", "A composable, multi-platform, Future-based API for HTTP requests", ["http", "network", "request"], "networking"), ("retrofit", "Type-safe HTTP client generator", ["http", "network", "retrofit", "generator"], "networking"), # Storage ("shared_preferences", "Flutter plugin for reading and writing simple key-value pairs", ["storage", "preferences", "settings"], "storage"), ("sqflite", "SQLite plugin for Flutter", ["database", "sqlite", "sql", "storage"], "storage"), ("hive", "Lightweight and blazing fast key-value database", ["database", "hive", "nosql", "storage"], "storage"), # Firebase ("firebase_core", "Flutter plugin to use Firebase Core API", ["firebase", "core", "backend"], "firebase"), ("firebase_auth", "Flutter plugin for Firebase Auth", ["firebase", "auth", "authentication"], "firebase"), ("cloud_firestore", "Flutter plugin for Cloud Firestore", ["firebase", "firestore", "database"], "firebase"), # UI/UX ("flutter_svg", "SVG rendering and widget library for Flutter", ["svg", "image", "vector", "ui"], "ui"), ("cached_network_image", "Flutter library to load and cache network images", ["image", "cache", "network", "ui"], "ui"), ("animations", "Beautiful pre-built animations for Flutter", ["animation", "transition", "ui"], "ui"), # Navigation ("go_router", "A declarative routing package for Flutter", ["navigation", "router", "routing"], "navigation"), ("auto_route", "Code generation for type-safe route navigation", ["navigation", "router", "generation"], "navigation"), # Platform ("url_launcher", "Flutter plugin for launching URLs", ["url", "launcher", "platform"], "platform"), ("path_provider", "Flutter plugin for getting commonly used locations on filesystem", ["path", "file", "platform"], "platform"), ("image_picker", "Flutter plugin for selecting images", ["image", "picker", "camera", "gallery"], "platform"), ] for package_name, description, keywords, category in packages: relevance = 0.0 # Direct match if query_lower == package_name: relevance = 1.0 elif query_lower in package_name: relevance = 0.8 elif package_name in query_lower: relevance = 0.7 # Keyword match if relevance < 0.3: for keyword in keywords: if keyword in query_lower or query_lower in keyword: relevance = max(relevance, 0.6) break # Category match if relevance < 0.3 and category in query_lower: relevance = 0.5 # Description match if relevance < 0.3 and query_lower in description.lower(): relevance = 0.4 if relevance > 0.3: package_results.append({ "id": f"pub:{package_name}", "type": "pub_package", "relevance": relevance, "title": package_name, "category": category, "description": description, "doc_size": "variable", "url": f"https://pub.dev/packages/{package_name}" }) return package_results async def search_concepts(): """Search programming concepts and patterns""" concept_results = [] concepts = { "state_management": { "title": "State Management in Flutter", "description": "Techniques for managing application state", "keywords": ["state", "management", "provider", "bloc", "riverpod"], "related": ["setState", "InheritedWidget", "provider", "bloc", "riverpod", "get"] }, "navigation": { "title": "Navigation & Routing", "description": "Moving between screens and managing navigation stack", "keywords": ["navigation", "routing", "navigator", "route", "screen"], "related": ["Navigator", "MaterialPageRoute", "go_router", "deep linking"] }, "async_programming": { "title": "Asynchronous Programming", "description": "Working with Futures, Streams, and async operations", "keywords": ["async", "future", "stream", "await", "asynchronous"], "related": ["Future", "Stream", "FutureBuilder", "StreamBuilder", "async/await"] }, "http_networking": { "title": "HTTP & Networking", "description": "Making HTTP requests and handling network operations", "keywords": ["http", "network", "api", "rest", "request"], "related": ["http", "dio", "retrofit", "REST API", "JSON"] }, "database_storage": { "title": "Database & Storage", "description": "Persisting data locally using various storage solutions", "keywords": ["database", "storage", "sqlite", "persistence", "cache"], "related": ["sqflite", "hive", "shared_preferences", "drift", "objectbox"] }, "animation": { "title": "Animations in Flutter", "description": "Creating smooth animations and transitions", "keywords": ["animation", "transition", "animate", "motion"], "related": ["AnimationController", "AnimatedBuilder", "Hero", "Curves"] }, "testing": { "title": "Testing Flutter Apps", "description": "Unit, widget, and integration testing strategies", "keywords": ["test", "testing", "unit", "widget", "integration"], "related": ["flutter_test", "mockito", "integration_test", "golden tests"] }, "architecture": { "title": "App Architecture Patterns", "description": "Organizing code with architectural patterns", "keywords": ["architecture", "pattern", "mvvm", "mvc", "clean"], "related": ["BLoC Pattern", "MVVM", "Clean Architecture", "Repository Pattern"] }, "performance": { "title": "Performance Optimization", "description": "Improving app performance and reducing jank", "keywords": ["performance", "optimization", "speed", "jank", "profile"], "related": ["Performance Profiling", "Widget Inspector", "const constructors"] }, "platform_integration": { "title": "Platform Integration", "description": "Integrating with native platform features", "keywords": ["platform", "native", "channel", "integration", "plugin"], "related": ["Platform Channels", "Method Channel", "Plugin Development"] } } for concept_id, concept_data in concepts.items(): relevance = 0.0 # Check keywords for keyword in concept_data["keywords"]: if keyword in query_lower or query_lower in keyword: relevance = max(relevance, 0.7) # Check title if query_lower in concept_data["title"].lower(): relevance = max(relevance, 0.8) # Check description if relevance < 0.3 and query_lower in concept_data["description"].lower(): relevance = 0.5 if relevance > 0.3: concept_results.append({ "id": f"concept:{concept_id}", "type": "concept", "relevance": relevance, "title": concept_data["title"], "description": concept_data["description"], "related_items": concept_data["related"], "doc_size": "summary" }) return concept_results # Execute all searches in parallel flutter_task = asyncio.create_task(search_flutter_classes()) dart_task = asyncio.create_task(search_dart_classes()) pub_task = asyncio.create_task(search_pub_packages()) concept_task = asyncio.create_task(search_concepts()) # Wait for all searches to complete flutter_results, dart_results, pub_results, concept_results = await asyncio.gather( flutter_task, dart_task, pub_task, concept_task ) # Combine all results all_results = flutter_results + dart_results + pub_results + concept_results # Sort by relevance and limit all_results.sort(key=lambda x: x["relevance"], reverse=True) results = all_results[:limit] # Add search metadata response = { "query": query, "total_results": len(all_results), "returned_results": len(results), "results": results, "result_types": { "flutter_classes": sum(1 for r in results if r["type"] == "flutter_class"), "dart_classes": sum(1 for r in results if r["type"] == "dart_class"), "pub_packages": sum(1 for r in results if r["type"] == "pub_package"), "concepts": sum(1 for r in results if r["type"] == "concept") }, "timestamp": datetime.utcnow().isoformat() } # Add search suggestions if results are limited if len(results) < 5: suggestions = [] if not any(r["type"] == "flutter_class" for r in results): suggestions.append("Try searching for specific widget names like 'Container' or 'Scaffold'") if not any(r["type"] == "pub_package" for r in results): suggestions.append("Search for package names like 'provider' or 'dio'") if not any(r["type"] == "concept" for r in results): suggestions.append("Try broader concepts like 'state management' or 'navigation'") response["suggestions"] = suggestions # Cache the results for 1 hour cache_manager.set(cache_key, response, 3600) logger.info("unified_search_completed", total_results=len(all_results), returned_results=len(results)) return response @mcp.tool() async def flutter_status() -> Dict[str, Any]: """ Check the health status of all Flutter documentation services. Returns: Health status including individual service checks and cache statistics """ checks = {} overall_status = "ok" timestamp = datetime.utcnow().isoformat() # Check Flutter docs scraper flutter_start = time.time() try: # Test with Container widget - a stable, core widget unlikely to be removed result = await get_flutter_docs("Container", "widgets") flutter_duration = int((time.time() - flutter_start) * 1000) if "error" in result: checks["flutter_docs"] = { "status": "failed", "target": "Container widget", "duration_ms": flutter_duration, "error": result["error"] } overall_status = "degraded" else: checks["flutter_docs"] = { "status": "ok", "target": "Container widget", "duration_ms": flutter_duration, "cached": result.get("source") == "cache" } except Exception as e: flutter_duration = int((time.time() - flutter_start) * 1000) checks["flutter_docs"] = { "status": "failed", "target": "Container widget", "duration_ms": flutter_duration, "error": str(e) } overall_status = "failed" # Check pub.dev scraper pub_start = time.time() try: # Test with provider package - extremely popular, unlikely to be removed result = await get_pub_package_info("provider") pub_duration = int((time.time() - pub_start) * 1000) if result is None: checks["pub_dev"] = { "status": "timeout", "target": "provider package", "duration_ms": pub_duration, "error": "Health check timed out after 10 seconds" } overall_status = "degraded" if overall_status == "ok" else overall_status elif result.get("error"): checks["pub_dev"] = { "status": "failed", "target": "provider package", "duration_ms": pub_duration, "error": result.get("message", "Unknown error"), "error_type": result.get("error_type", "unknown") } overall_status = "degraded" if overall_status == "ok" else overall_status else: # Additional validation - check if we got expected fields has_version = "version" in result and result["version"] != "unknown" has_readme = "readme" in result and len(result.get("readme", "")) > 100 if not has_version: checks["pub_dev"] = { "status": "degraded", "target": "provider package", "duration_ms": pub_duration, "error": "Could not parse version information", "cached": result.get("source") == "cache" } overall_status = "degraded" if overall_status == "ok" else overall_status elif not has_readme: checks["pub_dev"] = { "status": "degraded", "target": "provider package", "duration_ms": pub_duration, "error": "Could not parse README content", "cached": result.get("source") == "cache" } overall_status = "degraded" if overall_status == "ok" else overall_status else: checks["pub_dev"] = { "status": "ok", "target": "provider package", "duration_ms": pub_duration, "version": result["version"], "cached": result.get("source") == "cache" } except Exception as e: pub_duration = int((time.time() - pub_start) * 1000) checks["pub_dev"] = { "status": "failed", "target": "provider package", "duration_ms": pub_duration, "error": str(e) } overall_status = "failed" if overall_status == "failed" else "degraded" # Check cache status try: cache_stats = cache_manager.get_stats() checks["cache"] = { "status": "ok", "message": "SQLite cache operational", "stats": cache_stats } except Exception as e: checks["cache"] = { "status": "degraded", "message": "Cache error", "error": str(e) } overall_status = "degraded" return { "status": overall_status, "timestamp": timestamp, "checks": checks, "message": get_health_message(overall_status) } @mcp.tool() async def health_check() -> Dict[str, Any]: """ Check the health status of all scrapers and services. **DEPRECATED**: This tool is deprecated. Please use flutter_status() instead. Returns: Health status including individual scraper checks and overall status """ logger.warning("deprecated_tool_usage", tool="health_check", replacement="flutter_status") # Simply call the new flutter_status function return await flutter_status() def get_health_message(status: str) -> str: """Get a human-readable message for the health status""" messages = { "ok": "All systems operational", "degraded": "Service degraded - some features may be slow or unavailable", "failed": "Service failed - critical components are not working" } return messages.get(status, "Unknown status") def main(): """Main entry point for the Flutter MCP server""" import os # When running from CLI, the header is already printed # Only log when not running from CLI (e.g., direct execution) if not hasattr(sys, '_flutter_mcp_cli'): logger.info("flutter_mcp_starting", version="0.1.0") # Initialize cache and show stats try: cache_stats = cache_manager.get_stats() logger.info("cache_ready", stats=cache_stats) except Exception as e: logger.warning("cache_initialization_warning", error=str(e)) # Get transport configuration from environment transport = os.environ.get('MCP_TRANSPORT', 'stdio') host = os.environ.get('MCP_HOST', '127.0.0.1') port = int(os.environ.get('MCP_PORT', '8000')) # Run the MCP server with appropriate transport if transport == 'stdio': mcp.run() elif transport == 'sse': logger.info("starting_sse_transport", host=host, port=port) mcp.run(transport='sse', host=host, port=port) elif transport == 'http': logger.info("starting_http_transport", host=host, port=port) # FastMCP handles HTTP transport internally mcp.run(transport='http', host=host, port=port, path='/mcp') if __name__ == "__main__": main()

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/adamsmaka/flutter-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server