This is page 3 of 3. Use http://codebase.md/adamsmaka/flutter-mcp?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .dockerignore
├── .github
│ └── workflows
│ ├── build-executables.yml
│ └── publish-pypi.yml
├── .gitignore
├── .pypirc.template
├── .python-version
├── build.spec
├── CHANGELOG.md
├── CLAUDE.md
├── context7-installation-analysis.md
├── docker
│ ├── docker-compose.yml
│ └── Dockerfile
├── docs
│ ├── api-reference.md
│ ├── CLIENT-CONFIGURATIONS.md
│ ├── CONTRIBUTING.md
│ ├── DEVELOPMENT.md
│ ├── planning
│ │ ├── context7-marketing-analysis.md
│ │ ├── flutter-mcp-project-summary.md
│ │ ├── IMPLEMENTATION_SUMMARY.md
│ │ ├── ingestion-strategy.md
│ │ ├── initial-vision.md
│ │ ├── project-summary.md
│ │ ├── project-tasks.csv
│ │ ├── README-highlights.md
│ │ └── task-summary.md
│ ├── PUBLISHING.md
│ ├── README.md
│ ├── token-counting-analysis.md
│ ├── token-management-implementation.md
│ ├── TOOL-CONSOLIDATION-PLAN.md
│ ├── truncation-algorithm.md
│ └── VERSION_SPECIFICATION.md
├── ERROR_HANDLING.md
├── examples
│ ├── token_management_demo.py
│ └── truncation_demo.py
├── LICENSE
├── MANIFEST.in
├── npm-wrapper
│ ├── .gitignore
│ ├── bin
│ │ └── flutter-mcp.js
│ ├── index.js
│ ├── package.json
│ ├── publish.sh
│ ├── README.md
│ └── scripts
│ └── install.js
├── pyproject.toml
├── QUICK_FIX.md
├── README.md
├── RELEASE_GUIDE.md
├── scripts
│ ├── build-executables.sh
│ ├── publish-pypi.sh
│ └── test-server.sh
├── setup.py
├── src
│ └── flutter_mcp
│ ├── __init__.py
│ ├── __main__.py
│ ├── cache.py
│ ├── cli.py
│ ├── error_handling.py
│ ├── logging_utils.py
│ ├── recovery.py
│ ├── server.py
│ ├── token_manager.py
│ └── truncation.py
├── tests
│ ├── __init__.py
│ ├── test_integration.py
│ ├── test_token_management.py
│ ├── test_tools.py
│ └── test_truncation.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/src/flutter_mcp/server.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """Flutter MCP Server - Real-time Flutter/Dart documentation for AI assistants"""
3 |
4 | import asyncio
5 | import json
6 | import re
7 | from typing import Optional, Dict, List, Any, Tuple
8 | from datetime import datetime
9 | import time
10 |
11 | from mcp.server.fastmcp import FastMCP
12 | import httpx
13 | # Redis removed - using SQLite cache instead
14 | from bs4 import BeautifulSoup
15 | import structlog
16 | from structlog.contextvars import bind_contextvars
17 | from rich.console import Console
18 |
19 | # Import our custom logging utilities
20 | from .logging_utils import format_cache_stats, print_server_header
21 |
22 | # Initialize structured logging
23 | # IMPORTANT: For MCP servers, logs must go to stderr, not stdout
24 | # stdout is reserved for the JSON-RPC protocol
25 | import sys
26 | import logging
27 |
28 | # Configure structlog with enhanced formatting
29 | structlog.configure(
30 | processors=[
31 | structlog.contextvars.merge_contextvars,
32 | structlog.processors.add_log_level,
33 | structlog.processors.StackInfoRenderer(),
34 | structlog.dev.set_exc_info,
35 | structlog.processors.TimeStamper(fmt="%H:%M:%S", utc=False),
36 | # Our custom processor comes before the renderer!
37 | format_cache_stats,
38 | # Use ConsoleRenderer for beautiful colored output
39 | structlog.dev.ConsoleRenderer(
40 | colors=True,
41 | exception_formatter=structlog.dev.plain_traceback,
42 | ),
43 | ],
44 | wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
45 | context_class=dict,
46 | logger_factory=structlog.PrintLoggerFactory(file=sys.stderr),
47 | cache_logger_on_first_use=True,
48 | )
49 | logger = structlog.get_logger()
50 |
51 | # Rich console for direct output
52 | console = Console(stderr=True)
53 |
54 | # Initialize FastMCP server
55 | mcp = FastMCP("Flutter Docs Server")
56 |
57 | # Import our SQLite-based cache
58 | from .cache import get_cache
59 | # Import error handling utilities
60 | from .error_handling import (
61 | NetworkError, DocumentationNotFoundError, RateLimitError,
62 | with_retry, safe_http_get, format_error_response,
63 | CircuitBreaker
64 | )
65 | # Legacy version parser functionality now integrated in resolve_identifier()
66 | # Import truncation utilities
67 | from .truncation import truncate_flutter_docs, create_truncator, DocumentTruncator
68 | # Import token management
69 | from .token_manager import TokenManager
70 |
71 | # Initialize cache manager
72 | cache_manager = get_cache()
73 | logger.info("cache_initialized", cache_type="sqlite", path=cache_manager.db_path)
74 |
75 | # Initialize token manager
76 | token_manager = TokenManager()
77 |
78 |
79 | class RateLimiter:
80 | """Rate limiter for respectful web scraping (2 requests/second)"""
81 |
82 | def __init__(self, calls_per_second: float = 2.0):
83 | self.semaphore = asyncio.Semaphore(1)
84 | self.min_interval = 1.0 / calls_per_second
85 | self.last_call = 0
86 |
87 | async def acquire(self):
88 | async with self.semaphore:
89 | current_time = time.time()
90 | elapsed = current_time - self.last_call
91 | if elapsed < self.min_interval:
92 | await asyncio.sleep(self.min_interval - elapsed)
93 | self.last_call = time.time()
94 |
95 |
96 | # Global rate limiter instance
97 | rate_limiter = RateLimiter()
98 |
99 |
100 | # ============================================================================
101 | # Helper Functions for Tool Consolidation
102 | # ============================================================================
103 |
104 | def resolve_identifier(identifier: str) -> Tuple[str, str, Optional[str]]:
105 | """
106 | Resolve an identifier to determine its type and clean form.
107 |
108 | Args:
109 | identifier: The identifier to resolve (e.g., "Container", "material.AppBar",
110 | "dart:async.Future", "provider", "dio:^5.0.0")
111 |
112 | Returns:
113 | Tuple of (type, clean_id, library) where:
114 | - type: "flutter_class", "dart_class", "pub_package", or "unknown"
115 | - clean_id: Cleaned identifier without prefixes or version constraints
116 | - library: Library name for classes, None for packages
117 | """
118 | # Check for version constraint (indicates package)
119 | if ':' in identifier and not identifier.startswith('dart:'):
120 | # It's a package with version constraint
121 | package_name = identifier.split(':')[0]
122 | return ("pub_package", package_name, None)
123 |
124 | # Check for Dart API pattern (dart:library.Class)
125 | if identifier.startswith('dart:'):
126 | match = re.match(r'dart:(\w+)\.(\w+)', identifier)
127 | if match:
128 | library = f"dart:{match.group(1)}"
129 | class_name = match.group(2)
130 | return ("dart_class", class_name, library)
131 | else:
132 | # Just dart:library without class
133 | return ("dart_class", identifier, None)
134 |
135 | # Check for Flutter library.class pattern
136 | flutter_libs = ['widgets', 'material', 'cupertino', 'painting', 'animation',
137 | 'rendering', 'services', 'gestures', 'foundation']
138 | for lib in flutter_libs:
139 | if identifier.startswith(f"{lib}."):
140 | class_name = identifier.split('.', 1)[1]
141 | return ("flutter_class", class_name, lib)
142 |
143 | # Check if it's a known Flutter widget (common ones)
144 | common_widgets = ['Container', 'Row', 'Column', 'Text', 'Scaffold', 'AppBar',
145 | 'ListView', 'GridView', 'Stack', 'Card', 'IconButton']
146 | if identifier in common_widgets:
147 | return ("flutter_class", identifier, "widgets")
148 |
149 | # Check if it looks like a package name (lowercase, may contain underscores)
150 | if identifier.islower() or '_' in identifier:
151 | return ("pub_package", identifier, None)
152 |
153 | # Default to unknown
154 | return ("unknown", identifier, None)
155 |
156 |
157 | def filter_by_topic(content: str, topic: str, doc_type: str) -> str:
158 | """
159 | Extract specific sections from documentation based on topic.
160 |
161 | Args:
162 | content: Full documentation content
163 | topic: Topic to filter by (e.g., "constructors", "methods", "properties",
164 | "examples", "dependencies", "usage")
165 | doc_type: Type of documentation ("flutter_class", "dart_class", "pub_package")
166 |
167 | Returns:
168 | Filtered content containing only the requested topic
169 | """
170 | if not content:
171 | return "No content available"
172 |
173 | topic_lower = topic.lower()
174 |
175 | if doc_type in ["flutter_class", "dart_class"]:
176 | # For class documentation, extract specific sections
177 | lines = content.split('\n')
178 | in_section = False
179 | section_content = []
180 | section_headers = {
181 | "constructors": ["## Constructors", "### Constructors"],
182 | "methods": ["## Methods", "### Methods"],
183 | "properties": ["## Properties", "### Properties"],
184 | "examples": ["## Code Examples", "### Examples", "## Examples"],
185 | "description": ["## Description", "### Description"],
186 | }
187 |
188 | if topic_lower in section_headers:
189 | headers = section_headers[topic_lower]
190 | for i, line in enumerate(lines):
191 | if any(header in line for header in headers):
192 | in_section = True
193 | section_content.append(line)
194 | elif in_section and line.startswith('##'):
195 | # Reached next major section
196 | break
197 | elif in_section:
198 | section_content.append(line)
199 |
200 | if section_content:
201 | return '\n'.join(section_content)
202 | else:
203 | return f"No {topic} section found in documentation"
204 | else:
205 | return f"Unknown topic '{topic}' for class documentation"
206 |
207 | elif doc_type == "pub_package":
208 | # For package documentation, different sections
209 | if topic_lower == "dependencies":
210 | # Extract dependencies from the content
211 | deps_match = re.search(r'"dependencies":\s*\[(.*?)\]', content, re.DOTALL)
212 | if deps_match:
213 | deps = deps_match.group(1)
214 | return f"Dependencies: {deps}"
215 | return "No dependencies information found"
216 |
217 | elif topic_lower == "usage":
218 | # Try to extract usage/getting started section from README
219 | if "readme" in content.lower():
220 | # Look for usage patterns in README
221 | patterns = [r'## Usage.*?(?=##|\Z)', r'## Getting Started.*?(?=##|\Z)',
222 | r'## Quick Start.*?(?=##|\Z)', r'## Installation.*?(?=##|\Z)']
223 | for pattern in patterns:
224 | match = re.search(pattern, content, re.DOTALL | re.IGNORECASE)
225 | if match:
226 | return match.group(0).strip()
227 | return "No usage information found"
228 |
229 | elif topic_lower == "examples":
230 | # Extract code examples from README
231 | code_blocks = re.findall(r'```(?:dart|flutter)?\n(.*?)\n```', content, re.DOTALL)
232 | if code_blocks:
233 | examples = []
234 | for i, code in enumerate(code_blocks[:5]): # Limit to 5 examples
235 | examples.append(f"Example {i+1}:\n```dart\n{code}\n```")
236 | return '\n\n'.join(examples)
237 | return "No code examples found"
238 |
239 | # Default: return full content if topic not recognized
240 | return content
241 |
242 |
243 | def to_unified_id(doc_type: str, identifier: str, library: str = None) -> str:
244 | """
245 | Convert documentation reference to unified ID format.
246 |
247 | Args:
248 | doc_type: Type of documentation ("flutter_class", "dart_class", "pub_package")
249 | identifier: The identifier (class name or package name)
250 | library: Optional library name for classes
251 |
252 | Returns:
253 | Unified ID string (e.g., "flutter:material.AppBar", "dart:async.Future", "package:dio")
254 | """
255 | if doc_type == "flutter_class":
256 | if library:
257 | return f"flutter:{library}.{identifier}"
258 | else:
259 | return f"flutter:widgets.{identifier}" # Default to widgets
260 | elif doc_type == "dart_class":
261 | if library:
262 | return f"{library}.{identifier}"
263 | else:
264 | return f"dart:core.{identifier}" # Default to core
265 | elif doc_type == "pub_package":
266 | return f"package:{identifier}"
267 | else:
268 | return identifier
269 |
270 |
271 | def from_unified_id(unified_id: str) -> Tuple[str, str, Optional[str]]:
272 | """
273 | Parse unified ID format back to components.
274 |
275 | Args:
276 | unified_id: Unified ID string (e.g., "flutter:material.AppBar")
277 |
278 | Returns:
279 | Tuple of (type, identifier, library)
280 | """
281 | if unified_id.startswith("flutter:"):
282 | parts = unified_id[8:].split('.', 1) # Remove "flutter:" prefix
283 | if len(parts) == 2:
284 | return ("flutter_class", parts[1], parts[0])
285 | else:
286 | return ("flutter_class", parts[0], "widgets")
287 |
288 | elif unified_id.startswith("dart:"):
289 | match = re.match(r'(dart:\w+)\.(\w+)', unified_id)
290 | if match:
291 | return ("dart_class", match.group(2), match.group(1))
292 | else:
293 | return ("dart_class", unified_id, None)
294 |
295 | elif unified_id.startswith("package:"):
296 | return ("pub_package", unified_id[8:], None)
297 |
298 | else:
299 | return ("unknown", unified_id, None)
300 |
301 |
302 | def estimate_doc_size(content: str) -> str:
303 | """
304 | Estimate documentation size category based on content length.
305 |
306 | Args:
307 | content: Documentation content
308 |
309 | Returns:
310 | Size category: "small", "medium", or "large"
311 | """
312 | if not content:
313 | return "small"
314 |
315 | # Rough token estimation (1 token ≈ 4 characters)
316 | estimated_tokens = len(content) / 4
317 |
318 | if estimated_tokens < 1000:
319 | return "small"
320 | elif estimated_tokens < 4000:
321 | return "medium"
322 | else:
323 | return "large"
324 |
325 |
326 | def rank_results(results: List[Dict[str, Any]], query: str) -> List[Dict[str, Any]]:
327 | """
328 | Rank search results based on relevance to query.
329 |
330 | Args:
331 | results: List of search results
332 | query: Original search query
333 |
334 | Returns:
335 | Sorted list of results with updated relevance scores
336 | """
337 | query_lower = query.lower()
338 | query_words = set(query_lower.split())
339 |
340 | for result in results:
341 | # Start with existing relevance score if present
342 | score = result.get("relevance", 0.5)
343 |
344 | # Boost for exact title match
345 | title = result.get("title", "").lower()
346 | if query_lower == title:
347 | score += 0.5
348 | elif query_lower in title:
349 | score += 0.3
350 |
351 | # Boost for word matches in title
352 | title_words = set(title.split())
353 | word_overlap = len(query_words & title_words) / len(query_words) if query_words else 0
354 | score += word_overlap * 0.2
355 |
356 | # Consider description matches
357 | description = result.get("description", "").lower()
358 | if query_lower in description:
359 | score += 0.1
360 |
361 | # Boost for type preferences
362 | if "state" in query_lower and result.get("type") == "concept":
363 | score += 0.2
364 | elif "package" in query_lower and result.get("type") == "pub_package":
365 | score += 0.2
366 | elif any(word in query_lower for word in ["widget", "class"]) and result.get("type") == "flutter_class":
367 | score += 0.2
368 |
369 | # Cap score at 1.0
370 | result["relevance"] = min(score, 1.0)
371 |
372 | # Sort by relevance score (descending)
373 | return sorted(results, key=lambda x: x.get("relevance", 0), reverse=True)
374 |
375 |
376 | # Circuit breakers for external services
377 | flutter_docs_circuit = CircuitBreaker(
378 | failure_threshold=5,
379 | recovery_timeout=60.0,
380 | expected_exception=(NetworkError, httpx.HTTPStatusError)
381 | )
382 |
383 | pub_dev_circuit = CircuitBreaker(
384 | failure_threshold=5,
385 | recovery_timeout=60.0,
386 | expected_exception=(NetworkError, httpx.HTTPStatusError)
387 | )
388 |
389 | # Cache TTL strategy (in seconds)
390 | CACHE_DURATIONS = {
391 | "flutter_api": 86400, # 24 hours for stable APIs
392 | "dart_api": 86400, # 24 hours for Dart APIs
393 | "pub_package": 43200, # 12 hours for packages (may update more frequently)
394 | "cookbook": 604800, # 7 days for examples
395 | "stackoverflow": 3600, # 1 hour for community content
396 | }
397 |
398 |
399 | def get_cache_key(doc_type: str, identifier: str, version: str = None) -> str:
400 | """Generate cache keys for different documentation types"""
401 | if version:
402 | # Normalize version string for cache key
403 | version = version.replace(' ', '_').replace('>=', 'gte').replace('<=', 'lte').replace('^', 'caret')
404 | return f"{doc_type}:{identifier}:{version}"
405 | return f"{doc_type}:{identifier}"
406 |
407 |
408 | def clean_text(element) -> str:
409 | """Clean and extract text from BeautifulSoup element"""
410 | if not element:
411 | return ""
412 | text = element.get_text(strip=True)
413 | # Remove excessive whitespace
414 | text = re.sub(r'\s+', ' ', text)
415 | return text.strip()
416 |
417 |
418 | def format_constructors(constructors: List) -> str:
419 | """Format constructor information for AI consumption"""
420 | if not constructors:
421 | return "No constructors found"
422 |
423 | result = []
424 | for constructor in constructors:
425 | name = constructor.find('h3')
426 | signature = constructor.find('pre')
427 | desc = constructor.find('p')
428 |
429 | if name:
430 | result.append(f"### {clean_text(name)}")
431 | if signature:
432 | result.append(f"```dart\n{clean_text(signature)}\n```")
433 | if desc:
434 | result.append(clean_text(desc))
435 | result.append("")
436 |
437 | return "\n".join(result)
438 |
439 |
440 | def format_properties(properties: List) -> str:
441 | """Format property information"""
442 | if not properties:
443 | return "No properties found"
444 |
445 | result = []
446 | for prop_list in properties:
447 | items = prop_list.find_all('dt')
448 | for item in items:
449 | prop_name = clean_text(item)
450 | prop_desc = item.find_next_sibling('dd')
451 | if prop_name:
452 | result.append(f"- **{prop_name}**: {clean_text(prop_desc) if prop_desc else 'No description'}")
453 |
454 | return "\n".join(result)
455 |
456 |
457 | def format_methods(methods: List) -> str:
458 | """Format method information"""
459 | if not methods:
460 | return "No methods found"
461 |
462 | result = []
463 | for method in methods:
464 | name = method.find('h3')
465 | signature = method.find('pre')
466 | desc = method.find('p')
467 |
468 | if name:
469 | result.append(f"### {clean_text(name)}")
470 | if signature:
471 | result.append(f"```dart\n{clean_text(signature)}\n```")
472 | if desc:
473 | result.append(clean_text(desc))
474 | result.append("")
475 |
476 | return "\n".join(result)
477 |
478 |
479 | def extract_code_examples(soup: BeautifulSoup) -> str:
480 | """Extract code examples from documentation"""
481 | examples = soup.find_all('pre', class_='language-dart')
482 | if not examples:
483 | examples = soup.find_all('pre') # Fallback to any pre tags
484 |
485 | if not examples:
486 | return "No code examples found"
487 |
488 | result = []
489 | for i, example in enumerate(examples[:5]): # Limit to 5 examples
490 | code = clean_text(example)
491 | if code:
492 | result.append(f"#### Example {i+1}:\n```dart\n{code}\n```\n")
493 |
494 | return "\n".join(result)
495 |
496 |
497 | async def process_documentation(html: str, class_name: str, tokens: int = None) -> Dict[str, Any]:
498 | """Context7-style documentation processing pipeline with smart truncation and token counting.
499 |
500 | Returns a dict containing:
501 | - content: The processed markdown content
502 | - token_count: Final token count after any truncation
503 | - original_tokens: Original token count before truncation
504 | - truncated: Boolean indicating if content was truncated
505 | - truncation_note: Optional note about truncation
506 | """
507 | soup = BeautifulSoup(html, 'html.parser')
508 |
509 | # Remove navigation, scripts, styles, etc.
510 | for element in soup.find_all(['script', 'style', 'nav', 'header', 'footer']):
511 | element.decompose()
512 |
513 | # 1. Parse - Extract key sections
514 | description = soup.find('section', class_='desc')
515 | constructors = soup.find_all('section', class_='constructor')
516 | properties = soup.find_all('dl', class_='properties')
517 | methods = soup.find_all('section', class_='method')
518 |
519 | # 2. Enrich - Format for AI consumption
520 | markdown = f"""# {class_name}
521 |
522 | ## Description
523 | {clean_text(description) if description else 'No description available'}
524 |
525 | ## Constructors
526 | {format_constructors(constructors)}
527 |
528 | ## Properties
529 | {format_properties(properties)}
530 |
531 | ## Methods
532 | {format_methods(methods)}
533 |
534 | ## Code Examples
535 | {extract_code_examples(soup)}
536 | """
537 |
538 | # Count tokens before truncation
539 | original_tokens = token_manager.count_tokens(markdown)
540 | truncated = False
541 | truncation_note = None
542 |
543 | # 3. Truncate if needed
544 | if tokens and original_tokens > tokens:
545 | markdown = truncate_flutter_docs(
546 | markdown,
547 | class_name,
548 | max_tokens=tokens,
549 | strategy="balanced"
550 | )
551 | truncated = True
552 | truncation_note = f"Documentation truncated from {original_tokens} to approximately {tokens} tokens"
553 |
554 | # Count final tokens
555 | final_tokens = token_manager.count_tokens(markdown)
556 |
557 | return {
558 | "content": markdown,
559 | "token_count": final_tokens,
560 | "original_tokens": original_tokens if truncated else final_tokens,
561 | "truncated": truncated,
562 | "truncation_note": truncation_note
563 | }
564 |
565 |
566 | def resolve_flutter_url(query: str) -> Optional[str]:
567 | """Intelligently resolve documentation URLs from queries"""
568 | # Common Flutter class patterns
569 | patterns = {
570 | r"^(\w+)$": "https://api.flutter.dev/flutter/widgets/{0}-class.html",
571 | r"^widgets\.(\w+)$": "https://api.flutter.dev/flutter/widgets/{0}-class.html",
572 | r"^material\.(\w+)$": "https://api.flutter.dev/flutter/material/{0}-class.html",
573 | r"^cupertino\.(\w+)$": "https://api.flutter.dev/flutter/cupertino/{0}-class.html",
574 | r"^painting\.(\w+)$": "https://api.flutter.dev/flutter/painting/{0}-class.html",
575 | r"^animation\.(\w+)$": "https://api.flutter.dev/flutter/animation/{0}-class.html",
576 | r"^rendering\.(\w+)$": "https://api.flutter.dev/flutter/rendering/{0}-class.html",
577 | r"^services\.(\w+)$": "https://api.flutter.dev/flutter/services/{0}-class.html",
578 | r"^gestures\.(\w+)$": "https://api.flutter.dev/flutter/gestures/{0}-class.html",
579 | r"^foundation\.(\w+)$": "https://api.flutter.dev/flutter/foundation/{0}-class.html",
580 | # Dart core libraries
581 | r"^dart:core\.(\w+)$": "https://api.dart.dev/stable/dart-core/{0}-class.html",
582 | r"^dart:async\.(\w+)$": "https://api.dart.dev/stable/dart-async/{0}-class.html",
583 | r"^dart:collection\.(\w+)$": "https://api.dart.dev/stable/dart-collection/{0}-class.html",
584 | r"^dart:convert\.(\w+)$": "https://api.dart.dev/stable/dart-convert/{0}-class.html",
585 | r"^dart:io\.(\w+)$": "https://api.dart.dev/stable/dart-io/{0}-class.html",
586 | r"^dart:math\.(\w+)$": "https://api.dart.dev/stable/dart-math/{0}-class.html",
587 | r"^dart:typed_data\.(\w+)$": "https://api.dart.dev/stable/dart-typed_data/{0}-class.html",
588 | r"^dart:ui\.(\w+)$": "https://api.dart.dev/stable/dart-ui/{0}-class.html",
589 | }
590 |
591 | for pattern, url_template in patterns.items():
592 | if match := re.match(pattern, query, re.IGNORECASE):
593 | return url_template.format(*match.groups())
594 |
595 | return None
596 |
597 |
598 |
599 |
600 | @mcp.tool()
601 | async def get_flutter_docs(
602 | class_name: str,
603 | library: str = "widgets",
604 | tokens: int = 8000
605 | ) -> Dict[str, Any]:
606 | """
607 | Get Flutter class documentation on-demand with optional smart truncation.
608 |
609 | **DEPRECATED**: This tool is deprecated. Please use flutter_docs() instead.
610 | The new tool provides better query resolution and unified interface.
611 |
612 | Args:
613 | class_name: Name of the Flutter class (e.g., "Container", "Scaffold")
614 | library: Flutter library (e.g., "widgets", "material", "cupertino")
615 | tokens: Maximum token limit for truncation (default: 8000, min: 500)
616 |
617 | Returns:
618 | Dictionary with documentation content or error message
619 | """
620 | bind_contextvars(tool="get_flutter_docs", class_name=class_name, library=library)
621 | logger.warning("deprecated_tool_usage", tool="get_flutter_docs", replacement="flutter_docs")
622 |
623 | # Validate tokens parameter
624 | if tokens < 500:
625 | return {"error": "tokens parameter must be at least 500"}
626 |
627 | # Call the new flutter_docs tool
628 | identifier = f"{library}.{class_name}" if library != "widgets" else class_name
629 | result = await flutter_docs(identifier, max_tokens=tokens)
630 |
631 | # Transform back to old format
632 | if result.get("error"):
633 | return {
634 | "error": result["error"],
635 | "suggestion": result.get("suggestion", "")
636 | }
637 | else:
638 | return {
639 | "source": result.get("source", "live"),
640 | "class": result.get("class", class_name),
641 | "library": result.get("library", library),
642 | "content": result.get("content", ""),
643 | "fetched_at": datetime.utcnow().isoformat(),
644 | "truncated": result.get("truncated", False)
645 | }
646 |
647 |
648 | async def _get_flutter_docs_impl(
649 | class_name: str,
650 | library: str = "widgets",
651 | tokens: int = None
652 | ) -> Dict[str, Any]:
653 | """
654 | Internal implementation of get_flutter_docs functionality.
655 | """
656 | # Check cache first
657 | cache_key = get_cache_key("flutter_api", f"{library}:{class_name}")
658 |
659 | # Check cache
660 | cached_data = cache_manager.get(cache_key)
661 | if cached_data:
662 | logger.info("cache_hit")
663 | return cached_data
664 |
665 | # Rate-limited fetch from Flutter docs
666 | await rate_limiter.acquire()
667 |
668 | # Determine URL based on library type
669 | if library.startswith("dart:"):
670 | # Convert dart:core to dart-core format for Dart API
671 | dart_lib = library.replace("dart:", "dart-")
672 | url = f"https://api.dart.dev/stable/{dart_lib}/{class_name}-class.html"
673 | else:
674 | # Flutter libraries use api.flutter.dev
675 | url = f"https://api.flutter.dev/flutter/{library}/{class_name}-class.html"
676 |
677 | logger.info("fetching_docs", url=url)
678 |
679 | try:
680 | async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
681 | response = await client.get(
682 | url,
683 | headers={
684 | "User-Agent": "Flutter-MCP-Docs/1.0 (github.com/flutter-mcp/flutter-mcp)"
685 | }
686 | )
687 | response.raise_for_status()
688 |
689 | # Process HTML - Context7 style pipeline with truncation
690 | doc_result = await process_documentation(response.text, class_name, tokens)
691 |
692 | # Cache the result with token metadata
693 | result = {
694 | "source": "live",
695 | "class": class_name,
696 | "library": library,
697 | "content": doc_result["content"],
698 | "fetched_at": datetime.utcnow().isoformat(),
699 | "truncated": doc_result["truncated"],
700 | "token_count": doc_result["token_count"],
701 | "original_tokens": doc_result["original_tokens"],
702 | "truncation_note": doc_result["truncation_note"]
703 | }
704 | cache_manager.set(cache_key, result, CACHE_DURATIONS["flutter_api"], token_count=doc_result["token_count"])
705 |
706 | logger.info("docs_fetched_success",
707 | content_length=len(doc_result["content"]),
708 | token_count=doc_result["token_count"],
709 | truncated=doc_result["truncated"])
710 | return result
711 |
712 | except httpx.HTTPStatusError as e:
713 | logger.error("http_error", status_code=e.response.status_code)
714 | return {
715 | "error": f"HTTP {e.response.status_code}: Documentation not found for {library}.{class_name}",
716 | "suggestion": "Check the class name and library. Common libraries: widgets, material, cupertino"
717 | }
718 | except Exception as e:
719 | logger.error("fetch_error", error=str(e))
720 | return {
721 | "error": f"Failed to fetch documentation: {str(e)}",
722 | "url": url
723 | }
724 |
725 |
726 | @mcp.tool()
727 | async def search_flutter_docs(query: str, tokens: int = 5000) -> Dict[str, Any]:
728 | """
729 | Search across Flutter/Dart documentation sources with fuzzy matching.
730 |
731 | **DEPRECATED**: This tool is deprecated. Please use flutter_search() instead.
732 | The new tool provides better filtering and more structured results.
733 |
734 | Searches Flutter API docs, Dart API docs, and pub.dev packages.
735 | Returns top 5-10 most relevant results with brief descriptions.
736 |
737 | Args:
738 | query: Search query (e.g., "state management", "Container", "navigation", "http requests")
739 | tokens: Maximum token limit for response (default: 5000, min: 500)
740 |
741 | Returns:
742 | Search results with relevance scores and brief descriptions
743 | """
744 | bind_contextvars(tool="search_flutter_docs", query=query)
745 | logger.warning("deprecated_tool_usage", tool="search_flutter_docs", replacement="flutter_search")
746 |
747 | # Validate tokens parameter
748 | if tokens < 500:
749 | return {"error": "tokens parameter must be at least 500"}
750 |
751 | # Call new flutter_search tool
752 | result = await flutter_search(query, limit=10)
753 |
754 | # Transform back to old format
755 | return {
756 | "query": result["query"],
757 | "results": result["results"],
758 | "total": result.get("total_results", result.get("returned_results", 0)),
759 | "timestamp": result.get("timestamp", datetime.utcnow().isoformat()),
760 | "suggestions": result.get("suggestions", [])
761 | }
762 |
763 |
764 | async def _search_flutter_docs_impl(
765 | query: str,
766 | limit: int = 10,
767 | types: List[str] = None
768 | ) -> Dict[str, Any]:
769 | """
770 | Internal implementation of search functionality.
771 | """
772 | logger.info("searching_docs")
773 |
774 | results = []
775 | query_lower = query.lower()
776 |
777 | # Check cache for search results
778 | cache_key = get_cache_key("search_results", query_lower)
779 | cached_data = cache_manager.get(cache_key)
780 | if cached_data:
781 | logger.info("search_cache_hit")
782 | return cached_data
783 |
784 | # 1. Try direct URL resolution first (exact matches)
785 | if url := resolve_flutter_url(query):
786 | logger.info("url_resolved", url=url)
787 |
788 | # Extract class name and library from URL
789 | if "flutter/widgets" in url:
790 | library = "widgets"
791 | elif "flutter/material" in url:
792 | library = "material"
793 | elif "flutter/cupertino" in url:
794 | library = "cupertino"
795 | else:
796 | library = "unknown"
797 |
798 | class_match = re.search(r'/([^/]+)-class\.html$', url)
799 | if class_match:
800 | class_name = class_match.group(1)
801 | doc = await _get_flutter_docs_impl(class_name, library)
802 | if "error" not in doc:
803 | results.append({
804 | "type": "flutter_class",
805 | "relevance": 1.0,
806 | "title": f"{class_name} ({library})",
807 | "description": f"Flutter {library} widget/class",
808 | "url": url,
809 | "content_preview": doc.get("content", "")[:200] + "..."
810 | })
811 |
812 | # 2. Check common Flutter widgets and classes
813 | common_flutter_items = [
814 | # State management related
815 | ("StatefulWidget", "widgets", "Base class for widgets that have mutable state"),
816 | ("StatelessWidget", "widgets", "Base class for widgets that don't require mutable state"),
817 | ("State", "widgets", "Logic and internal state for a StatefulWidget"),
818 | ("InheritedWidget", "widgets", "Base class for widgets that propagate information down the tree"),
819 | ("Provider", "widgets", "A widget that provides a value to its descendants"),
820 | ("ValueListenableBuilder", "widgets", "Rebuilds when ValueListenable changes"),
821 | ("NotificationListener", "widgets", "Listens for Notifications bubbling up"),
822 |
823 | # Layout widgets
824 | ("Container", "widgets", "A convenience widget that combines common painting, positioning, and sizing"),
825 | ("Row", "widgets", "Displays children in a horizontal array"),
826 | ("Column", "widgets", "Displays children in a vertical array"),
827 | ("Stack", "widgets", "Positions children relative to the box edges"),
828 | ("Scaffold", "material", "Basic material design visual layout structure"),
829 | ("Expanded", "widgets", "Expands a child to fill available space in Row/Column"),
830 | ("Flexible", "widgets", "Controls how a child flexes in Row/Column"),
831 | ("Wrap", "widgets", "Displays children in multiple runs"),
832 | ("Flow", "widgets", "Positions children using transformation matrices"),
833 | ("Table", "widgets", "Displays children in a table layout"),
834 | ("Align", "widgets", "Aligns a child within itself"),
835 | ("Center", "widgets", "Centers a child within itself"),
836 | ("Positioned", "widgets", "Positions a child in a Stack"),
837 | ("FittedBox", "widgets", "Scales and positions child within itself"),
838 | ("AspectRatio", "widgets", "Constrains child to specific aspect ratio"),
839 | ("ConstrainedBox", "widgets", "Imposes additional constraints on child"),
840 | ("SizedBox", "widgets", "Box with a specified size"),
841 | ("FractionallySizedBox", "widgets", "Sizes child to fraction of total space"),
842 | ("LimitedBox", "widgets", "Limits child size when unconstrained"),
843 | ("Offstage", "widgets", "Lays out child as if visible but paints nothing"),
844 | ("LayoutBuilder", "widgets", "Builds widget tree based on parent constraints"),
845 |
846 | # Navigation
847 | ("Navigator", "widgets", "Manages a stack of Route objects"),
848 | ("Route", "widgets", "An abstraction for an entry managed by a Navigator"),
849 | ("MaterialPageRoute", "material", "A modal route that replaces the entire screen"),
850 | ("NavigationBar", "material", "Material 3 navigation bar"),
851 | ("NavigationRail", "material", "Material navigation rail"),
852 | ("BottomNavigationBar", "material", "Bottom navigation bar"),
853 | ("Drawer", "material", "Material design drawer"),
854 | ("TabBar", "material", "Material design tabs"),
855 | ("TabBarView", "material", "Page view for TabBar"),
856 | ("WillPopScope", "widgets", "Intercepts back button press"),
857 | ("BackButton", "material", "Material design back button"),
858 |
859 | # Input widgets
860 | ("TextField", "material", "A material design text field"),
861 | ("TextFormField", "material", "A FormField that contains a TextField"),
862 | ("Form", "widgets", "Container for form fields"),
863 | ("GestureDetector", "widgets", "Detects gestures on widgets"),
864 | ("InkWell", "material", "Rectangular area that responds to touch with ripple"),
865 | ("Dismissible", "widgets", "Can be dismissed by dragging"),
866 | ("Draggable", "widgets", "Can be dragged to DragTarget"),
867 | ("LongPressDraggable", "widgets", "Draggable triggered by long press"),
868 | ("DragTarget", "widgets", "Receives data from Draggable"),
869 | ("DropdownButton", "material", "Material design dropdown button"),
870 | ("Slider", "material", "Material design slider"),
871 | ("Switch", "material", "Material design switch"),
872 | ("Checkbox", "material", "Material design checkbox"),
873 | ("Radio", "material", "Material design radio button"),
874 | ("DatePicker", "material", "Material design date picker"),
875 | ("TimePicker", "material", "Material design time picker"),
876 |
877 | # Lists & Grids
878 | ("ListView", "widgets", "Scrollable list of widgets"),
879 | ("GridView", "widgets", "Scrollable 2D array of widgets"),
880 | ("CustomScrollView", "widgets", "ScrollView with slivers"),
881 | ("SingleChildScrollView", "widgets", "Box with single scrollable child"),
882 | ("PageView", "widgets", "Scrollable list that works page by page"),
883 | ("ReorderableListView", "material", "List where items can be reordered"),
884 | ("RefreshIndicator", "material", "Material design pull-to-refresh"),
885 |
886 | # Common material widgets
887 | ("AppBar", "material", "A material design app bar"),
888 | ("Card", "material", "A material design card"),
889 | ("ListTile", "material", "A single fixed-height row for lists"),
890 | ("IconButton", "material", "A material design icon button"),
891 | ("ElevatedButton", "material", "A material design elevated button"),
892 | ("FloatingActionButton", "material", "A material design floating action button"),
893 | ("Chip", "material", "Material design chip"),
894 | ("ChoiceChip", "material", "Material design choice chip"),
895 | ("FilterChip", "material", "Material design filter chip"),
896 | ("ActionChip", "material", "Material design action chip"),
897 | ("CircularProgressIndicator", "material", "Material circular progress"),
898 | ("LinearProgressIndicator", "material", "Material linear progress"),
899 | ("SnackBar", "material", "Material design snackbar"),
900 | ("BottomSheet", "material", "Material design bottom sheet"),
901 | ("ExpansionPanel", "material", "Material expansion panel"),
902 | ("Stepper", "material", "Material design stepper"),
903 | ("DataTable", "material", "Material design data table"),
904 |
905 | # Visual Effects
906 | ("Opacity", "widgets", "Makes child partially transparent"),
907 | ("Transform", "widgets", "Applies transformation before painting"),
908 | ("RotatedBox", "widgets", "Rotates child by integral quarters"),
909 | ("ClipRect", "widgets", "Clips child to rectangle"),
910 | ("ClipRRect", "widgets", "Clips child to rounded rectangle"),
911 | ("ClipOval", "widgets", "Clips child to oval"),
912 | ("ClipPath", "widgets", "Clips child to path"),
913 | ("DecoratedBox", "widgets", "Paints decoration around child"),
914 | ("BackdropFilter", "widgets", "Applies filter to existing painted content"),
915 |
916 | # Animation
917 | ("AnimatedBuilder", "widgets", "A widget that rebuilds when animation changes"),
918 | ("AnimationController", "animation", "Controls an animation"),
919 | ("Hero", "widgets", "Marks a child for hero animations"),
920 | ("AnimatedContainer", "widgets", "Animated version of Container"),
921 | ("AnimatedOpacity", "widgets", "Animated version of Opacity"),
922 | ("AnimatedPositioned", "widgets", "Animated version of Positioned"),
923 | ("AnimatedDefaultTextStyle", "widgets", "Animated version of DefaultTextStyle"),
924 | ("AnimatedAlign", "widgets", "Animated version of Align"),
925 | ("AnimatedPadding", "widgets", "Animated version of Padding"),
926 | ("AnimatedSize", "widgets", "Animates its size to match child"),
927 | ("AnimatedCrossFade", "widgets", "Cross-fades between two children"),
928 | ("AnimatedSwitcher", "widgets", "Animates when switching between children"),
929 |
930 | # Async widgets
931 | ("FutureBuilder", "widgets", "Builds based on interaction with a Future"),
932 | ("StreamBuilder", "widgets", "Builds based on interaction with a Stream"),
933 |
934 | # Utility widgets
935 | ("MediaQuery", "widgets", "Establishes media query subtree"),
936 | ("Theme", "material", "Applies theme to descendant widgets"),
937 | ("DefaultTextStyle", "widgets", "Default text style for descendants"),
938 | ("Semantics", "widgets", "Annotates widget tree with semantic descriptions"),
939 | ("MergeSemantics", "widgets", "Merges semantics of descendants"),
940 | ("ExcludeSemantics", "widgets", "Drops semantics of descendants"),
941 | ]
942 |
943 | # Score Flutter items based on query match
944 | for class_name, library, description in common_flutter_items:
945 | relevance = calculate_relevance(query_lower, class_name.lower(), description.lower())
946 | if relevance > 0.3: # Threshold for inclusion
947 | results.append({
948 | "type": "flutter_class",
949 | "relevance": relevance,
950 | "title": f"{class_name} ({library})",
951 | "description": description,
952 | "class_name": class_name,
953 | "library": library
954 | })
955 |
956 | # 3. Check common Dart core classes
957 | common_dart_items = [
958 | ("List", "dart:core", "An indexable collection of objects with a length"),
959 | ("Map", "dart:core", "A collection of key/value pairs"),
960 | ("Set", "dart:core", "A collection of objects with no duplicate elements"),
961 | ("String", "dart:core", "A sequence of UTF-16 code units"),
962 | ("Future", "dart:async", "Represents a computation that completes with a value or error"),
963 | ("Stream", "dart:async", "A source of asynchronous data events"),
964 | ("Duration", "dart:core", "A span of time"),
965 | ("DateTime", "dart:core", "An instant in time"),
966 | ("RegExp", "dart:core", "A regular expression pattern"),
967 | ("Iterable", "dart:core", "A collection of values that can be accessed sequentially"),
968 | ]
969 |
970 | for class_name, library, description in common_dart_items:
971 | relevance = calculate_relevance(query_lower, class_name.lower(), description.lower())
972 | if relevance > 0.3:
973 | results.append({
974 | "type": "dart_class",
975 | "relevance": relevance,
976 | "title": f"{class_name} ({library})",
977 | "description": description,
978 | "class_name": class_name,
979 | "library": library
980 | })
981 |
982 | # 4. Search popular pub.dev packages
983 | popular_packages = [
984 | # State Management
985 | ("provider", "State management library that makes it easy to connect business logic to widgets"),
986 | ("riverpod", "A reactive caching and data-binding framework"),
987 | ("bloc", "State management library implementing the BLoC design pattern"),
988 | ("get", "Open source state management, navigation and utilities"),
989 | ("mobx", "Reactive state management library"),
990 | ("redux", "Predictable state container"),
991 | ("stacked", "MVVM architecture solution"),
992 | ("get_it", "Service locator for dependency injection"),
993 |
994 | # Networking
995 | ("dio", "Powerful HTTP client for Dart with interceptors and FormData"),
996 | ("http", "A composable, multi-platform, Future-based API for HTTP requests"),
997 | ("retrofit", "Type-safe HTTP client generator"),
998 | ("chopper", "HTTP client with built-in JsonConverter"),
999 | ("graphql_flutter", "GraphQL client for Flutter"),
1000 | ("socket_io_client", "Socket.IO client"),
1001 | ("web_socket_channel", "WebSocket connections"),
1002 |
1003 | # Storage & Database
1004 | ("shared_preferences", "Flutter plugin for reading and writing simple key-value pairs"),
1005 | ("sqflite", "SQLite plugin for Flutter with support for iOS, Android and MacOS"),
1006 | ("hive", "Lightweight and blazing fast key-value database written in pure Dart"),
1007 | ("isar", "Fast cross-platform database"),
1008 | ("objectbox", "High-performance NoSQL database"),
1009 | ("drift", "Reactive persistence library"),
1010 | ("floor", "SQLite abstraction with Room-like API"),
1011 |
1012 | # Firebase
1013 | ("firebase_core", "Flutter plugin to use Firebase Core API"),
1014 | ("firebase_auth", "Flutter plugin for Firebase Auth"),
1015 | ("firebase_database", "Flutter plugin for Firebase Realtime Database"),
1016 | ("cloud_firestore", "Flutter plugin for Cloud Firestore"),
1017 | ("firebase_messaging", "Push notifications via FCM"),
1018 | ("firebase_storage", "Flutter plugin for Firebase Cloud Storage"),
1019 | ("firebase_analytics", "Flutter plugin for Google Analytics for Firebase"),
1020 |
1021 | # UI/UX Libraries
1022 | ("flutter_bloc", "Flutter widgets that make it easy to implement BLoC design pattern"),
1023 | ("animations", "Beautiful pre-built animations for Flutter"),
1024 | ("flutter_svg", "SVG rendering and widget library for Flutter"),
1025 | ("cached_network_image", "Flutter library to load and cache network images"),
1026 | ("flutter_slidable", "Slidable list item actions"),
1027 | ("shimmer", "Shimmer loading effect"),
1028 | ("liquid_swipe", "Liquid swipe page transitions"),
1029 | ("flutter_staggered_grid_view", "Staggered grid layouts"),
1030 | ("carousel_slider", "Carousel widget"),
1031 | ("photo_view", "Zoomable image widget"),
1032 | ("flutter_spinkit", "Loading indicators collection"),
1033 | ("lottie", "Render After Effects animations"),
1034 | ("rive", "Interactive animations"),
1035 |
1036 | # Platform Integration
1037 | ("url_launcher", "Flutter plugin for launching URLs"),
1038 | ("path_provider", "Flutter plugin for getting commonly used locations on the filesystem"),
1039 | ("image_picker", "Flutter plugin for selecting images from image library or camera"),
1040 | ("connectivity_plus", "Flutter plugin for discovering network connectivity"),
1041 | ("permission_handler", "Permission plugin for Flutter"),
1042 | ("geolocator", "Flutter geolocation plugin for Android and iOS"),
1043 | ("google_fonts", "Flutter package to use fonts from fonts.google.com"),
1044 | ("flutter_local_notifications", "Local notifications"),
1045 | ("share_plus", "Share content to other apps"),
1046 | ("file_picker", "Native file picker"),
1047 | ("open_file", "Open files with default apps"),
1048 |
1049 | # Navigation
1050 | ("go_router", "A declarative routing package for Flutter"),
1051 | ("auto_route", "Code generation for type-safe route navigation"),
1052 | ("beamer", "Handle your application routing"),
1053 | ("fluro", "Flutter routing library"),
1054 |
1055 | # Developer Tools
1056 | ("logger", "Beautiful logging utility"),
1057 | ("pretty_dio_logger", "Dio interceptor for logging"),
1058 | ("flutter_dotenv", "Load environment variables"),
1059 | ("device_info_plus", "Device information"),
1060 | ("package_info_plus", "App package information"),
1061 | ("equatable", "Simplify equality comparisons"),
1062 | ("freezed", "Code generation for immutable classes"),
1063 | ("json_serializable", "Automatically generate code for JSON"),
1064 | ("build_runner", "Build system for Dart code generation"),
1065 | ]
1066 |
1067 | for package_name, description in popular_packages:
1068 | relevance = calculate_relevance(query_lower, package_name.lower(), description.lower())
1069 | if relevance > 0.3:
1070 | results.append({
1071 | "type": "pub_package",
1072 | "relevance": relevance,
1073 | "title": f"{package_name} (pub.dev)",
1074 | "description": description,
1075 | "package_name": package_name
1076 | })
1077 |
1078 | # 5. Concept-based search (for queries like "state management", "navigation", etc.)
1079 | concepts = {
1080 | "state management": [
1081 | ("setState", "The simplest way to manage state in Flutter"),
1082 | ("InheritedWidget", "Share data across the widget tree"),
1083 | ("provider", "Popular state management package"),
1084 | ("riverpod", "Improved provider with compile-time safety"),
1085 | ("bloc", "Business Logic Component pattern"),
1086 | ("get", "Lightweight state management solution"),
1087 | ("mobx", "Reactive state management"),
1088 | ("redux", "Predictable state container"),
1089 | ("ValueNotifier", "Simple observable pattern"),
1090 | ("ChangeNotifier", "Observable object for multiple listeners"),
1091 | ],
1092 | "navigation": [
1093 | ("Navigator", "Stack-based navigation in Flutter"),
1094 | ("go_router", "Declarative routing package"),
1095 | ("auto_route", "Code generation for routes"),
1096 | ("Named routes", "Navigation using route names"),
1097 | ("Deep linking", "Handle URLs in your app"),
1098 | ("WillPopScope", "Intercept back navigation"),
1099 | ("NavigatorObserver", "Observe navigation events"),
1100 | ("Hero animations", "Animate widgets between routes"),
1101 | ("Modal routes", "Full-screen modal pages"),
1102 | ("BottomSheet navigation", "Navigate with bottom sheets"),
1103 | ],
1104 | "http": [
1105 | ("http", "Official Dart HTTP package"),
1106 | ("dio", "Advanced HTTP client with interceptors"),
1107 | ("retrofit", "Type-safe HTTP client generator"),
1108 | ("chopper", "HTTP client with built-in JsonConverter"),
1109 | ("GraphQL", "Query language for APIs"),
1110 | ("REST API", "RESTful web services"),
1111 | ("WebSocket", "Real-time bidirectional communication"),
1112 | ("gRPC", "High performance RPC framework"),
1113 | ],
1114 | "database": [
1115 | ("sqflite", "SQLite for Flutter"),
1116 | ("hive", "NoSQL database for Flutter"),
1117 | ("drift", "Reactive persistence library"),
1118 | ("objectbox", "Fast NoSQL database"),
1119 | ("shared_preferences", "Simple key-value storage"),
1120 | ("isar", "Fast cross-platform database"),
1121 | ("floor", "SQLite abstraction"),
1122 | ("sembast", "NoSQL persistent store"),
1123 | ("Firebase Realtime Database", "Cloud-hosted NoSQL database"),
1124 | ("Cloud Firestore", "Scalable NoSQL cloud database"),
1125 | ],
1126 | "animation": [
1127 | ("AnimationController", "Control animations"),
1128 | ("AnimatedBuilder", "Build animations efficiently"),
1129 | ("Hero", "Shared element transitions"),
1130 | ("animations", "Pre-built animation package"),
1131 | ("rive", "Interactive animations"),
1132 | ("lottie", "After Effects animations"),
1133 | ("AnimatedContainer", "Implicit animations"),
1134 | ("TweenAnimationBuilder", "Simple custom animations"),
1135 | ("Curves", "Animation easing functions"),
1136 | ("Physics-based animations", "Spring and friction animations"),
1137 | ],
1138 | "architecture": [
1139 | ("BLoC Pattern", "Business Logic Component pattern for state management"),
1140 | ("MVVM", "Model-View-ViewModel architecture pattern"),
1141 | ("Clean Architecture", "Domain-driven design with clear separation"),
1142 | ("Repository Pattern", "Abstraction layer for data sources"),
1143 | ("Provider Pattern", "Dependency injection and state management"),
1144 | ("GetX Pattern", "Reactive state management with GetX"),
1145 | ("MVC", "Model-View-Controller pattern in Flutter"),
1146 | ("Redux", "Predictable state container pattern"),
1147 | ("Riverpod Architecture", "Modern reactive caching framework"),
1148 | ("Domain Driven Design", "DDD principles in Flutter"),
1149 | ("Hexagonal Architecture", "Ports and adapters pattern"),
1150 | ("Feature-based structure", "Organize code by features"),
1151 | ],
1152 | "testing": [
1153 | ("Widget Testing", "Testing Flutter widgets in isolation"),
1154 | ("Integration Testing", "End-to-end testing of Flutter apps"),
1155 | ("Unit Testing", "Testing Dart code logic"),
1156 | ("Golden Testing", "Visual regression testing"),
1157 | ("Mockito", "Mocking framework for Dart"),
1158 | ("flutter_test", "Flutter testing framework"),
1159 | ("test", "Dart testing package"),
1160 | ("integration_test", "Flutter integration testing"),
1161 | ("mocktail", "Type-safe mocking library"),
1162 | ("Test Coverage", "Measuring test completeness"),
1163 | ("TDD", "Test-driven development"),
1164 | ("BDD", "Behavior-driven development"),
1165 | ],
1166 | "performance": [
1167 | ("Performance Profiling", "Analyzing app performance"),
1168 | ("Widget Inspector", "Debugging widget trees"),
1169 | ("Timeline View", "Performance timeline analysis"),
1170 | ("Memory Profiling", "Analyzing memory usage"),
1171 | ("Shader Compilation", "Reducing shader jank"),
1172 | ("Build Optimization", "Optimizing build methods"),
1173 | ("Lazy Loading", "Loading content on demand"),
1174 | ("Image Caching", "Efficient image loading"),
1175 | ("Code Splitting", "Reducing initial bundle size"),
1176 | ("Tree Shaking", "Removing unused code"),
1177 | ("Const Constructors", "Compile-time optimizations"),
1178 | ("RepaintBoundary", "Isolate expensive paints"),
1179 | ],
1180 | "platform": [
1181 | ("Platform Channels", "Communication with native code"),
1182 | ("Method Channel", "Invoking platform-specific APIs"),
1183 | ("Event Channel", "Streaming data from platform"),
1184 | ("iOS Integration", "Flutter iOS-specific features"),
1185 | ("Android Integration", "Flutter Android-specific features"),
1186 | ("Web Support", "Flutter web-specific features"),
1187 | ("Desktop Support", "Flutter desktop applications"),
1188 | ("Embedding Flutter", "Adding Flutter to existing apps"),
1189 | ("Platform Views", "Embedding native views"),
1190 | ("FFI", "Foreign Function Interface"),
1191 | ("Plugin Development", "Creating Flutter plugins"),
1192 | ("Platform-specific UI", "Adaptive UI patterns"),
1193 | ],
1194 | "debugging": [
1195 | ("Flutter Inspector", "Visual debugging tool"),
1196 | ("Logging", "Debug logging in Flutter"),
1197 | ("Breakpoints", "Using breakpoints in Flutter"),
1198 | ("DevTools", "Flutter DevTools suite"),
1199 | ("Error Handling", "Handling errors in Flutter"),
1200 | ("Crash Reporting", "Capturing and reporting crashes"),
1201 | ("Debug Mode", "Flutter debug mode features"),
1202 | ("Assert Statements", "Debug-only checks"),
1203 | ("Stack Traces", "Understanding error traces"),
1204 | ("Network Debugging", "Inspecting network requests"),
1205 | ("Layout Explorer", "Visualize layout constraints"),
1206 | ("Performance Overlay", "On-device performance metrics"),
1207 | ],
1208 | "forms": [
1209 | ("Form", "Container for form fields"),
1210 | ("TextFormField", "Text input with validation"),
1211 | ("FormField", "Base class for form fields"),
1212 | ("Form Validation", "Validating user input"),
1213 | ("Input Decoration", "Styling form fields"),
1214 | ("Focus Management", "Managing input focus"),
1215 | ("Keyboard Actions", "Custom keyboard actions"),
1216 | ("Input Formatters", "Format input as typed"),
1217 | ("Form State", "Managing form state"),
1218 | ("Custom Form Fields", "Creating custom inputs"),
1219 | ],
1220 | "theming": [
1221 | ("ThemeData", "Application theme configuration"),
1222 | ("Material Theme", "Material Design theming"),
1223 | ("Dark Mode", "Supporting dark theme"),
1224 | ("Custom Themes", "Creating custom themes"),
1225 | ("Theme Extensions", "Extending theme data"),
1226 | ("Color Schemes", "Material 3 color system"),
1227 | ("Typography", "Text theming"),
1228 | ("Dynamic Theming", "Runtime theme changes"),
1229 | ("Platform Theming", "Platform-specific themes"),
1230 | ],
1231 | }
1232 |
1233 | # Check if query matches any concept
1234 | for concept, items in concepts.items():
1235 | if concept in query_lower or any(word in concept for word in query_lower.split()):
1236 | for item_name, item_desc in items:
1237 | results.append({
1238 | "type": "concept",
1239 | "relevance": 0.8,
1240 | "title": item_name,
1241 | "description": item_desc,
1242 | "concept": concept
1243 | })
1244 |
1245 | # Apply type filtering if specified
1246 | if types:
1247 | filtered_results = []
1248 | for result in results:
1249 | result_type = result.get("type", "")
1250 | # Map result types to filter types
1251 | if "flutter" in types and result_type == "flutter_class":
1252 | filtered_results.append(result)
1253 | elif "dart" in types and result_type == "dart_class":
1254 | filtered_results.append(result)
1255 | elif "package" in types and result_type == "pub_package":
1256 | filtered_results.append(result)
1257 | elif "concept" in types and result_type == "concept":
1258 | filtered_results.append(result)
1259 | results = filtered_results
1260 |
1261 | # Sort results by relevance
1262 | results.sort(key=lambda x: x["relevance"], reverse=True)
1263 |
1264 | # Apply limit
1265 | results = results[:limit]
1266 |
1267 | # Fetch actual documentation for top results if needed
1268 | enriched_results = []
1269 | for result in results:
1270 | if result["type"] == "flutter_class" and "class_name" in result:
1271 | # Only fetch full docs for top 3 Flutter classes
1272 | if len(enriched_results) < 3:
1273 | try:
1274 | doc = await _get_flutter_docs_impl(result["class_name"], result["library"])
1275 | if not doc.get("error"):
1276 | result["documentation_available"] = True
1277 | result["content_preview"] = doc.get("content", "")[:300] + "..."
1278 | else:
1279 | result["documentation_available"] = False
1280 | result["error_info"] = doc.get("error_type", "unknown")
1281 | except Exception as e:
1282 | logger.warning("search_enrichment_error", error=str(e), class_name=result.get("class_name"))
1283 | result["documentation_available"] = False
1284 | result["error_info"] = "enrichment_failed"
1285 | elif result["type"] == "pub_package" and "package_name" in result:
1286 | # Add pub.dev URL
1287 | result["url"] = f"https://pub.dev/packages/{result['package_name']}"
1288 | result["documentation_available"] = True
1289 |
1290 | enriched_results.append(result)
1291 |
1292 | # Prepare final response
1293 | response = {
1294 | "query": query,
1295 | "results": enriched_results,
1296 | "total": len(enriched_results),
1297 | "timestamp": datetime.utcnow().isoformat(),
1298 | "suggestions": generate_search_suggestions(query_lower, enriched_results)
1299 | }
1300 |
1301 | # Cache the search results for 1 hour
1302 | cache_manager.set(cache_key, response, 3600)
1303 |
1304 | return response
1305 |
1306 |
1307 | def calculate_relevance(query: str, title: str, description: str) -> float:
1308 | """Calculate relevance score based on fuzzy matching."""
1309 | score = 0.0
1310 |
1311 | # Exact match in title
1312 | if query == title:
1313 | score += 1.0
1314 | # Partial match in title
1315 | elif query in title:
1316 | score += 0.8
1317 | # Word match in title
1318 | elif any(word in title for word in query.split()):
1319 | score += 0.6
1320 |
1321 | # Match in description
1322 | if query in description:
1323 | score += 0.4
1324 | elif any(word in description for word in query.split() if len(word) > 3):
1325 | score += 0.2
1326 |
1327 | # Fuzzy match using character overlap
1328 | title_overlap = len(set(query) & set(title)) / len(set(query) | set(title)) if title else 0
1329 | desc_overlap = len(set(query) & set(description)) / len(set(query) | set(description)) if description else 0
1330 | score += (title_overlap * 0.3 + desc_overlap * 0.1)
1331 |
1332 | return min(score, 1.0)
1333 |
1334 |
1335 | def generate_search_suggestions(query: str, results: List[Dict]) -> List[str]:
1336 | """Generate helpful search suggestions based on query and results."""
1337 | suggestions = []
1338 |
1339 | if not results:
1340 | suggestions.append(f"Try searching for specific widget names like 'Container' or 'Scaffold'")
1341 | suggestions.append(f"Use package names from pub.dev like 'provider' or 'dio'")
1342 | suggestions.append(f"Search for concepts like 'state management' or 'navigation'")
1343 | elif len(results) < 3:
1344 | suggestions.append(f"For more results, try broader terms or related concepts")
1345 | if any(r["type"] == "flutter_class" for r in results):
1346 | suggestions.append(f"You can also search for specific libraries like 'material.AppBar'")
1347 |
1348 | return suggestions
1349 |
1350 |
1351 | @mcp.tool()
1352 | async def flutter_docs(
1353 | identifier: str,
1354 | topic: Optional[str] = None,
1355 | tokens: int = 10000
1356 | ) -> Dict[str, Any]:
1357 | """
1358 | Unified tool to get Flutter/Dart documentation with smart identifier resolution.
1359 |
1360 | Automatically detects the type of identifier and fetches appropriate documentation.
1361 | Supports Flutter classes, Dart classes, and pub.dev packages.
1362 |
1363 | Args:
1364 | identifier: The identifier to look up. Examples:
1365 | - "Container" (Flutter widget)
1366 | - "material.AppBar" (library-qualified Flutter class)
1367 | - "dart:async.Future" (Dart API)
1368 | - "provider" (pub.dev package)
1369 | - "pub:dio" (explicit pub.dev package)
1370 | - "flutter:Container" (explicit Flutter class)
1371 | topic: Optional topic filter. For classes: "constructors", "methods",
1372 | "properties", "examples". For packages: "getting-started",
1373 | "examples", "api", "installation"
1374 | tokens: Maximum tokens for response (default: 10000, min: 1000)
1375 |
1376 | Returns:
1377 | Dictionary with documentation content, type, and metadata
1378 | """
1379 | bind_contextvars(tool="flutter_docs", identifier=identifier, topic=topic)
1380 | logger.info("resolving_identifier", identifier=identifier)
1381 |
1382 | # Validate tokens parameter
1383 | if tokens < 1000:
1384 | return {"error": "tokens parameter must be at least 1000"}
1385 |
1386 | # Parse identifier to determine type
1387 | identifier_lower = identifier.lower()
1388 | doc_type = None
1389 | library = None
1390 | class_name = None
1391 | package_name = None
1392 |
1393 | # Check for explicit prefixes
1394 | if identifier.startswith("pub:"):
1395 | doc_type = "pub_package"
1396 | package_name = identifier[4:]
1397 | elif identifier.startswith("flutter:"):
1398 | doc_type = "flutter_class"
1399 | class_name = identifier[8:]
1400 | library = "widgets" # Default to widgets
1401 | elif identifier.startswith("dart:"):
1402 | doc_type = "dart_class"
1403 | # Parse dart:library.Class format
1404 | parts = identifier.split(".")
1405 | if len(parts) == 2:
1406 | library = parts[0]
1407 | class_name = parts[1]
1408 | else:
1409 | class_name = identifier[5:]
1410 | library = "dart:core"
1411 | elif "." in identifier:
1412 | # Library-qualified name (e.g., material.AppBar)
1413 | parts = identifier.split(".", 1)
1414 | library = parts[0]
1415 | class_name = parts[1]
1416 |
1417 | if library.startswith("dart:"):
1418 | doc_type = "dart_class"
1419 | else:
1420 | doc_type = "flutter_class"
1421 | else:
1422 | # Auto-detect type by trying different sources
1423 | # First check if it's a known Flutter class
1424 | flutter_libs = ["widgets", "material", "cupertino", "painting", "animation",
1425 | "rendering", "services", "gestures", "foundation"]
1426 |
1427 | # Try to find in common Flutter widgets
1428 | for lib in flutter_libs:
1429 | test_url = f"https://api.flutter.dev/flutter/{lib}/{identifier}-class.html"
1430 | if identifier.lower() in ["container", "scaffold", "appbar", "column", "row",
1431 | "text", "button", "listview", "gridview", "stack"]:
1432 | doc_type = "flutter_class"
1433 | class_name = identifier
1434 | library = "widgets" if identifier.lower() in ["container", "column", "row", "text", "stack"] else "material"
1435 | break
1436 |
1437 | if not doc_type:
1438 | # Could be a package or unknown Flutter class
1439 | # We'll try both and see what works
1440 | doc_type = "auto"
1441 | class_name = identifier
1442 | package_name = identifier
1443 |
1444 | # Based on detected type, fetch documentation
1445 | result = {
1446 | "identifier": identifier,
1447 | "type": doc_type,
1448 | "topic": topic,
1449 | "max_tokens": tokens
1450 | }
1451 |
1452 | if doc_type == "flutter_class" or (doc_type == "auto" and class_name):
1453 | # Try Flutter documentation first
1454 | flutter_doc = await get_flutter_docs(class_name, library or "widgets", tokens=tokens)
1455 |
1456 | if "error" not in flutter_doc:
1457 | # Successfully found Flutter documentation
1458 | content = flutter_doc.get("content", "")
1459 |
1460 | # Apply topic filtering if requested
1461 | if topic:
1462 | content = filter_documentation_by_topic(content, topic, "flutter_class")
1463 | # Recount tokens after filtering
1464 | filtered_tokens = token_manager.count_tokens(content)
1465 | # If filtering reduced content below token limit, no need for further truncation
1466 | if tokens and filtered_tokens > tokens:
1467 | content = truncate_flutter_docs(content, class_name, tokens, strategy="balanced")
1468 | final_tokens = token_manager.count_tokens(content)
1469 | else:
1470 | final_tokens = filtered_tokens
1471 | else:
1472 | # Use the token count from get_flutter_docs if no filtering
1473 | final_tokens = flutter_doc.get("token_count", token_manager.count_tokens(content))
1474 |
1475 | result.update({
1476 | "type": "flutter_class",
1477 | "class": class_name,
1478 | "library": flutter_doc.get("library"),
1479 | "content": content,
1480 | "source": flutter_doc.get("source"),
1481 | "truncated": flutter_doc.get("truncated", False) or topic is not None,
1482 | "token_count": final_tokens,
1483 | "original_tokens": flutter_doc.get("original_tokens", final_tokens),
1484 | "truncation_note": flutter_doc.get("truncation_note")
1485 | })
1486 | return result
1487 | elif doc_type != "auto":
1488 | # Explicit Flutter class not found
1489 | return {
1490 | "identifier": identifier,
1491 | "type": "flutter_class",
1492 | "error": flutter_doc.get("error"),
1493 | "suggestion": flutter_doc.get("suggestion")
1494 | }
1495 |
1496 | if doc_type == "dart_class":
1497 | # Try Dart documentation
1498 | dart_doc = await get_flutter_docs(class_name, library, tokens=tokens)
1499 |
1500 | if "error" not in dart_doc:
1501 | content = dart_doc.get("content", "")
1502 |
1503 | # Apply topic filtering if requested
1504 | if topic:
1505 | content = filter_documentation_by_topic(content, topic, "dart_class")
1506 | # Recount tokens after filtering
1507 | filtered_tokens = token_manager.count_tokens(content)
1508 | # If filtering reduced content below token limit, no need for further truncation
1509 | if tokens and filtered_tokens > tokens:
1510 | content = truncate_flutter_docs(content, class_name, tokens, strategy="balanced")
1511 | final_tokens = token_manager.count_tokens(content)
1512 | else:
1513 | final_tokens = filtered_tokens
1514 | else:
1515 | # Use the token count from get_flutter_docs if no filtering
1516 | final_tokens = dart_doc.get("token_count", token_manager.count_tokens(content))
1517 |
1518 | result.update({
1519 | "type": "dart_class",
1520 | "class": class_name,
1521 | "library": library,
1522 | "content": content,
1523 | "source": dart_doc.get("source"),
1524 | "truncated": dart_doc.get("truncated", False) or topic is not None,
1525 | "token_count": final_tokens,
1526 | "original_tokens": dart_doc.get("original_tokens", final_tokens),
1527 | "truncation_note": dart_doc.get("truncation_note")
1528 | })
1529 | return result
1530 | else:
1531 | return {
1532 | "identifier": identifier,
1533 | "type": "dart_class",
1534 | "error": dart_doc.get("error"),
1535 | "suggestion": "Check the class name and library. Example: dart:async.Future"
1536 | }
1537 |
1538 | if doc_type == "pub_package" or doc_type == "auto":
1539 | # Try pub.dev package
1540 | package_doc = await _get_pub_package_info_impl(package_name)
1541 |
1542 | if "error" not in package_doc:
1543 | # Successfully found package
1544 | # Format content based on topic
1545 | if topic:
1546 | content = format_package_content_by_topic(package_doc, topic)
1547 | else:
1548 | content = format_package_content(package_doc)
1549 |
1550 | # Count original tokens
1551 | original_tokens = token_manager.count_tokens(content)
1552 | truncated = False
1553 | truncation_note = None
1554 |
1555 | # Apply token truncation if needed
1556 | if tokens and original_tokens > tokens:
1557 | truncator = create_truncator(tokens)
1558 | content = truncator.truncate(content)
1559 | truncated = True
1560 | truncation_note = f"Documentation truncated from {original_tokens} to approximately {tokens} tokens"
1561 |
1562 | # Count final tokens
1563 | final_tokens = token_manager.count_tokens(content)
1564 |
1565 | result.update({
1566 | "type": "pub_package",
1567 | "package": package_name,
1568 | "version": package_doc.get("version"),
1569 | "content": content,
1570 | "source": package_doc.get("source"),
1571 | "metadata": {
1572 | "description": package_doc.get("description"),
1573 | "homepage": package_doc.get("homepage"),
1574 | "repository": package_doc.get("repository"),
1575 | "likes": package_doc.get("likes"),
1576 | "pub_points": package_doc.get("pub_points"),
1577 | "platforms": package_doc.get("platforms")
1578 | },
1579 | "truncated": truncated or topic is not None,
1580 | "token_count": final_tokens,
1581 | "original_tokens": original_tokens if truncated else final_tokens,
1582 | "truncation_note": truncation_note
1583 | })
1584 | return result
1585 | elif doc_type == "pub_package":
1586 | # Explicit package not found
1587 | return {
1588 | "identifier": identifier,
1589 | "type": "pub_package",
1590 | "error": package_doc.get("error"),
1591 | "suggestion": "Check the package name on pub.dev"
1592 | }
1593 |
1594 | # If auto-detection failed to find anything
1595 | if doc_type == "auto":
1596 | # Try search as last resort
1597 | search_results = await search_flutter_docs(identifier)
1598 | if search_results.get("results"):
1599 | top_result = search_results["results"][0]
1600 | return {
1601 | "identifier": identifier,
1602 | "type": "search_suggestion",
1603 | "error": f"Could not find exact match for '{identifier}'",
1604 | "suggestion": f"Did you mean '{top_result['title']}'?",
1605 | "search_results": search_results["results"][:3]
1606 | }
1607 | else:
1608 | return {
1609 | "identifier": identifier,
1610 | "type": "not_found",
1611 | "error": f"No documentation found for '{identifier}'",
1612 | "suggestion": "Try using explicit prefixes like 'pub:', 'flutter:', or 'dart:'"
1613 | }
1614 |
1615 | # Should not reach here
1616 | return {
1617 | "identifier": identifier,
1618 | "type": "error",
1619 | "error": "Failed to resolve identifier"
1620 | }
1621 |
1622 |
1623 | def filter_documentation_by_topic(content: str, topic: str, doc_type: str) -> str:
1624 | """Filter documentation content by topic"""
1625 | topic_lower = topic.lower()
1626 |
1627 | if doc_type in ["flutter_class", "dart_class"]:
1628 | # Class documentation topics
1629 | lines = content.split('\n')
1630 | filtered_lines = []
1631 | current_section = None
1632 | include_section = False
1633 |
1634 | for line in lines:
1635 | # Detect section headers
1636 | if line.startswith('## '):
1637 | section_name = line[3:].lower()
1638 | current_section = section_name
1639 |
1640 | # Determine if we should include this section
1641 | if topic_lower == "constructors" and "constructor" in section_name:
1642 | include_section = True
1643 | elif topic_lower == "methods" and "method" in section_name:
1644 | include_section = True
1645 | elif topic_lower == "properties" and "propert" in section_name:
1646 | include_section = True
1647 | elif topic_lower == "examples" and ("example" in section_name or "code" in section_name):
1648 | include_section = True
1649 | else:
1650 | include_section = False
1651 |
1652 | # Always include the class name and description
1653 | if line.startswith('# ') or (current_section == "description" and not line.startswith('## ')):
1654 | filtered_lines.append(line)
1655 | elif include_section:
1656 | filtered_lines.append(line)
1657 |
1658 | return '\n'.join(filtered_lines)
1659 |
1660 | return content
1661 |
1662 |
1663 | def format_package_content(package_doc: Dict[str, Any]) -> str:
1664 | """Format package documentation into readable content"""
1665 | content = []
1666 |
1667 | # Header
1668 | content.append(f"# {package_doc['name']} v{package_doc['version']}")
1669 | content.append("")
1670 |
1671 | # Description
1672 | content.append("## Description")
1673 | content.append(package_doc.get('description', 'No description available'))
1674 | content.append("")
1675 |
1676 | # Metadata
1677 | content.append("## Package Information")
1678 | content.append(f"- **Version**: {package_doc['version']}")
1679 | content.append(f"- **Published**: {package_doc.get('updated', 'Unknown')}")
1680 | content.append(f"- **Publisher**: {package_doc.get('publisher', 'Unknown')}")
1681 | content.append(f"- **Platforms**: {', '.join(package_doc.get('platforms', []))}")
1682 | content.append(f"- **Likes**: {package_doc.get('likes', 0)}")
1683 | content.append(f"- **Pub Points**: {package_doc.get('pub_points', 0)}")
1684 | content.append(f"- **Popularity**: {package_doc.get('popularity', 0)}")
1685 | content.append("")
1686 |
1687 | # Links
1688 | if package_doc.get('homepage') or package_doc.get('repository'):
1689 | content.append("## Links")
1690 | if package_doc.get('homepage'):
1691 | content.append(f"- **Homepage**: {package_doc['homepage']}")
1692 | if package_doc.get('repository'):
1693 | content.append(f"- **Repository**: {package_doc['repository']}")
1694 | if package_doc.get('documentation'):
1695 | content.append(f"- **Documentation**: {package_doc['documentation']}")
1696 | content.append("")
1697 |
1698 | # Dependencies
1699 | if package_doc.get('dependencies'):
1700 | content.append("## Dependencies")
1701 | for dep in package_doc['dependencies']:
1702 | content.append(f"- {dep}")
1703 | content.append("")
1704 |
1705 | # Environment
1706 | if package_doc.get('environment'):
1707 | content.append("## Environment")
1708 | for key, value in package_doc['environment'].items():
1709 | content.append(f"- **{key}**: {value}")
1710 | content.append("")
1711 |
1712 | # README
1713 | if package_doc.get('readme'):
1714 | content.append("## README")
1715 | content.append(package_doc['readme'])
1716 |
1717 | return '\n'.join(content)
1718 |
1719 |
1720 | def format_package_content_by_topic(package_doc: Dict[str, Any], topic: str) -> str:
1721 | """Format package documentation filtered by topic"""
1722 | topic_lower = topic.lower()
1723 | content = []
1724 |
1725 | # Always include header
1726 | content.append(f"# {package_doc['name']} v{package_doc['version']}")
1727 | content.append("")
1728 |
1729 | if topic_lower == "installation":
1730 | content.append("## Installation")
1731 | content.append("")
1732 | content.append("Add this to your package's `pubspec.yaml` file:")
1733 | content.append("")
1734 | content.append("```yaml")
1735 | content.append("dependencies:")
1736 | content.append(f" {package_doc['name']}: ^{package_doc['version']}")
1737 | content.append("```")
1738 | content.append("")
1739 | content.append("Then run:")
1740 | content.append("```bash")
1741 | content.append("flutter pub get")
1742 | content.append("```")
1743 |
1744 | # Include environment requirements
1745 | if package_doc.get('environment'):
1746 | content.append("")
1747 | content.append("### Requirements")
1748 | for key, value in package_doc['environment'].items():
1749 | content.append(f"- **{key}**: {value}")
1750 |
1751 | elif topic_lower == "getting-started":
1752 | content.append("## Getting Started")
1753 | content.append("")
1754 | content.append(package_doc.get('description', 'No description available'))
1755 | content.append("")
1756 |
1757 | # Extract getting started section from README if available
1758 | if package_doc.get('readme'):
1759 | readme_lower = package_doc['readme'].lower()
1760 | # Look for getting started section
1761 | start_idx = readme_lower.find("getting started")
1762 | if start_idx == -1:
1763 | start_idx = readme_lower.find("quick start")
1764 | if start_idx == -1:
1765 | start_idx = readme_lower.find("usage")
1766 |
1767 | if start_idx != -1:
1768 | # Extract section
1769 | readme_section = package_doc['readme'][start_idx:]
1770 | # Find next section header
1771 | next_section = readme_section.find("\n## ")
1772 | if next_section != -1:
1773 | readme_section = readme_section[:next_section]
1774 | content.append(readme_section)
1775 |
1776 | elif topic_lower == "examples":
1777 | content.append("## Examples")
1778 | content.append("")
1779 |
1780 | # Extract examples from README
1781 | if package_doc.get('readme'):
1782 | readme = package_doc['readme']
1783 | # Find code blocks
1784 | code_blocks = re.findall(r'```[\w]*\n(.*?)\n```', readme, re.DOTALL)
1785 | if code_blocks:
1786 | for i, code in enumerate(code_blocks[:5]): # Limit to 5 examples
1787 | content.append(f"### Example {i+1}")
1788 | content.append("```dart")
1789 | content.append(code)
1790 | content.append("```")
1791 | content.append("")
1792 | else:
1793 | content.append("No code examples found in documentation.")
1794 |
1795 | elif topic_lower == "api":
1796 | content.append("## API Reference")
1797 | content.append("")
1798 | content.append(f"Full API documentation: https://pub.dev/documentation/{package_doc['name']}/latest/")
1799 | content.append("")
1800 |
1801 | # Include basic package info
1802 | content.append("### Package Information")
1803 | content.append(f"- **Version**: {package_doc['version']}")
1804 | content.append(f"- **Platforms**: {', '.join(package_doc.get('platforms', []))}")
1805 |
1806 | if package_doc.get('dependencies'):
1807 | content.append("")
1808 | content.append("### Dependencies")
1809 | for dep in package_doc['dependencies']:
1810 | content.append(f"- {dep}")
1811 |
1812 | else:
1813 | # Default to full content for unknown topics
1814 | return format_package_content(package_doc)
1815 |
1816 | return '\n'.join(content)
1817 |
1818 |
1819 | @mcp.tool()
1820 | async def process_flutter_mentions(text: str, tokens: int = 4000) -> Dict[str, Any]:
1821 | """
1822 | Parse text for @flutter_mcp mentions and return relevant documentation.
1823 |
1824 | NOTE: This tool is maintained for backward compatibility. For new integrations,
1825 | consider using the unified tools directly:
1826 | - flutter_docs: For Flutter/Dart classes and pub.dev packages
1827 | - flutter_search: For searching Flutter/Dart documentation
1828 |
1829 | Supports patterns like:
1830 | - @flutter_mcp provider (pub.dev package - latest version)
1831 | - @flutter_mcp provider:^6.0.0 (specific version constraint)
1832 | - @flutter_mcp riverpod:2.5.1 (exact version)
1833 | - @flutter_mcp dio:>=5.0.0 <6.0.0 (version range)
1834 | - @flutter_mcp bloc:latest (latest version keyword)
1835 | - @flutter_mcp material.AppBar (Flutter class)
1836 | - @flutter_mcp dart:async.Future (Dart API)
1837 | - @flutter_mcp Container (widget)
1838 |
1839 | Args:
1840 | text: Text containing @flutter_mcp mentions
1841 | tokens: Maximum token limit for each mention's documentation (default: 4000, min: 500)
1842 |
1843 | Returns:
1844 | Dictionary with parsed mentions and their documentation
1845 | """
1846 | bind_contextvars(tool="process_flutter_mentions", text_length=len(text))
1847 |
1848 | # Validate tokens parameter
1849 | if tokens < 500:
1850 | return {"error": "tokens parameter must be at least 500"}
1851 |
1852 | # Updated pattern to match @flutter_mcp mentions with version constraints
1853 | # Now supports version constraints like :^6.0.0, :>=5.0.0 <6.0.0, etc.
1854 | pattern = r'@flutter_mcp\s+([a-zA-Z0-9_.:]+(?:\s*[<>=^]+\s*[0-9.+\-\w]+(?:\s*[<>=]+\s*[0-9.+\-\w]+)?)?)'
1855 | mentions = re.findall(pattern, text)
1856 |
1857 | if not mentions:
1858 | return {
1859 | "mentions_found": 0,
1860 | "message": "No @flutter_mcp mentions found in text",
1861 | "results": []
1862 | }
1863 |
1864 | logger.info("mentions_found", count=len(mentions))
1865 | results = []
1866 |
1867 | # Process each mention using the unified flutter_docs tool
1868 | for mention in mentions:
1869 | logger.info("processing_mention", mention=mention)
1870 |
1871 | try:
1872 | # Parse version constraints if present
1873 | if ':' in mention and not mention.startswith('dart:'):
1874 | # Package with version constraint
1875 | parts = mention.split(':', 1)
1876 | identifier = parts[0]
1877 | version_spec = parts[1]
1878 |
1879 | # For packages with version constraints, use get_pub_package_info
1880 | if version_spec and version_spec != 'latest':
1881 | # Extract actual version if it's a simple version number
1882 | version = None
1883 | if re.match(r'^\d+\.\d+\.\d+$', version_spec.strip()):
1884 | version = version_spec.strip()
1885 |
1886 | # Get package with specific version
1887 | doc_result = await get_pub_package_info(identifier, version=version)
1888 |
1889 | if "error" not in doc_result:
1890 | results.append({
1891 | "mention": mention,
1892 | "type": "pub_package",
1893 | "documentation": doc_result
1894 | })
1895 | if version_spec and version_spec != version:
1896 | results[-1]["documentation"]["version_constraint"] = version_spec
1897 | else:
1898 | results.append({
1899 | "mention": mention,
1900 | "type": "package_version_error",
1901 | "error": doc_result["error"]
1902 | })
1903 | else:
1904 | # Latest version requested
1905 | doc_result = await flutter_docs(identifier, max_tokens=tokens)
1906 | else:
1907 | # Use unified flutter_docs for all other cases
1908 | doc_result = await flutter_docs(mention, max_tokens=tokens)
1909 |
1910 | # Process the result from flutter_docs
1911 | if "error" not in doc_result:
1912 | # Determine type based on result
1913 | doc_type = doc_result.get("type", "unknown")
1914 |
1915 | if doc_type == "flutter_class":
1916 | results.append({
1917 | "mention": mention,
1918 | "type": "flutter_class",
1919 | "documentation": doc_result
1920 | })
1921 | elif doc_type == "dart_class":
1922 | results.append({
1923 | "mention": mention,
1924 | "type": "dart_api",
1925 | "documentation": doc_result
1926 | })
1927 | elif doc_type == "pub_package":
1928 | results.append({
1929 | "mention": mention,
1930 | "type": "pub_package",
1931 | "documentation": doc_result
1932 | })
1933 | else:
1934 | # Fallback for auto-detected types
1935 | results.append({
1936 | "mention": mention,
1937 | "type": doc_result.get("type", "flutter_widget"),
1938 | "documentation": doc_result
1939 | })
1940 | else:
1941 | # Try search as fallback
1942 | search_result = await flutter_search(mention, limit=1)
1943 | if search_result.get("results"):
1944 | results.append({
1945 | "mention": mention,
1946 | "type": search_result["results"][0].get("type", "flutter_widget"),
1947 | "documentation": search_result["results"][0]
1948 | })
1949 | else:
1950 | results.append({
1951 | "mention": mention,
1952 | "type": "not_found",
1953 | "error": f"No documentation found for '{mention}'"
1954 | })
1955 |
1956 | except Exception as e:
1957 | logger.error("mention_processing_error", mention=mention, error=str(e))
1958 | results.append({
1959 | "mention": mention,
1960 | "type": "error",
1961 | "error": f"Error processing mention: {str(e)}"
1962 | })
1963 |
1964 | # Format results - keep the same format for backward compatibility
1965 | formatted_results = []
1966 | for result in results:
1967 | if "error" in result:
1968 | formatted_results.append({
1969 | "mention": result["mention"],
1970 | "type": result["type"],
1971 | "error": result["error"]
1972 | })
1973 | else:
1974 | doc = result["documentation"]
1975 | if result["type"] == "pub_package":
1976 | # Format package info
1977 | formatted_result = {
1978 | "mention": result["mention"],
1979 | "type": "pub_package",
1980 | "name": doc.get("name", ""),
1981 | "version": doc.get("version", ""),
1982 | "description": doc.get("description", ""),
1983 | "documentation_url": doc.get("documentation", ""),
1984 | "dependencies": doc.get("dependencies", []),
1985 | "likes": doc.get("likes", 0),
1986 | "pub_points": doc.get("pub_points", 0)
1987 | }
1988 |
1989 | # Add version constraint info if present
1990 | if "version_constraint" in doc:
1991 | formatted_result["version_constraint"] = doc["version_constraint"]
1992 | if "resolved_version" in doc:
1993 | formatted_result["resolved_version"] = doc["resolved_version"]
1994 |
1995 | formatted_results.append(formatted_result)
1996 | else:
1997 | # Format Flutter/Dart documentation
1998 | formatted_results.append({
1999 | "mention": result["mention"],
2000 | "type": result["type"],
2001 | "class": doc.get("class", doc.get("identifier", "")),
2002 | "library": doc.get("library", ""),
2003 | "content": doc.get("content", ""),
2004 | "source": doc.get("source", "live")
2005 | })
2006 |
2007 | return {
2008 | "mentions_found": len(mentions),
2009 | "unique_mentions": len(set(mentions)),
2010 | "results": formatted_results,
2011 | "timestamp": datetime.utcnow().isoformat(),
2012 | "note": "This tool is maintained for backward compatibility. Consider using flutter_docs or flutter_search directly."
2013 | }
2014 |
2015 |
2016 | def clean_readme_markdown(readme_content: str) -> str:
2017 | """Clean and format README markdown for AI consumption"""
2018 | if not readme_content:
2019 | return "No README available"
2020 |
2021 | # Remove HTML comments
2022 | readme_content = re.sub(r'<!--.*?-->', '', readme_content, flags=re.DOTALL)
2023 |
2024 | # Remove excessive blank lines
2025 | readme_content = re.sub(r'\n{3,}', '\n\n', readme_content)
2026 |
2027 | # Remove badges/shields (common in READMEs but not useful for AI)
2028 | readme_content = re.sub(r'!\[.*?\]\(.*?shields\.io.*?\)', '', readme_content)
2029 | readme_content = re.sub(r'!\[.*?\]\(.*?badge.*?\)', '', readme_content)
2030 |
2031 | # Clean up any remaining formatting issues
2032 | readme_content = readme_content.strip()
2033 |
2034 | return readme_content
2035 |
2036 |
2037 | @mcp.tool()
2038 | async def get_pub_package_info(package_name: str, version: Optional[str] = None, tokens: int = 6000) -> Dict[str, Any]:
2039 | """
2040 | Get package information from pub.dev including README content.
2041 |
2042 | **DEPRECATED**: This tool is deprecated. Please use flutter_docs() instead
2043 | with the "pub:" prefix (e.g., flutter_docs("pub:provider")).
2044 |
2045 | Args:
2046 | package_name: Name of the pub.dev package (e.g., "provider", "bloc", "dio")
2047 | version: Optional specific version to fetch (e.g., "6.0.5", "2.5.1")
2048 | tokens: Maximum token limit for response (default: 6000, min: 500)
2049 |
2050 | Returns:
2051 | Package information including version, description, metadata, and README
2052 | """
2053 | bind_contextvars(tool="get_pub_package_info", package=package_name, version=version)
2054 | logger.warning("deprecated_tool_usage", tool="get_pub_package_info", replacement="flutter_docs")
2055 |
2056 | # Validate tokens parameter
2057 | if tokens < 500:
2058 | return {"error": "tokens parameter must be at least 500"}
2059 |
2060 | # Call new flutter_docs tool
2061 | identifier = f"pub:{package_name}"
2062 | if version:
2063 | identifier += f":{version}"
2064 |
2065 | result = await flutter_docs(identifier, max_tokens=tokens)
2066 |
2067 | # Transform back to old format
2068 | if result.get("error"):
2069 | return {
2070 | "error": result["error"]
2071 | }
2072 | else:
2073 | metadata = result.get("metadata", {})
2074 | return {
2075 | "source": result.get("source", "live"),
2076 | "name": result.get("package", package_name),
2077 | "version": result.get("version", "latest"),
2078 | "description": metadata.get("description", ""),
2079 | "homepage": metadata.get("homepage", ""),
2080 | "repository": metadata.get("repository", ""),
2081 | "documentation": f"https://pub.dev/packages/{package_name}",
2082 | "dependencies": [], # Not included in new format
2083 | "readme": result.get("content", ""),
2084 | "pub_points": metadata.get("pub_points", 0),
2085 | "likes": metadata.get("likes", 0),
2086 | "fetched_at": datetime.utcnow().isoformat()
2087 | }
2088 |
2089 |
2090 | async def _get_pub_package_info_impl(package_name: str, version: Optional[str] = None) -> Dict[str, Any]:
2091 | """
2092 | Internal implementation of get_pub_package_info functionality.
2093 | """
2094 |
2095 | # Check cache first
2096 | cache_key = get_cache_key("pub_package", package_name, version)
2097 |
2098 | # Check cache
2099 | cached_data = cache_manager.get(cache_key)
2100 | if cached_data:
2101 | logger.info("cache_hit")
2102 | return cached_data
2103 |
2104 | # Rate limit before fetching
2105 | await rate_limiter.acquire()
2106 |
2107 | # Fetch from pub.dev API
2108 | url = f"https://pub.dev/api/packages/{package_name}"
2109 | logger.info("fetching_package", url=url)
2110 |
2111 | try:
2112 | async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
2113 | # Fetch package info
2114 | response = await client.get(
2115 | url,
2116 | headers={
2117 | "User-Agent": "Flutter-MCP-Docs/1.0 (github.com/flutter-mcp/flutter-mcp)"
2118 | }
2119 | )
2120 | response.raise_for_status()
2121 |
2122 | data = response.json()
2123 |
2124 | # If specific version requested, find it in versions list
2125 | if version:
2126 | version_data = None
2127 | for v in data.get("versions", []):
2128 | if v.get("version") == version:
2129 | version_data = v
2130 | break
2131 |
2132 | if not version_data:
2133 | return {
2134 | "error": f"Version '{version}' not found for package '{package_name}'",
2135 | "available_versions": [v.get("version") for v in data.get("versions", [])][:10] # Show first 10
2136 | }
2137 |
2138 | pubspec = version_data.get("pubspec", {})
2139 | actual_version = version_data.get("version", version)
2140 | published_date = version_data.get("published", "")
2141 | else:
2142 | # Use latest version
2143 | latest = data.get("latest", {})
2144 | pubspec = latest.get("pubspec", {})
2145 | actual_version = latest.get("version", "unknown")
2146 | published_date = latest.get("published", "")
2147 |
2148 | result = {
2149 | "source": "live",
2150 | "name": package_name,
2151 | "version": actual_version,
2152 | "description": pubspec.get("description", "No description available"),
2153 | "homepage": pubspec.get("homepage", ""),
2154 | "repository": pubspec.get("repository", ""),
2155 | "documentation": pubspec.get("documentation", f"https://pub.dev/packages/{package_name}"),
2156 | "dependencies": list(pubspec.get("dependencies", {}).keys()),
2157 | "dev_dependencies": list(pubspec.get("dev_dependencies", {}).keys()),
2158 | "environment": pubspec.get("environment", {}),
2159 | "platforms": data.get("platforms", []),
2160 | "updated": published_date,
2161 | "publisher": data.get("publisher", ""),
2162 | "likes": data.get("likeCount", 0),
2163 | "pub_points": data.get("pubPoints", 0),
2164 | "popularity": data.get("popularityScore", 0)
2165 | }
2166 |
2167 | # Fetch README content from package page
2168 | # For specific versions, pub.dev uses /versions/{version} path
2169 | if version:
2170 | readme_url = f"https://pub.dev/packages/{package_name}/versions/{actual_version}"
2171 | else:
2172 | readme_url = f"https://pub.dev/packages/{package_name}"
2173 | logger.info("fetching_readme", url=readme_url)
2174 |
2175 | try:
2176 | # Rate limit before second request
2177 | await rate_limiter.acquire()
2178 |
2179 | readme_response = await client.get(
2180 | readme_url,
2181 | headers={
2182 | "User-Agent": "Flutter-MCP-Docs/1.0 (github.com/flutter-mcp/flutter-mcp)"
2183 | }
2184 | )
2185 | readme_response.raise_for_status()
2186 |
2187 | # Parse page HTML to extract README
2188 | soup = BeautifulSoup(readme_response.text, 'html.parser')
2189 |
2190 | # Find the README content - pub.dev uses a section with specific classes
2191 | readme_div = soup.find('section', class_='detail-tab-readme-content')
2192 | if not readme_div:
2193 | # Try finding any section with markdown-body class
2194 | readme_div = soup.find('section', class_='markdown-body')
2195 | if not readme_div:
2196 | # Try finding div with markdown-body
2197 | readme_div = soup.find('div', class_='markdown-body')
2198 |
2199 | if readme_div:
2200 | # Extract text content and preserve basic markdown structure
2201 | # Convert common HTML elements back to markdown
2202 | for br in readme_div.find_all('br'):
2203 | br.replace_with('\n')
2204 |
2205 | for p in readme_div.find_all('p'):
2206 | p.insert_after('\n\n')
2207 |
2208 | for h1 in readme_div.find_all('h1'):
2209 | h1.insert_before('# ')
2210 | h1.insert_after('\n\n')
2211 |
2212 | for h2 in readme_div.find_all('h2'):
2213 | h2.insert_before('## ')
2214 | h2.insert_after('\n\n')
2215 |
2216 | for h3 in readme_div.find_all('h3'):
2217 | h3.insert_before('### ')
2218 | h3.insert_after('\n\n')
2219 |
2220 | for code in readme_div.find_all('code'):
2221 | if code.parent.name != 'pre':
2222 | code.insert_before('`')
2223 | code.insert_after('`')
2224 |
2225 | for pre in readme_div.find_all('pre'):
2226 | code_block = pre.find('code')
2227 | if code_block:
2228 | lang_class = code_block.get('class', [])
2229 | lang = ''
2230 | for cls in lang_class if isinstance(lang_class, list) else [lang_class]:
2231 | if cls and cls.startswith('language-'):
2232 | lang = cls.replace('language-', '')
2233 | break
2234 | pre.insert_before(f'\n```{lang}\n')
2235 | pre.insert_after('\n```\n')
2236 |
2237 | readme_text = readme_div.get_text()
2238 | result["readme"] = clean_readme_markdown(readme_text)
2239 | else:
2240 | result["readme"] = "README parsing failed - content structure not recognized"
2241 |
2242 | except httpx.HTTPStatusError as e:
2243 | logger.warning("readme_fetch_failed", status_code=e.response.status_code)
2244 | result["readme"] = f"README not available (HTTP {e.response.status_code})"
2245 | except Exception as e:
2246 | logger.warning("readme_fetch_error", error=str(e))
2247 | result["readme"] = f"Failed to fetch README: {str(e)}"
2248 |
2249 | # Cache for 12 hours
2250 | cache_manager.set(cache_key, result, CACHE_DURATIONS["pub_package"])
2251 |
2252 | logger.info("package_fetched_success", has_readme="readme" in result)
2253 | return result
2254 |
2255 | except httpx.HTTPStatusError as e:
2256 | logger.error("http_error", status_code=e.response.status_code)
2257 | return {
2258 | "error": f"Package '{package_name}' not found on pub.dev",
2259 | "status_code": e.response.status_code
2260 | }
2261 | except Exception as e:
2262 | logger.error("fetch_error", error=str(e))
2263 | return {
2264 | "error": f"Failed to fetch package information: {str(e)}"
2265 | }
2266 |
2267 |
2268 | @mcp.tool()
2269 | async def flutter_search(query: str, limit: int = 10, tokens: int = 5000) -> Dict[str, Any]:
2270 | """
2271 | Search across multiple Flutter/Dart documentation sources with unified results.
2272 |
2273 | Searches Flutter classes, Dart classes, pub packages, and concepts in parallel.
2274 | Returns structured results with relevance scoring and documentation hints.
2275 |
2276 | Args:
2277 | query: Search query (e.g., "state management", "Container", "http")
2278 | limit: Maximum number of results to return (default: 10, max: 25)
2279 | tokens: Maximum token limit for response (default: 5000, min: 500)
2280 |
2281 | Returns:
2282 | Unified search results with type classification and relevance scores
2283 | """
2284 | bind_contextvars(tool="flutter_search", query=query, limit=limit)
2285 | logger.info("unified_search_started")
2286 |
2287 | # Validate tokens parameter
2288 | if tokens < 500:
2289 | return {"error": "tokens parameter must be at least 500"}
2290 |
2291 | # Validate limit
2292 | limit = min(max(limit, 1), 25)
2293 |
2294 | # Check cache for search results
2295 | cache_key = get_cache_key("unified_search", f"{query}:{limit}")
2296 | cached_data = cache_manager.get(cache_key)
2297 | if cached_data:
2298 | logger.info("unified_search_cache_hit")
2299 | return cached_data
2300 |
2301 | # Prepare search tasks for parallel execution
2302 | search_tasks = []
2303 | results = []
2304 | query_lower = query.lower()
2305 |
2306 | # Define search functions for parallel execution
2307 | async def search_flutter_classes():
2308 | """Search Flutter widget/class documentation"""
2309 | flutter_results = []
2310 |
2311 | # Check if query is a direct Flutter class reference
2312 | if url := resolve_flutter_url(query):
2313 | # Extract class and library info from resolved URL
2314 | library = "widgets" # Default
2315 | if "flutter/material" in url:
2316 | library = "material"
2317 | elif "flutter/cupertino" in url:
2318 | library = "cupertino"
2319 | elif "flutter/animation" in url:
2320 | library = "animation"
2321 | elif "flutter/painting" in url:
2322 | library = "painting"
2323 | elif "flutter/rendering" in url:
2324 | library = "rendering"
2325 | elif "flutter/services" in url:
2326 | library = "services"
2327 | elif "flutter/gestures" in url:
2328 | library = "gestures"
2329 | elif "flutter/foundation" in url:
2330 | library = "foundation"
2331 |
2332 | class_match = re.search(r'/([^/]+)-class\.html$', url)
2333 | if class_match:
2334 | class_name = class_match.group(1)
2335 | flutter_results.append({
2336 | "id": f"flutter:{library}:{class_name}",
2337 | "type": "flutter_class",
2338 | "relevance": 1.0,
2339 | "title": class_name,
2340 | "library": library,
2341 | "description": f"Flutter {library} class",
2342 | "doc_size": "large",
2343 | "url": url
2344 | })
2345 |
2346 | # Search common Flutter classes
2347 | flutter_classes = [
2348 | # State management
2349 | ("StatefulWidget", "widgets", "Base class for widgets that have mutable state", ["state", "stateful", "widget"]),
2350 | ("StatelessWidget", "widgets", "Base class for widgets that don't require mutable state", ["state", "stateless", "widget"]),
2351 | ("State", "widgets", "Logic and internal state for a StatefulWidget", ["state", "lifecycle"]),
2352 | ("InheritedWidget", "widgets", "Base class for widgets that propagate information down the tree", ["inherited", "propagate", "state"]),
2353 | ("ValueListenableBuilder", "widgets", "Rebuilds when ValueListenable changes", ["value", "listenable", "builder", "state"]),
2354 |
2355 | # Layout widgets
2356 | ("Container", "widgets", "A convenience widget that combines common painting, positioning, and sizing", ["container", "box", "layout"]),
2357 | ("Row", "widgets", "Displays children in a horizontal array", ["row", "horizontal", "layout"]),
2358 | ("Column", "widgets", "Displays children in a vertical array", ["column", "vertical", "layout"]),
2359 | ("Stack", "widgets", "Positions children relative to the box edges", ["stack", "overlay", "position"]),
2360 | ("Scaffold", "material", "Basic material design visual layout structure", ["scaffold", "material", "layout", "structure"]),
2361 |
2362 | # Navigation
2363 | ("Navigator", "widgets", "Manages a stack of Route objects", ["navigator", "navigation", "route"]),
2364 | ("MaterialPageRoute", "material", "A modal route that replaces the entire screen", ["route", "navigation", "page"]),
2365 |
2366 | # Input widgets
2367 | ("TextField", "material", "A material design text field", ["text", "input", "field", "form"]),
2368 | ("GestureDetector", "widgets", "Detects gestures on widgets", ["gesture", "touch", "tap", "click"]),
2369 |
2370 | # Lists
2371 | ("ListView", "widgets", "Scrollable list of widgets", ["list", "scroll", "view"]),
2372 | ("GridView", "widgets", "Scrollable 2D array of widgets", ["grid", "scroll", "view"]),
2373 |
2374 | # Visual
2375 | ("AppBar", "material", "A material design app bar", ["app", "bar", "header", "material"]),
2376 | ("Card", "material", "A material design card", ["card", "material"]),
2377 |
2378 | # Async
2379 | ("FutureBuilder", "widgets", "Builds based on interaction with a Future", ["future", "async", "builder"]),
2380 | ("StreamBuilder", "widgets", "Builds based on interaction with a Stream", ["stream", "async", "builder"]),
2381 | ]
2382 |
2383 | for class_name, library, description, keywords in flutter_classes:
2384 | # Calculate relevance based on query match
2385 | relevance = 0.0
2386 |
2387 | # Direct match
2388 | if query_lower == class_name.lower():
2389 | relevance = 1.0
2390 | elif query_lower in class_name.lower():
2391 | relevance = 0.8
2392 | elif class_name.lower() in query_lower:
2393 | relevance = 0.7
2394 |
2395 | # Keyword match
2396 | if relevance < 0.3:
2397 | for keyword in keywords:
2398 | if keyword in query_lower or query_lower in keyword:
2399 | relevance = max(relevance, 0.5)
2400 | break
2401 |
2402 | # Description match
2403 | if relevance < 0.3 and query_lower in description.lower():
2404 | relevance = 0.4
2405 |
2406 | if relevance > 0.3:
2407 | flutter_results.append({
2408 | "id": f"flutter:{library}:{class_name}",
2409 | "type": "flutter_class",
2410 | "relevance": relevance,
2411 | "title": class_name,
2412 | "library": library,
2413 | "description": description,
2414 | "doc_size": "large"
2415 | })
2416 |
2417 | return flutter_results
2418 |
2419 | async def search_dart_classes():
2420 | """Search Dart core library documentation"""
2421 | dart_results = []
2422 |
2423 | dart_classes = [
2424 | ("List", "dart:core", "An indexable collection of objects with a length", ["list", "array", "collection"]),
2425 | ("Map", "dart:core", "A collection of key/value pairs", ["map", "dictionary", "hash", "key", "value"]),
2426 | ("Set", "dart:core", "A collection of objects with no duplicate elements", ["set", "unique", "collection"]),
2427 | ("String", "dart:core", "A sequence of UTF-16 code units", ["string", "text"]),
2428 | ("Future", "dart:async", "Represents a computation that completes with a value or error", ["future", "async", "promise"]),
2429 | ("Stream", "dart:async", "A source of asynchronous data events", ["stream", "async", "event"]),
2430 | ("Duration", "dart:core", "A span of time", ["duration", "time", "span"]),
2431 | ("DateTime", "dart:core", "An instant in time", ["date", "time", "datetime"]),
2432 | ("RegExp", "dart:core", "A regular expression pattern", ["regex", "regexp", "pattern"]),
2433 | ("Iterable", "dart:core", "A collection of values that can be accessed sequentially", ["iterable", "collection", "sequence"]),
2434 | ]
2435 |
2436 | for class_name, library, description, keywords in dart_classes:
2437 | relevance = 0.0
2438 |
2439 | # Direct match
2440 | if query_lower == class_name.lower():
2441 | relevance = 1.0
2442 | elif query_lower in class_name.lower():
2443 | relevance = 0.8
2444 | elif class_name.lower() in query_lower:
2445 | relevance = 0.7
2446 |
2447 | # Keyword match
2448 | if relevance < 0.3:
2449 | for keyword in keywords:
2450 | if keyword in query_lower or query_lower in keyword:
2451 | relevance = max(relevance, 0.5)
2452 | break
2453 |
2454 | # Description match
2455 | if relevance < 0.3 and query_lower in description.lower():
2456 | relevance = 0.4
2457 |
2458 | if relevance > 0.3:
2459 | dart_results.append({
2460 | "id": f"dart:{library.replace('dart:', '')}:{class_name}",
2461 | "type": "dart_class",
2462 | "relevance": relevance,
2463 | "title": class_name,
2464 | "library": library,
2465 | "description": description,
2466 | "doc_size": "medium"
2467 | })
2468 |
2469 | return dart_results
2470 |
2471 | async def search_pub_packages():
2472 | """Search pub.dev packages"""
2473 | package_results = []
2474 |
2475 | # Define popular packages with categories
2476 | packages = [
2477 | # State Management
2478 | ("provider", "State management library that makes it easy to connect business logic to widgets", ["state", "management", "provider"], "state_management"),
2479 | ("riverpod", "A reactive caching and data-binding framework", ["state", "management", "riverpod", "reactive"], "state_management"),
2480 | ("bloc", "State management library implementing the BLoC design pattern", ["state", "management", "bloc", "pattern"], "state_management"),
2481 | ("get", "Open source state management, navigation and utilities", ["state", "management", "get", "navigation"], "state_management"),
2482 |
2483 | # Networking
2484 | ("dio", "Powerful HTTP client for Dart with interceptors and FormData", ["http", "network", "dio", "api"], "networking"),
2485 | ("http", "A composable, multi-platform, Future-based API for HTTP requests", ["http", "network", "request"], "networking"),
2486 | ("retrofit", "Type-safe HTTP client generator", ["http", "network", "retrofit", "generator"], "networking"),
2487 |
2488 | # Storage
2489 | ("shared_preferences", "Flutter plugin for reading and writing simple key-value pairs", ["storage", "preferences", "settings"], "storage"),
2490 | ("sqflite", "SQLite plugin for Flutter", ["database", "sqlite", "sql", "storage"], "storage"),
2491 | ("hive", "Lightweight and blazing fast key-value database", ["database", "hive", "nosql", "storage"], "storage"),
2492 |
2493 | # Firebase
2494 | ("firebase_core", "Flutter plugin to use Firebase Core API", ["firebase", "core", "backend"], "firebase"),
2495 | ("firebase_auth", "Flutter plugin for Firebase Auth", ["firebase", "auth", "authentication"], "firebase"),
2496 | ("cloud_firestore", "Flutter plugin for Cloud Firestore", ["firebase", "firestore", "database"], "firebase"),
2497 |
2498 | # UI/UX
2499 | ("flutter_svg", "SVG rendering and widget library for Flutter", ["svg", "image", "vector", "ui"], "ui"),
2500 | ("cached_network_image", "Flutter library to load and cache network images", ["image", "cache", "network", "ui"], "ui"),
2501 | ("animations", "Beautiful pre-built animations for Flutter", ["animation", "transition", "ui"], "ui"),
2502 |
2503 | # Navigation
2504 | ("go_router", "A declarative routing package for Flutter", ["navigation", "router", "routing"], "navigation"),
2505 | ("auto_route", "Code generation for type-safe route navigation", ["navigation", "router", "generation"], "navigation"),
2506 |
2507 | # Platform
2508 | ("url_launcher", "Flutter plugin for launching URLs", ["url", "launcher", "platform"], "platform"),
2509 | ("path_provider", "Flutter plugin for getting commonly used locations on filesystem", ["path", "file", "platform"], "platform"),
2510 | ("image_picker", "Flutter plugin for selecting images", ["image", "picker", "camera", "gallery"], "platform"),
2511 | ]
2512 |
2513 | for package_name, description, keywords, category in packages:
2514 | relevance = 0.0
2515 |
2516 | # Direct match
2517 | if query_lower == package_name:
2518 | relevance = 1.0
2519 | elif query_lower in package_name:
2520 | relevance = 0.8
2521 | elif package_name in query_lower:
2522 | relevance = 0.7
2523 |
2524 | # Keyword match
2525 | if relevance < 0.3:
2526 | for keyword in keywords:
2527 | if keyword in query_lower or query_lower in keyword:
2528 | relevance = max(relevance, 0.6)
2529 | break
2530 |
2531 | # Category match
2532 | if relevance < 0.3 and category in query_lower:
2533 | relevance = 0.5
2534 |
2535 | # Description match
2536 | if relevance < 0.3 and query_lower in description.lower():
2537 | relevance = 0.4
2538 |
2539 | if relevance > 0.3:
2540 | package_results.append({
2541 | "id": f"pub:{package_name}",
2542 | "type": "pub_package",
2543 | "relevance": relevance,
2544 | "title": package_name,
2545 | "category": category,
2546 | "description": description,
2547 | "doc_size": "variable",
2548 | "url": f"https://pub.dev/packages/{package_name}"
2549 | })
2550 |
2551 | return package_results
2552 |
2553 | async def search_concepts():
2554 | """Search programming concepts and patterns"""
2555 | concept_results = []
2556 |
2557 | concepts = {
2558 | "state_management": {
2559 | "title": "State Management in Flutter",
2560 | "description": "Techniques for managing application state",
2561 | "keywords": ["state", "management", "provider", "bloc", "riverpod"],
2562 | "related": ["setState", "InheritedWidget", "provider", "bloc", "riverpod", "get"]
2563 | },
2564 | "navigation": {
2565 | "title": "Navigation & Routing",
2566 | "description": "Moving between screens and managing navigation stack",
2567 | "keywords": ["navigation", "routing", "navigator", "route", "screen"],
2568 | "related": ["Navigator", "MaterialPageRoute", "go_router", "deep linking"]
2569 | },
2570 | "async_programming": {
2571 | "title": "Asynchronous Programming",
2572 | "description": "Working with Futures, Streams, and async operations",
2573 | "keywords": ["async", "future", "stream", "await", "asynchronous"],
2574 | "related": ["Future", "Stream", "FutureBuilder", "StreamBuilder", "async/await"]
2575 | },
2576 | "http_networking": {
2577 | "title": "HTTP & Networking",
2578 | "description": "Making HTTP requests and handling network operations",
2579 | "keywords": ["http", "network", "api", "rest", "request"],
2580 | "related": ["http", "dio", "retrofit", "REST API", "JSON"]
2581 | },
2582 | "database_storage": {
2583 | "title": "Database & Storage",
2584 | "description": "Persisting data locally using various storage solutions",
2585 | "keywords": ["database", "storage", "sqlite", "persistence", "cache"],
2586 | "related": ["sqflite", "hive", "shared_preferences", "drift", "objectbox"]
2587 | },
2588 | "animation": {
2589 | "title": "Animations in Flutter",
2590 | "description": "Creating smooth animations and transitions",
2591 | "keywords": ["animation", "transition", "animate", "motion"],
2592 | "related": ["AnimationController", "AnimatedBuilder", "Hero", "Curves"]
2593 | },
2594 | "testing": {
2595 | "title": "Testing Flutter Apps",
2596 | "description": "Unit, widget, and integration testing strategies",
2597 | "keywords": ["test", "testing", "unit", "widget", "integration"],
2598 | "related": ["flutter_test", "mockito", "integration_test", "golden tests"]
2599 | },
2600 | "architecture": {
2601 | "title": "App Architecture Patterns",
2602 | "description": "Organizing code with architectural patterns",
2603 | "keywords": ["architecture", "pattern", "mvvm", "mvc", "clean"],
2604 | "related": ["BLoC Pattern", "MVVM", "Clean Architecture", "Repository Pattern"]
2605 | },
2606 | "performance": {
2607 | "title": "Performance Optimization",
2608 | "description": "Improving app performance and reducing jank",
2609 | "keywords": ["performance", "optimization", "speed", "jank", "profile"],
2610 | "related": ["Performance Profiling", "Widget Inspector", "const constructors"]
2611 | },
2612 | "platform_integration": {
2613 | "title": "Platform Integration",
2614 | "description": "Integrating with native platform features",
2615 | "keywords": ["platform", "native", "channel", "integration", "plugin"],
2616 | "related": ["Platform Channels", "Method Channel", "Plugin Development"]
2617 | }
2618 | }
2619 |
2620 | for concept_id, concept_data in concepts.items():
2621 | relevance = 0.0
2622 |
2623 | # Check keywords
2624 | for keyword in concept_data["keywords"]:
2625 | if keyword in query_lower or query_lower in keyword:
2626 | relevance = max(relevance, 0.7)
2627 |
2628 | # Check title
2629 | if query_lower in concept_data["title"].lower():
2630 | relevance = max(relevance, 0.8)
2631 |
2632 | # Check description
2633 | if relevance < 0.3 and query_lower in concept_data["description"].lower():
2634 | relevance = 0.5
2635 |
2636 | if relevance > 0.3:
2637 | concept_results.append({
2638 | "id": f"concept:{concept_id}",
2639 | "type": "concept",
2640 | "relevance": relevance,
2641 | "title": concept_data["title"],
2642 | "description": concept_data["description"],
2643 | "related_items": concept_data["related"],
2644 | "doc_size": "summary"
2645 | })
2646 |
2647 | return concept_results
2648 |
2649 | # Execute all searches in parallel
2650 | flutter_task = asyncio.create_task(search_flutter_classes())
2651 | dart_task = asyncio.create_task(search_dart_classes())
2652 | pub_task = asyncio.create_task(search_pub_packages())
2653 | concept_task = asyncio.create_task(search_concepts())
2654 |
2655 | # Wait for all searches to complete
2656 | flutter_results, dart_results, pub_results, concept_results = await asyncio.gather(
2657 | flutter_task, dart_task, pub_task, concept_task
2658 | )
2659 |
2660 | # Combine all results
2661 | all_results = flutter_results + dart_results + pub_results + concept_results
2662 |
2663 | # Sort by relevance and limit
2664 | all_results.sort(key=lambda x: x["relevance"], reverse=True)
2665 | results = all_results[:limit]
2666 |
2667 | # Add search metadata
2668 | response = {
2669 | "query": query,
2670 | "total_results": len(all_results),
2671 | "returned_results": len(results),
2672 | "results": results,
2673 | "result_types": {
2674 | "flutter_classes": sum(1 for r in results if r["type"] == "flutter_class"),
2675 | "dart_classes": sum(1 for r in results if r["type"] == "dart_class"),
2676 | "pub_packages": sum(1 for r in results if r["type"] == "pub_package"),
2677 | "concepts": sum(1 for r in results if r["type"] == "concept")
2678 | },
2679 | "timestamp": datetime.utcnow().isoformat()
2680 | }
2681 |
2682 | # Add search suggestions if results are limited
2683 | if len(results) < 5:
2684 | suggestions = []
2685 | if not any(r["type"] == "flutter_class" for r in results):
2686 | suggestions.append("Try searching for specific widget names like 'Container' or 'Scaffold'")
2687 | if not any(r["type"] == "pub_package" for r in results):
2688 | suggestions.append("Search for package names like 'provider' or 'dio'")
2689 | if not any(r["type"] == "concept" for r in results):
2690 | suggestions.append("Try broader concepts like 'state management' or 'navigation'")
2691 |
2692 | response["suggestions"] = suggestions
2693 |
2694 | # Cache the results for 1 hour
2695 | cache_manager.set(cache_key, response, 3600)
2696 |
2697 | logger.info("unified_search_completed",
2698 | total_results=len(all_results),
2699 | returned_results=len(results))
2700 |
2701 | return response
2702 |
2703 |
2704 | @mcp.tool()
2705 | async def flutter_status() -> Dict[str, Any]:
2706 | """
2707 | Check the health status of all Flutter documentation services.
2708 |
2709 | Returns:
2710 | Health status including individual service checks and cache statistics
2711 | """
2712 | checks = {}
2713 | overall_status = "ok"
2714 | timestamp = datetime.utcnow().isoformat()
2715 |
2716 | # Check Flutter docs scraper
2717 | flutter_start = time.time()
2718 | try:
2719 | # Test with Container widget - a stable, core widget unlikely to be removed
2720 | result = await get_flutter_docs("Container", "widgets")
2721 | flutter_duration = int((time.time() - flutter_start) * 1000)
2722 |
2723 | if "error" in result:
2724 | checks["flutter_docs"] = {
2725 | "status": "failed",
2726 | "target": "Container widget",
2727 | "duration_ms": flutter_duration,
2728 | "error": result["error"]
2729 | }
2730 | overall_status = "degraded"
2731 | else:
2732 | checks["flutter_docs"] = {
2733 | "status": "ok",
2734 | "target": "Container widget",
2735 | "duration_ms": flutter_duration,
2736 | "cached": result.get("source") == "cache"
2737 | }
2738 | except Exception as e:
2739 | flutter_duration = int((time.time() - flutter_start) * 1000)
2740 | checks["flutter_docs"] = {
2741 | "status": "failed",
2742 | "target": "Container widget",
2743 | "duration_ms": flutter_duration,
2744 | "error": str(e)
2745 | }
2746 | overall_status = "failed"
2747 |
2748 | # Check pub.dev scraper
2749 | pub_start = time.time()
2750 | try:
2751 | # Test with provider package - extremely popular, unlikely to be removed
2752 | result = await get_pub_package_info("provider")
2753 | pub_duration = int((time.time() - pub_start) * 1000)
2754 |
2755 | if result is None:
2756 | checks["pub_dev"] = {
2757 | "status": "timeout",
2758 | "target": "provider package",
2759 | "duration_ms": pub_duration,
2760 | "error": "Health check timed out after 10 seconds"
2761 | }
2762 | overall_status = "degraded" if overall_status == "ok" else overall_status
2763 | elif result.get("error"):
2764 | checks["pub_dev"] = {
2765 | "status": "failed",
2766 | "target": "provider package",
2767 | "duration_ms": pub_duration,
2768 | "error": result.get("message", "Unknown error"),
2769 | "error_type": result.get("error_type", "unknown")
2770 | }
2771 | overall_status = "degraded" if overall_status == "ok" else overall_status
2772 | else:
2773 | # Additional validation - check if we got expected fields
2774 | has_version = "version" in result and result["version"] != "unknown"
2775 | has_readme = "readme" in result and len(result.get("readme", "")) > 100
2776 |
2777 | if not has_version:
2778 | checks["pub_dev"] = {
2779 | "status": "degraded",
2780 | "target": "provider package",
2781 | "duration_ms": pub_duration,
2782 | "error": "Could not parse version information",
2783 | "cached": result.get("source") == "cache"
2784 | }
2785 | overall_status = "degraded" if overall_status == "ok" else overall_status
2786 | elif not has_readme:
2787 | checks["pub_dev"] = {
2788 | "status": "degraded",
2789 | "target": "provider package",
2790 | "duration_ms": pub_duration,
2791 | "error": "Could not parse README content",
2792 | "cached": result.get("source") == "cache"
2793 | }
2794 | overall_status = "degraded" if overall_status == "ok" else overall_status
2795 | else:
2796 | checks["pub_dev"] = {
2797 | "status": "ok",
2798 | "target": "provider package",
2799 | "duration_ms": pub_duration,
2800 | "version": result["version"],
2801 | "cached": result.get("source") == "cache"
2802 | }
2803 | except Exception as e:
2804 | pub_duration = int((time.time() - pub_start) * 1000)
2805 | checks["pub_dev"] = {
2806 | "status": "failed",
2807 | "target": "provider package",
2808 | "duration_ms": pub_duration,
2809 | "error": str(e)
2810 | }
2811 | overall_status = "failed" if overall_status == "failed" else "degraded"
2812 |
2813 | # Check cache status
2814 | try:
2815 | cache_stats = cache_manager.get_stats()
2816 | checks["cache"] = {
2817 | "status": "ok",
2818 | "message": "SQLite cache operational",
2819 | "stats": cache_stats
2820 | }
2821 | except Exception as e:
2822 | checks["cache"] = {
2823 | "status": "degraded",
2824 | "message": "Cache error",
2825 | "error": str(e)
2826 | }
2827 | overall_status = "degraded"
2828 |
2829 | return {
2830 | "status": overall_status,
2831 | "timestamp": timestamp,
2832 | "checks": checks,
2833 | "message": get_health_message(overall_status)
2834 | }
2835 |
2836 |
2837 | @mcp.tool()
2838 | async def health_check() -> Dict[str, Any]:
2839 | """
2840 | Check the health status of all scrapers and services.
2841 |
2842 | **DEPRECATED**: This tool is deprecated. Please use flutter_status() instead.
2843 |
2844 | Returns:
2845 | Health status including individual scraper checks and overall status
2846 | """
2847 | logger.warning("deprecated_tool_usage", tool="health_check", replacement="flutter_status")
2848 |
2849 | # Simply call the new flutter_status function
2850 | return await flutter_status()
2851 |
2852 |
2853 | def get_health_message(status: str) -> str:
2854 | """Get a human-readable message for the health status"""
2855 | messages = {
2856 | "ok": "All systems operational",
2857 | "degraded": "Service degraded - some features may be slow or unavailable",
2858 | "failed": "Service failed - critical components are not working"
2859 | }
2860 | return messages.get(status, "Unknown status")
2861 |
2862 |
2863 | def main():
2864 | """Main entry point for the Flutter MCP server"""
2865 | import os
2866 |
2867 | # When running from CLI, the header is already printed
2868 | # Only log when not running from CLI (e.g., direct execution)
2869 | if not hasattr(sys, '_flutter_mcp_cli'):
2870 | logger.info("flutter_mcp_starting", version="0.1.0")
2871 |
2872 | # Initialize cache and show stats
2873 | try:
2874 | cache_stats = cache_manager.get_stats()
2875 | logger.info("cache_ready", stats=cache_stats)
2876 | except Exception as e:
2877 | logger.warning("cache_initialization_warning", error=str(e))
2878 |
2879 | # Get transport configuration from environment
2880 | transport = os.environ.get('MCP_TRANSPORT', 'stdio')
2881 | host = os.environ.get('MCP_HOST', '127.0.0.1')
2882 | port = int(os.environ.get('MCP_PORT', '8000'))
2883 |
2884 | # Run the MCP server with appropriate transport
2885 | if transport == 'stdio':
2886 | mcp.run()
2887 | elif transport == 'sse':
2888 | logger.info("starting_sse_transport", host=host, port=port)
2889 | mcp.run(transport='sse', host=host, port=port)
2890 | elif transport == 'http':
2891 | logger.info("starting_http_transport", host=host, port=port)
2892 | # FastMCP handles HTTP transport internally
2893 | mcp.run(transport='http', host=host, port=port, path='/mcp')
2894 |
2895 |
2896 | if __name__ == "__main__":
2897 | main()
```