This is page 2 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/cache.py:
--------------------------------------------------------------------------------
```python
1 | """SQLite-based cache implementation for Flutter MCP Server."""
2 |
3 | import json
4 | import sqlite3
5 | import time
6 | from pathlib import Path
7 | from typing import Optional, Dict, Any
8 | import logging
9 |
10 | try:
11 | from platformdirs import user_cache_dir
12 | except ImportError:
13 | # Fallback to simple home directory approach
14 | def user_cache_dir(app_name: str, app_author: str) -> str:
15 | """Simple fallback for cache directory."""
16 | home = Path.home()
17 | if hasattr(home, 'absolute'):
18 | return str(home / '.cache' / app_name)
19 | return str(Path('.') / '.cache' / app_name)
20 |
21 | logger = logging.getLogger(__name__)
22 |
23 |
24 | class CacheManager:
25 | """SQLite-based cache manager for Flutter documentation."""
26 |
27 | def __init__(self, app_name: str = "FlutterMCP", ttl_hours: int = 24):
28 | """Initialize cache manager.
29 |
30 | Args:
31 | app_name: Application name for cache directory
32 | ttl_hours: Time-to-live for cache entries in hours
33 | """
34 | self.app_name = app_name
35 | self.ttl_seconds = ttl_hours * 3600
36 | self.db_path = self._get_db_path()
37 | self._init_db()
38 |
39 | def _get_db_path(self) -> Path:
40 | """Get platform-specific cache database path."""
41 | cache_dir = user_cache_dir(self.app_name, self.app_name)
42 | cache_path = Path(cache_dir)
43 | cache_path.mkdir(parents=True, exist_ok=True)
44 | return cache_path / "cache.db"
45 |
46 | def _init_db(self) -> None:
47 | """Initialize the cache database."""
48 | with sqlite3.connect(str(self.db_path)) as conn:
49 | # Check if we need to migrate the schema
50 | cursor = conn.execute("""
51 | SELECT sql FROM sqlite_master
52 | WHERE type='table' AND name='doc_cache'
53 | """)
54 | existing_schema = cursor.fetchone()
55 |
56 | if existing_schema and 'token_count' not in existing_schema[0]:
57 | # Migrate existing table to add token_count column
58 | logger.info("Migrating cache schema to add token_count")
59 | conn.execute("""
60 | ALTER TABLE doc_cache
61 | ADD COLUMN token_count INTEGER DEFAULT NULL
62 | """)
63 | else:
64 | # Create new table with token_count
65 | conn.execute("""
66 | CREATE TABLE IF NOT EXISTS doc_cache (
67 | key TEXT PRIMARY KEY NOT NULL,
68 | value TEXT NOT NULL,
69 | created_at INTEGER NOT NULL,
70 | expires_at INTEGER NOT NULL,
71 | token_count INTEGER DEFAULT NULL
72 | )
73 | """)
74 |
75 | # Create index for expiration queries
76 | conn.execute("""
77 | CREATE INDEX IF NOT EXISTS idx_expires_at
78 | ON doc_cache(expires_at)
79 | """)
80 | conn.commit()
81 |
82 | def get(self, key: str) -> Optional[Dict[str, Any]]:
83 | """Get value from cache with lazy expiration.
84 |
85 | Args:
86 | key: Cache key
87 |
88 | Returns:
89 | Cached value as dict or None if not found/expired
90 | """
91 | current_time = int(time.time())
92 |
93 | with sqlite3.connect(str(self.db_path)) as conn:
94 | cursor = conn.execute(
95 | "SELECT value, expires_at, token_count FROM doc_cache WHERE key = ?",
96 | (key,)
97 | )
98 | row = cursor.fetchone()
99 |
100 | if not row:
101 | return None
102 |
103 | value, expires_at, token_count = row
104 |
105 | # Check if expired
106 | if expires_at < current_time:
107 | # Lazy deletion
108 | conn.execute("DELETE FROM doc_cache WHERE key = ?", (key,))
109 | conn.commit()
110 | logger.debug(f"Cache expired for key: {key}")
111 | return None
112 |
113 | try:
114 | result = json.loads(value)
115 | # Add token_count to the result if it exists
116 | if token_count is not None:
117 | result['_cached_token_count'] = token_count
118 | return result
119 | except json.JSONDecodeError:
120 | logger.error(f"Failed to decode cache value for key: {key}")
121 | return None
122 |
123 | def set(self, key: str, value: Dict[str, Any], ttl_override: Optional[int] = None, token_count: Optional[int] = None) -> None:
124 | """Set value in cache.
125 |
126 | Args:
127 | key: Cache key
128 | value: Value to cache (must be JSON-serializable)
129 | ttl_override: Optional TTL override in seconds
130 | token_count: Optional token count to store with the cached data
131 | """
132 | current_time = int(time.time())
133 | ttl = ttl_override or self.ttl_seconds
134 | expires_at = current_time + ttl
135 |
136 | # Extract token count from value if present and not provided explicitly
137 | if token_count is None and '_cached_token_count' in value:
138 | token_count = value.pop('_cached_token_count', None)
139 |
140 | try:
141 | value_json = json.dumps(value)
142 | except (TypeError, ValueError) as e:
143 | logger.error(f"Failed to serialize value for key {key}: {e}")
144 | return
145 |
146 | with sqlite3.connect(str(self.db_path)) as conn:
147 | conn.execute(
148 | """INSERT OR REPLACE INTO doc_cache
149 | (key, value, created_at, expires_at, token_count)
150 | VALUES (?, ?, ?, ?, ?)""",
151 | (key, value_json, current_time, expires_at, token_count)
152 | )
153 | conn.commit()
154 | logger.debug(f"Cached key: {key} (expires in {ttl}s, tokens: {token_count})")
155 |
156 | def delete(self, key: str) -> None:
157 | """Delete a key from cache.
158 |
159 | Args:
160 | key: Cache key to delete
161 | """
162 | with sqlite3.connect(str(self.db_path)) as conn:
163 | conn.execute("DELETE FROM doc_cache WHERE key = ?", (key,))
164 | conn.commit()
165 |
166 | def clear_expired(self) -> int:
167 | """Clear all expired entries from cache.
168 |
169 | Returns:
170 | Number of entries cleared
171 | """
172 | current_time = int(time.time())
173 |
174 | with sqlite3.connect(str(self.db_path)) as conn:
175 | cursor = conn.execute(
176 | "DELETE FROM doc_cache WHERE expires_at < ?",
177 | (current_time,)
178 | )
179 | conn.commit()
180 | return cursor.rowcount
181 |
182 | def clear_all(self) -> None:
183 | """Clear all entries from cache."""
184 | with sqlite3.connect(str(self.db_path)) as conn:
185 | conn.execute("DELETE FROM doc_cache")
186 | conn.commit()
187 |
188 | def get_stats(self) -> Dict[str, Any]:
189 | """Get cache statistics.
190 |
191 | Returns:
192 | Dictionary with cache statistics
193 | """
194 | current_time = int(time.time())
195 |
196 | with sqlite3.connect(str(self.db_path)) as conn:
197 | # Total entries
198 | total = conn.execute("SELECT COUNT(*) FROM doc_cache").fetchone()[0]
199 |
200 | # Expired entries
201 | expired = conn.execute(
202 | "SELECT COUNT(*) FROM doc_cache WHERE expires_at < ?",
203 | (current_time,)
204 | ).fetchone()[0]
205 |
206 | # Entries with token counts
207 | with_tokens = conn.execute(
208 | "SELECT COUNT(*) FROM doc_cache WHERE token_count IS NOT NULL"
209 | ).fetchone()[0]
210 |
211 | # Total tokens cached
212 | total_tokens = conn.execute(
213 | "SELECT SUM(token_count) FROM doc_cache WHERE token_count IS NOT NULL AND expires_at >= ?",
214 | (current_time,)
215 | ).fetchone()[0] or 0
216 |
217 | # Database size
218 | db_size = self.db_path.stat().st_size if self.db_path.exists() else 0
219 |
220 | return {
221 | "total_entries": total,
222 | "expired_entries": expired,
223 | "active_entries": total - expired,
224 | "entries_with_token_counts": with_tokens,
225 | "total_cached_tokens": total_tokens,
226 | "database_size_bytes": db_size,
227 | "database_path": str(self.db_path)
228 | }
229 |
230 |
231 | # Global cache instance
232 | _cache_instance: Optional[CacheManager] = None
233 |
234 |
235 | def get_cache() -> CacheManager:
236 | """Get or create the global cache instance."""
237 | global _cache_instance
238 | if _cache_instance is None:
239 | _cache_instance = CacheManager()
240 | return _cache_instance
```
--------------------------------------------------------------------------------
/docs/planning/project-tasks.csv:
--------------------------------------------------------------------------------
```
1 | Task ID,Category,Task Description,Priority,Status,Who Does This,Implementation Notes,Dependencies,Estimated Time
2 | T001,Core Development,Set up FastMCP Python project structure,Critical,DONE,Claude,Use uv package manager with mcp[cli] httpx redis beautifulsoup4 structlog,,1 hour
3 | T002,Core Development,Implement Redis connection and caching layer,Critical,DONE,Claude,Configure TTLs: 24h for API docs / 12h for packages / 7d for cookbook examples,T001,2 hours
4 | T003,Core Development,Create rate limiter class (2 req/sec),Critical,DONE,Claude,Use asyncio Semaphore pattern from initial-vision.md,T001,1 hour
5 | T004,Documentation Processing,Build HTML to Markdown converter,Critical,DONE,Claude,Extract: description / constructors / properties / methods / code examples,T001,4 hours
6 | T005,Documentation Processing,Implement smart URL resolver,Critical,DONE,Claude,Pattern matching for widgets/material/cupertino/dart:core libraries,T004,2 hours
7 | T006,API & Integration,Create get_flutter_docs tool,Critical,DONE,Claude,On-demand fetching with cache check first,T002/T003/T004,3 hours
8 | T007,API & Integration,Create get_pub_package_docs tool,Critical,DONE,Claude,Use official pub.dev API - format package info nicely,T002/T003,2 hours
9 | T008,API & Integration,Implement @flutter_mcp activation parser,Critical,DONE,Claude,Parse mentions from prompts to trigger package-specific context,T006/T007,2 hours
10 | T009,Core Development,Add structured logging with structlog,High,DONE,Claude,Log: cache hits/misses / fetch times / errors / query patterns,T001,1 hour
11 | T010,Testing & QA,Test with popular packages,Critical,DONE,You + Claude,Test: provider / bloc / get_it / dio / http / freezed,T006/T007,2 hours
12 | T011,Testing & QA,Test with MCP Inspector,Critical,DONE,You,Verify tools work correctly in MCP Inspector before Claude Desktop,T006/T007,1 hour
13 | T012,Documentation,Write comprehensive README,Critical,DONE,Claude,Installation / features / examples / troubleshooting / contribution guide,,2 hours
14 | T013,Documentation,Create installation one-liners,Critical,DONE,Claude,npx command / pip install / docker run examples,T012,1 hour
15 | T014,Marketing & Launch,Create demo video/GIF,Critical,TODO,You,Show: LLM failing -> add @flutter_mcp -> success with latest API info,,3 hours
16 | T015,Marketing & Launch,Write launch blog post,High,TODO,Claude,Technical deep-dive on architecture and problem solved,T014,2 hours
17 | T016,Marketing & Launch,Prepare Reddit launch post,Critical,TODO,Claude,Title: 'Built a free tool that gives Claude/ChatGPT current knowledge of any pub.dev package',T014,1 hour
18 | T017,Community Engagement,Set up GitHub repository,Critical,DONE,You + Claude,Clear README / CONTRIBUTING.md / issue templates / MIT license,,1 hour
19 | T018,Community Engagement,Create Discord/Slack presence,Medium,TODO,You,Join Flutter Discord / be active before launch,,Ongoing
20 | T019,Distribution,Package for PyPI,High,DONE,Claude,Create pyproject.toml with proper metadata and dependencies,T010,1 hour
21 | T020,Distribution,Create Docker image,Medium,DONE,Claude,Include Redis or document external Redis requirement,T010,2 hours
22 | T021,Distribution,npm wrapper package,Low,DONE,Claude,For easier npx installation (wraps Python server),T019,2 hours
23 | T022,Advanced Features,Add search_flutter_docs tool,Medium,DONE,Claude,Search across multiple sources when direct URL fails,T005,3 hours
24 | T023,Advanced Features,Flutter cookbook integration,Medium,TODO,Claude,Scrape and process Flutter cookbook examples,T004,4 hours
25 | T024,Advanced Features,Stack Overflow integration,Low,TODO,Claude,Search Flutter-tagged questions for common issues,T022,4 hours
26 | T025,Advanced Features,Version-specific documentation,Low,TODO,Claude,Allow @flutter_mcp provider:5.0.0 syntax,T006,6 hours
27 | T026,Marketing & Launch,Create comparison table,Medium,TODO,Claude,Flutter MCP vs vanilla LLM vs other tools,,1 hour
28 | T027,Marketing & Launch,Reach out to Flutter influencers,Medium,TODO,You,Contact Flutter YouTubers / bloggers for early access,,2 hours
29 | T028,Community Engagement,Implement live request fulfillment,High,TODO,You,Monitor launch thread and add requested packages in real-time,T007,4 hours
30 | T029,Testing & QA,Load testing with concurrent requests,Medium,TODO,You,Ensure Redis and rate limiting work under load,T010,2 hours
31 | T030,Documentation,Create API documentation,Medium,DONE,Claude,Document all MCP tools and their parameters,T012,1 hour
32 | T031,Core Development,Error handling and fallbacks,High,DONE,Claude,Graceful handling when docs not found / network issues,T006/T007,2 hours
33 | T032,Marketing & Launch,SEO optimize GitHub repository,Low,TODO,Claude,Keywords: Flutter MCP / Claude Flutter / Cursor Flutter / AI Flutter docs,,30 min
34 | T033,Community Engagement,Create feedback collection system,Medium,TODO,You + Claude,GitHub discussions / feedback form / analytics,T017,1 hour
35 | T034,Advanced Features,Null safety awareness,Medium,TODO,Claude,Tag content with null-safety status where applicable,T004,3 hours
36 | T035,Marketing & Launch,Create 'use @flutter_mcp' badge,Low,TODO,Claude,Markdown badge for package maintainers to add to READMEs,,1 hour
37 | T036,Core Development,Implement on-demand ingestion,Critical,TODO,Claude,Queue system for processing new packages when first requested,T007,6 hours
38 | T037,Documentation Processing,Build Dart code parser,High,TODO,Claude,Extract doc comments / class signatures / method signatures from source,T036,8 hours
39 | T038,Advanced Features,Public status page,Medium,TODO,Claude,Show which packages are indexed / being processed / can be requested,T036,3 hours
40 | T039,Marketing & Launch,Define success metrics,High,TODO,Claude,GitHub stars / daily active users / packages indexed / cache hit rate,,1 hour
41 | T040,Testing & QA,Create integration tests,Medium,DONE,Claude,Test full flow: request -> fetch -> process -> cache -> retrieve,T010,3 hours
42 | T041,Marketing & Launch,Create developer testimonial system,High,TODO,You,Google Form for collecting specific problem-solving stories,T014,1 hour
43 | T042,Marketing & Launch,Build showcase page,High,TODO,Claude,Curated examples of complex queries the tool handles perfectly,T014,2 hours
44 | T043,Marketing & Launch,Design 'Powered by @flutter_mcp' badge,Medium,TODO,Claude,SVG badge for users to add to their projects/READMEs,,1 hour
45 | T044,Community Engagement,Create Discord channel,High,TODO,You,Dedicated space for Q&A / feature requests / bug reports,T017,1 hour
46 | T045,Marketing & Launch,Flutter package maintainer outreach,Critical,TODO,You,Contact top 20 package maintainers for early access and feedback,T010,3 hours
47 | T046,Content Creation,Write technical deep-dive posts,Medium,TODO,Claude,3-part series: Architecture / Parsing Strategy / RAG Optimization,T015,6 hours
48 | T047,Community Engagement,Define contribution guidelines,High,TODO,Claude,Good first issues / PR template / code of conduct,T017,2 hours
49 | T048,Marketing & Launch,Create meme templates,Low,TODO,Claude,Generic LLM vs @flutter_mcp format - let community fill in,T014,1 hour
50 | T049,Marketing & Launch,YouTube influencer outreach,High,TODO,You,Contact: FilledStacks / Reso Coder / Flutter Explained / Code With Andrea,T014,3 hours
51 | T050,Advanced Features,Package maintainer dashboard,Low,TODO,Claude,Let maintainers see usage stats for their packages,T038,4 hours
52 | T051,Marketing & Launch,Launch week planning,Critical,TODO,You + Claude,Day 1: Reddit / Day 2: Twitter / Day 3: Newsletter / Day 4-5: YouTube,T016,2 hours
53 | T052,Community Engagement,Implement feature voting system,Medium,TODO,Claude,Let community vote on which packages to prioritize,T033,2 hours
54 | T053,Marketing & Launch,Create comparison demos,Critical,TODO,Claude,Side-by-side: ChatGPT vs ChatGPT+@flutter_mcp on same query,T014,3 hours
55 | T054,Content Creation,Weekly 'Flutter AI Tips' series,Low,TODO,You + Claude,Short posts showing cool things possible with @flutter_mcp,,Ongoing
56 | T055,Marketing & Launch,SEO content creation,Medium,TODO,Claude,Blog posts targeting 'Flutter AI' / 'Claude Flutter' / 'Cursor Flutter' keywords,T015,4 hours
57 | T056,Community Engagement,Host live coding session,Medium,TODO,You,Twitch/YouTube stream solving Flutter problems with the tool,T014,2 hours
58 | T057,Marketing & Launch,Create viral launch video,Critical,TODO,You,30-second video: problem → solution → wow moment,T014,4 hours
59 | T058,Advanced Features,VS Code extension exploration,Low,TODO,Claude,Research deeper IDE integration possibilities beyond MCP,T010,6 hours
60 | T059,Marketing & Launch,Press kit creation,Medium,TODO,Claude + You,Logo / screenshots / one-pager / key messages for easy sharing,T014,2 hours
61 | T060,Community Engagement,Flutter Weekly submission,High,TODO,You,Submit to Flutter Weekly newsletter after launch,T016,30 min
```
--------------------------------------------------------------------------------
/tests/test_integration.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """Integration tests for Flutter MCP Server"""
3 |
4 | import asyncio
5 | import sys
6 | from typing import List, Tuple
7 | from pathlib import Path
8 |
9 | # Add the src directory to the Python path
10 | sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
11 |
12 | from flutter_mcp.server import get_flutter_docs, search_flutter_docs, get_pub_package_info, process_flutter_mentions, health_check
13 |
14 |
15 | class TestResult:
16 | def __init__(self, name: str, passed: bool, message: str = ""):
17 | self.name = name
18 | self.passed = passed
19 | self.message = message
20 |
21 |
22 | async def test_flutter_docs_core_widgets() -> TestResult:
23 | """Test fetching documentation for core Flutter widgets"""
24 | test_cases = [
25 | ("Container", "widgets"),
26 | ("Scaffold", "material"),
27 | ("Text", "widgets"),
28 | ("Column", "widgets"),
29 | ("Row", "widgets"),
30 | ]
31 |
32 | for class_name, library in test_cases:
33 | try:
34 | result = await get_flutter_docs(class_name, library)
35 | if "error" in result:
36 | return TestResult(
37 | "Flutter Docs - Core Widgets",
38 | False,
39 | f"Failed to fetch {library}.{class_name}: {result['error']}"
40 | )
41 |
42 | # Verify content
43 | content = result.get("content", "")
44 | if len(content) < 100:
45 | return TestResult(
46 | "Flutter Docs - Core Widgets",
47 | False,
48 | f"Content too short for {library}.{class_name}: {len(content)} chars"
49 | )
50 | except Exception as e:
51 | return TestResult(
52 | "Flutter Docs - Core Widgets",
53 | False,
54 | f"Exception fetching {library}.{class_name}: {str(e)}"
55 | )
56 |
57 | return TestResult("Flutter Docs - Core Widgets", True, f"Tested {len(test_cases)} core widgets")
58 |
59 |
60 | async def test_pub_packages_popular() -> TestResult:
61 | """Test fetching information for popular pub.dev packages"""
62 | test_packages = [
63 | "provider",
64 | "bloc",
65 | "dio",
66 | "get",
67 | "riverpod",
68 | ]
69 |
70 | for package in test_packages:
71 | try:
72 | result = await get_pub_package_info(package)
73 | if "error" in result:
74 | return TestResult(
75 | "Pub.dev Packages - Popular",
76 | False,
77 | f"Failed to fetch {package}: {result['error']}"
78 | )
79 |
80 | # Verify required fields
81 | if not result.get("version"):
82 | return TestResult(
83 | "Pub.dev Packages - Popular",
84 | False,
85 | f"No version for {package}"
86 | )
87 |
88 | if "readme" not in result or len(result["readme"]) < 100:
89 | return TestResult(
90 | "Pub.dev Packages - Popular",
91 | False,
92 | f"README missing or too short for {package}"
93 | )
94 | except Exception as e:
95 | return TestResult(
96 | "Pub.dev Packages - Popular",
97 | False,
98 | f"Exception fetching {package}: {str(e)}"
99 | )
100 |
101 | return TestResult("Pub.dev Packages - Popular", True, f"Tested {len(test_packages)} popular packages")
102 |
103 |
104 | async def test_search_functionality() -> TestResult:
105 | """Test search functionality with various patterns"""
106 | test_queries = [
107 | "Container",
108 | "material.AppBar",
109 | "cupertino.CupertinoButton",
110 | "dart:core.List",
111 | ]
112 |
113 | for query in test_queries:
114 | try:
115 | result = await search_flutter_docs(query)
116 | if result.get("total", 0) == 0:
117 | return TestResult(
118 | "Search Functionality",
119 | False,
120 | f"No results for query: {query}"
121 | )
122 | except Exception as e:
123 | return TestResult(
124 | "Search Functionality",
125 | False,
126 | f"Exception searching for {query}: {str(e)}"
127 | )
128 |
129 | return TestResult("Search Functionality", True, f"Tested {len(test_queries)} search patterns")
130 |
131 |
132 | async def test_flutter_mentions() -> TestResult:
133 | """Test @flutter_mcp mention processing"""
134 | test_text = """
135 | I need help with @flutter_mcp provider for state management.
136 | Also, how do I use @flutter_mcp material.Scaffold properly?
137 | What about @flutter_mcp dart:async.Future?
138 | """
139 |
140 | try:
141 | result = await process_flutter_mentions(test_text)
142 |
143 | if result.get("mentions_found", 0) != 3:
144 | return TestResult(
145 | "Flutter Mentions",
146 | False,
147 | f"Expected 3 mentions, found {result.get('mentions_found', 0)}"
148 | )
149 |
150 | # Check each mention was processed
151 | results = result.get("results", [])
152 | if len(results) != 3:
153 | return TestResult(
154 | "Flutter Mentions",
155 | False,
156 | f"Expected 3 results, got {len(results)}"
157 | )
158 |
159 | # Verify mention types
160 | expected_types = ["pub_package", "flutter_class", "dart_api"]
161 | actual_types = [r.get("type") for r in results]
162 |
163 | for expected in expected_types:
164 | if expected not in actual_types:
165 | return TestResult(
166 | "Flutter Mentions",
167 | False,
168 | f"Missing expected type: {expected}. Got: {actual_types}"
169 | )
170 |
171 | except Exception as e:
172 | return TestResult(
173 | "Flutter Mentions",
174 | False,
175 | f"Exception processing mentions: {str(e)}"
176 | )
177 |
178 | return TestResult("Flutter Mentions", True, "All mention types processed correctly")
179 |
180 |
181 | async def test_health_check_functionality() -> TestResult:
182 | """Test health check returns expected format"""
183 | try:
184 | result = await health_check()
185 |
186 | # Verify structure
187 | required_fields = ["status", "timestamp", "checks", "message"]
188 | for field in required_fields:
189 | if field not in result:
190 | return TestResult(
191 | "Health Check",
192 | False,
193 | f"Missing required field: {field}"
194 | )
195 |
196 | # Verify checks
197 | required_checks = ["flutter_docs", "pub_dev", "redis"]
198 | for check in required_checks:
199 | if check not in result["checks"]:
200 | return TestResult(
201 | "Health Check",
202 | False,
203 | f"Missing required check: {check}"
204 | )
205 |
206 | # Verify status is valid
207 | valid_statuses = ["ok", "degraded", "failed"]
208 | if result["status"] not in valid_statuses:
209 | return TestResult(
210 | "Health Check",
211 | False,
212 | f"Invalid status: {result['status']}"
213 | )
214 |
215 | except Exception as e:
216 | return TestResult(
217 | "Health Check",
218 | False,
219 | f"Exception running health check: {str(e)}"
220 | )
221 |
222 | return TestResult("Health Check", True, "Health check structure valid")
223 |
224 |
225 | async def run_all_tests():
226 | """Run all integration tests"""
227 | print("🧪 Flutter MCP Server Integration Tests")
228 | print("=" * 50)
229 |
230 | tests = [
231 | test_flutter_docs_core_widgets(),
232 | test_pub_packages_popular(),
233 | test_search_functionality(),
234 | test_flutter_mentions(),
235 | test_health_check_functionality(),
236 | ]
237 |
238 | # Run tests with progress indicator
239 | results = []
240 | for i, test in enumerate(tests, 1):
241 | print(f"\n[{i}/{len(tests)}] Running {test.__name__}...", end="", flush=True)
242 | result = await test
243 | results.append(result)
244 |
245 | if result.passed:
246 | print(f" ✅ PASSED")
247 | else:
248 | print(f" ❌ FAILED")
249 | print(f" {result.message}")
250 |
251 | # Summary
252 | print("\n" + "=" * 50)
253 | print("Test Summary:")
254 | print("=" * 50)
255 |
256 | passed = sum(1 for r in results if r.passed)
257 | total = len(results)
258 |
259 | for result in results:
260 | status = "✅ PASS" if result.passed else "❌ FAIL"
261 | print(f"{status} | {result.name}")
262 | if result.message and result.passed:
263 | print(f" {result.message}")
264 |
265 | print(f"\nTotal: {passed}/{total} passed ({passed/total*100:.0f}%)")
266 |
267 | # Return exit code
268 | return 0 if passed == total else 1
269 |
270 |
271 | if __name__ == "__main__":
272 | import warnings
273 | warnings.filterwarnings("ignore", category=DeprecationWarning)
274 |
275 | # Set up event loop
276 | exit_code = asyncio.run(run_all_tests())
277 | sys.exit(exit_code)
```
--------------------------------------------------------------------------------
/docs/token-counting-analysis.md:
--------------------------------------------------------------------------------
```markdown
1 | # Token Counting Analysis for Flutter MCP
2 |
3 | ## Executive Summary
4 |
5 | For the Flutter MCP documentation server, implementing accurate and performant token counting is crucial to ensure responses fit within LLM context windows. This analysis recommends using model-specific tokenizers with intelligent caching for optimal accuracy and performance.
6 |
7 | ## Recommended Approach
8 |
9 | ### Primary Strategy: Model-Specific Tokenizers with Caching
10 |
11 | Use official tokenizers for each supported model family:
12 | - **OpenAI Models**: `tiktoken` library
13 | - **Claude Models**: `anthropic` library's `count_tokens()` API
14 | - **Gemini Models**: `google-genai` library's `count_tokens()` method
15 |
16 | ### Implementation Architecture
17 |
18 | ```python
19 | # utils/token_counter.py
20 | import tiktoken
21 | import anthropic
22 | from google import genai
23 | from google.genai.types import HttpOptions
24 | from typing import Union, Dict, Any
25 | import structlog
26 |
27 | logger = structlog.get_logger()
28 |
29 | class TokenCounter:
30 | """Unified token counter with model-specific tokenizer support."""
31 |
32 | def __init__(self):
33 | self._tokenizer_cache: Dict[str, Any] = {}
34 | self._anthropic_client = None
35 | self._genai_client = None
36 |
37 | def _get_openai_tokenizer(self, model: str):
38 | """Get or create OpenAI tokenizer."""
39 | if model not in self._tokenizer_cache:
40 | try:
41 | # Try model-specific encoding
42 | self._tokenizer_cache[model] = tiktoken.encoding_for_model(model)
43 | except KeyError:
44 | # Fallback to cl100k_base for newer models
45 | self._tokenizer_cache[model] = tiktoken.get_encoding("cl100k_base")
46 | return self._tokenizer_cache[model]
47 |
48 | def _get_anthropic_client(self):
49 | """Get or create Anthropic client."""
50 | if self._anthropic_client is None:
51 | self._anthropic_client = anthropic.Anthropic()
52 | return self._anthropic_client
53 |
54 | def _get_genai_client(self):
55 | """Get or create Google GenAI client."""
56 | if self._genai_client is None:
57 | self._genai_client = genai.Client(
58 | http_options=HttpOptions(api_version="v1")
59 | )
60 | return self._genai_client
61 |
62 | def count_tokens(self, text: str, model: str = "gpt-4") -> int:
63 | """
64 | Count tokens for the given text and model.
65 |
66 | Args:
67 | text: The text to count tokens for
68 | model: The model name (e.g., "gpt-4", "claude-3-opus", "gemini-1.5-pro")
69 |
70 | Returns:
71 | Number of tokens
72 | """
73 | try:
74 | # OpenAI models
75 | if model.startswith(("gpt-", "text-embedding-")):
76 | tokenizer = self._get_openai_tokenizer(model)
77 | return len(tokenizer.encode(text))
78 |
79 | # Claude models
80 | elif model.startswith("claude-"):
81 | client = self._get_anthropic_client()
82 | response = client.beta.messages.count_tokens(
83 | model=model,
84 | messages=[{"role": "user", "content": text}]
85 | )
86 | return response.input_tokens
87 |
88 | # Gemini models
89 | elif model.startswith("gemini-"):
90 | client = self._get_genai_client()
91 | response = client.models.count_tokens(
92 | model=model,
93 | contents=text
94 | )
95 | return response.total_tokens
96 |
97 | # Unknown model - use cl100k_base as fallback
98 | else:
99 | logger.warning(f"Unknown model {model}, using cl100k_base tokenizer")
100 | tokenizer = self._get_openai_tokenizer("cl100k_base")
101 | return len(tokenizer.encode(text))
102 |
103 | except Exception as e:
104 | logger.error(f"Error counting tokens for {model}: {e}")
105 | # Fallback to character-based approximation with safety margin
106 | return int(len(text) / 3.5) # Conservative estimate
107 |
108 | # Global instance for reuse
109 | token_counter = TokenCounter()
110 | ```
111 |
112 | ## Performance Optimization Strategies
113 |
114 | ### 1. Tokenizer Caching
115 | - **Critical**: Cache tokenizer instances to avoid initialization overhead
116 | - OpenAI's `tiktoken` has minimal overhead, but still benefits from caching
117 | - Anthropic and Google clients should be singleton instances
118 |
119 | ### 2. Batch Processing
120 | When processing multiple documents:
121 | ```python
122 | # For tiktoken (OpenAI)
123 | encoding = tiktoken.get_encoding("cl100k_base")
124 | token_counts = [len(tokens) for tokens in encoding.encode_batch(texts)]
125 |
126 | # For other providers, implement parallel processing
127 | from concurrent.futures import ThreadPoolExecutor
128 |
129 | def batch_count_tokens(texts: List[str], model: str) -> List[int]:
130 | with ThreadPoolExecutor(max_workers=5) as executor:
131 | futures = [executor.submit(token_counter.count_tokens, text, model)
132 | for text in texts]
133 | return [future.result() for future in futures]
134 | ```
135 |
136 | ### 3. Redis Integration for Token Count Caching
137 | ```python
138 | async def get_documentation_with_token_count(
139 | self,
140 | query: str,
141 | model: str = "gpt-4"
142 | ) -> Tuple[str, int]:
143 | """Get documentation with pre-calculated token count."""
144 |
145 | # Check Redis for cached result with token count
146 | cache_key = f"flutter_doc:{query}:{model}"
147 | cached = await self.redis.get(cache_key)
148 |
149 | if cached:
150 | data = json.loads(cached)
151 | return data["content"], data["token_count"]
152 |
153 | # Fetch and process documentation
154 | content = await self.fetch_documentation(query)
155 |
156 | # Count tokens on final formatted content
157 | token_count = token_counter.count_tokens(content, model)
158 |
159 | # Cache with token count
160 | await self.redis.set(
161 | cache_key,
162 | json.dumps({
163 | "content": content,
164 | "token_count": token_count
165 | }),
166 | ex=86400 # 24 hour TTL
167 | )
168 |
169 | return content, token_count
170 | ```
171 |
172 | ## Markdown Formatting Considerations
173 |
174 | ### Count Tokens on Final Output
175 | Always count tokens on the exact string sent to the LLM:
176 |
177 | ```python
178 | def prepare_response(raw_content: str, max_tokens: int, model: str) -> str:
179 | """Prepare and truncate response to fit token limit."""
180 |
181 | # Apply all formatting transformations
182 | formatted_content = format_markdown(raw_content)
183 |
184 | # Count tokens on formatted content
185 | token_count = token_counter.count_tokens(formatted_content, model)
186 |
187 | # Truncate if necessary
188 | if token_count > max_tokens:
189 | # Intelligent truncation - keep complete sections
190 | formatted_content = truncate_intelligently(
191 | formatted_content,
192 | max_tokens,
193 | model
194 | )
195 |
196 | return formatted_content
197 | ```
198 |
199 | ### Token Impact of Markdown Elements
200 | - **Code blocks**: Very token-intensive (backticks + language + indentation)
201 | - **Links**: Full markdown syntax counts `[text](url)`
202 | - **Headers**: All `#` characters count as tokens
203 | - **Lists**: Bullets and indentation consume tokens
204 |
205 | ## Approximation Methods (Fallback Only)
206 |
207 | When model-specific tokenizers are unavailable:
208 |
209 | ```python
210 | def approximate_tokens(text: str, model_family: str = "general") -> int:
211 | """
212 | Approximate token count with model-specific adjustments.
213 | Use only as fallback when proper tokenizers unavailable.
214 | """
215 | # Base approximations
216 | char_ratio = {
217 | "gpt": 4.0, # GPT models: ~4 chars/token
218 | "claude": 3.8, # Claude: slightly more tokens
219 | "gemini": 4.2, # Gemini: slightly fewer tokens
220 | "general": 3.5 # Conservative default
221 | }
222 |
223 | ratio = char_ratio.get(model_family, 3.5)
224 | base_count = len(text) / ratio
225 |
226 | # Adjust for code content (more tokens)
227 | code_blocks = text.count("```")
228 | if code_blocks > 0:
229 | base_count *= 1.15
230 |
231 | # Safety margin
232 | return int(base_count * 1.2)
233 | ```
234 |
235 | ## Implementation Timeline
236 |
237 | ### Phase 1: Core Implementation (2 hours)
238 | 1. Implement `TokenCounter` class with OpenAI support
239 | 2. Add fallback approximation method
240 | 3. Integrate with existing response pipeline
241 |
242 | ### Phase 2: Multi-Model Support (2 hours)
243 | 1. Add Anthropic client support
244 | 2. Add Google GenAI client support
245 | 3. Implement model detection logic
246 |
247 | ### Phase 3: Optimization (1 hour)
248 | 1. Add Redis caching for token counts
249 | 2. Implement batch processing
250 | 3. Add performance monitoring
251 |
252 | ## Testing Strategy
253 |
254 | ```python
255 | # tests/test_token_counter.py
256 | import pytest
257 | from utils.token_counter import token_counter
258 |
259 | class TestTokenCounter:
260 |
261 | @pytest.mark.parametrize("model,text,expected_range", [
262 | ("gpt-4", "Hello, world!", (3, 5)),
263 | ("claude-3-opus-20240229", "Hello, world!", (3, 5)),
264 | ("gemini-1.5-pro", "Hello, world!", (3, 5)),
265 | ])
266 | def test_basic_counting(self, model, text, expected_range):
267 | count = token_counter.count_tokens(text, model)
268 | assert expected_range[0] <= count <= expected_range[1]
269 |
270 | def test_markdown_formatting(self):
271 | markdown = "# Header\n```python\nprint('hello')\n```"
272 | count = token_counter.count_tokens(markdown, "gpt-4")
273 | # Markdown should produce more tokens than plain text
274 | plain_count = token_counter.count_tokens("Header print hello", "gpt-4")
275 | assert count > plain_count
276 |
277 | def test_fallback_approximation(self):
278 | # Test with unknown model
279 | count = token_counter.count_tokens("Test text", "unknown-model")
280 | assert count > 0
281 | ```
282 |
283 | ## Recommendations
284 |
285 | 1. **Use Model-Specific Tokenizers**: Accuracy is worth the minimal performance cost
286 | 2. **Cache Everything**: Both tokenizer instances and token counts
287 | 3. **Count Final Output**: Always count tokens on the exact formatted string
288 | 4. **Plan for Growth**: Design the system to easily add new model support
289 | 5. **Monitor Performance**: Track token counting time in your metrics
290 |
291 | ## Conclusion
292 |
293 | For the Flutter MCP project, implementing proper token counting with model-specific tokenizers will ensure accurate context window management while maintaining the fast response times required by the Context7-style architecture. The recommended approach balances accuracy, performance, and maintainability while providing graceful fallbacks for edge cases.
```
--------------------------------------------------------------------------------
/src/flutter_mcp/truncation.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Smart truncation module for Flutter documentation.
3 |
4 | Implements priority-based truncation that preserves the most important content
5 | while respecting token limits and maintaining markdown formatting.
6 | """
7 |
8 | import re
9 | from typing import List, Tuple, Optional
10 | from dataclasses import dataclass
11 |
12 |
13 | @dataclass
14 | class Section:
15 | """Represents a documentation section with content and priority."""
16 | name: str
17 | content: str
18 | priority: int # Lower number = higher priority
19 | start_pos: int
20 | end_pos: int
21 |
22 |
23 | class DocumentTruncator:
24 | """Smart truncation for Flutter/Dart documentation."""
25 |
26 | # Section priorities (lower = more important)
27 | SECTION_PRIORITIES = {
28 | 'description': 1,
29 | 'summary': 1,
30 | 'constructors': 2,
31 | 'properties': 3,
32 | 'methods': 4,
33 | 'parameters': 5,
34 | 'returns': 5,
35 | 'examples': 6,
36 | 'example': 6,
37 | 'see also': 7,
38 | 'implementation': 8,
39 | 'source': 9,
40 | }
41 |
42 | # Approximate tokens per character (rough estimate)
43 | TOKENS_PER_CHAR = 0.25
44 |
45 | def __init__(self):
46 | """Initialize the document truncator."""
47 | pass
48 |
49 | def truncate_to_limit(self, content: str, token_limit: int) -> str:
50 | """
51 | Truncate content to fit within token limit while preserving structure.
52 |
53 | Args:
54 | content: The markdown content to truncate
55 | token_limit: Maximum number of tokens allowed
56 |
57 | Returns:
58 | Truncated content with truncation notice if needed
59 | """
60 | # Quick check - if content is already small enough, return as-is
61 | estimated_tokens = self._estimate_tokens(content)
62 | if estimated_tokens <= token_limit:
63 | return content
64 |
65 | # Detect sections in the content
66 | sections = self._detect_sections(content)
67 |
68 | # If no sections detected, fall back to simple truncation
69 | if not sections:
70 | return self._simple_truncate(content, token_limit)
71 |
72 | # Build truncated content based on priorities
73 | truncated = self._priority_truncate(content, sections, token_limit)
74 |
75 | # Add truncation notice
76 | return self._add_truncation_notice(truncated)
77 |
78 | def _estimate_tokens(self, text: str) -> int:
79 | """Estimate token count from text length."""
80 | return int(len(text) * self.TOKENS_PER_CHAR)
81 |
82 | def _detect_sections(self, content: str) -> List[Section]:
83 | """Detect markdown sections in the content."""
84 | sections = []
85 |
86 | # Pattern for markdown headers (##, ###, ####)
87 | header_pattern = r'^(#{2,4})\s+(.+)$'
88 |
89 | lines = content.split('\n')
90 | current_section = None
91 | current_content = []
92 | current_start = 0
93 |
94 | for i, line in enumerate(lines):
95 | match = re.match(header_pattern, line, re.MULTILINE)
96 |
97 | if match:
98 | # Save previous section if exists
99 | if current_section:
100 | section_content = '\n'.join(current_content)
101 | sections.append(Section(
102 | name=current_section,
103 | content=section_content,
104 | priority=self._get_section_priority(current_section),
105 | start_pos=current_start,
106 | end_pos=i
107 | ))
108 |
109 | # Start new section
110 | current_section = match.group(2).lower().strip()
111 | current_content = [line]
112 | current_start = i
113 | elif current_section:
114 | current_content.append(line)
115 |
116 | # Don't forget the last section
117 | if current_section and current_content:
118 | section_content = '\n'.join(current_content)
119 | sections.append(Section(
120 | name=current_section,
121 | content=section_content,
122 | priority=self._get_section_priority(current_section),
123 | start_pos=current_start,
124 | end_pos=len(lines)
125 | ))
126 |
127 | # Sort sections by priority
128 | sections.sort(key=lambda s: s.priority)
129 |
130 | return sections
131 |
132 | def _get_section_priority(self, section_name: str) -> int:
133 | """Get priority for a section name."""
134 | section_lower = section_name.lower()
135 |
136 | # Check for exact matches first
137 | if section_lower in self.SECTION_PRIORITIES:
138 | return self.SECTION_PRIORITIES[section_lower]
139 |
140 | # Check for partial matches
141 | for key, priority in self.SECTION_PRIORITIES.items():
142 | if key in section_lower or section_lower in key:
143 | return priority
144 |
145 | # Default priority for unknown sections
146 | return 10
147 |
148 | def _simple_truncate(self, content: str, token_limit: int) -> str:
149 | """Simple truncation when no sections are detected."""
150 | char_limit = int(token_limit / self.TOKENS_PER_CHAR)
151 |
152 | if len(content) <= char_limit:
153 | return content
154 |
155 | # Try to truncate at a paragraph boundary
156 | truncated = content[:char_limit]
157 | last_para = truncated.rfind('\n\n')
158 |
159 | if last_para > char_limit * 0.8: # If we found a paragraph break in the last 20%
160 | return truncated[:last_para]
161 |
162 | # Otherwise truncate at last complete sentence
163 | last_period = truncated.rfind('. ')
164 | if last_period > char_limit * 0.8:
165 | return truncated[:last_period + 1]
166 |
167 | # Last resort: truncate at word boundary
168 | last_space = truncated.rfind(' ')
169 | if last_space > 0:
170 | return truncated[:last_space]
171 |
172 | return truncated
173 |
174 | def _priority_truncate(self, content: str, sections: List[Section], token_limit: int) -> str:
175 | """Truncate based on section priorities."""
176 | result_parts = []
177 | current_tokens = 0
178 |
179 | # First, try to get the content before any sections (usually the main description)
180 | lines = content.split('\n')
181 | pre_section_content = []
182 |
183 | for line in lines:
184 | if re.match(r'^#{2,4}\s+', line):
185 | break
186 | pre_section_content.append(line)
187 |
188 | if pre_section_content:
189 | pre_content = '\n'.join(pre_section_content).strip()
190 | if pre_content:
191 | pre_tokens = self._estimate_tokens(pre_content)
192 | if current_tokens + pre_tokens <= token_limit:
193 | result_parts.append(pre_content)
194 | current_tokens += pre_tokens
195 |
196 | # Add sections by priority
197 | for section in sections:
198 | section_tokens = self._estimate_tokens(section.content)
199 |
200 | if current_tokens + section_tokens <= token_limit:
201 | result_parts.append(section.content)
202 | current_tokens += section_tokens
203 | else:
204 | # Try to add at least part of this section
205 | remaining_tokens = token_limit - current_tokens
206 | if remaining_tokens > 100: # Only add if we have reasonable space
207 | partial = self._simple_truncate(section.content, remaining_tokens)
208 | if partial.strip():
209 | result_parts.append(partial)
210 | break
211 |
212 | return '\n\n'.join(result_parts)
213 |
214 | def _add_truncation_notice(self, content: str) -> str:
215 | """Add a notice that content was truncated."""
216 | if not content.endswith('\n'):
217 | content += '\n'
218 |
219 | notice = "\n---\n*Note: This documentation has been truncated to fit within token limits. " \
220 | "Some sections may have been omitted or shortened.*"
221 |
222 | return content + notice
223 |
224 |
225 | # Module-level instance for convenience
226 | _truncator = DocumentTruncator()
227 |
228 |
229 | def truncate_flutter_docs(
230 | content: str,
231 | class_name: str,
232 | max_tokens: int,
233 | strategy: str = "balanced"
234 | ) -> str:
235 | """
236 | Truncate Flutter documentation to fit within token limit.
237 |
238 | Args:
239 | content: The documentation content to truncate
240 | class_name: Name of the class (for context, not currently used)
241 | max_tokens: Maximum number of tokens allowed
242 | strategy: Truncation strategy (currently only "balanced" is supported)
243 |
244 | Returns:
245 | Truncated documentation content
246 | """
247 | return _truncator.truncate_to_limit(content, max_tokens)
248 |
249 |
250 | def create_truncator() -> DocumentTruncator:
251 | """
252 | Create a new DocumentTruncator instance.
253 |
254 | Returns:
255 | A new DocumentTruncator instance
256 | """
257 | return DocumentTruncator()
258 |
259 |
260 | # Example usage and testing
261 | if __name__ == "__main__":
262 | truncator = DocumentTruncator()
263 |
264 | # Test with sample Flutter documentation
265 | sample_content = """
266 | # Widget class
267 |
268 | A widget is an immutable description of part of a user interface.
269 |
270 | ## Description
271 |
272 | Widgets are the central class hierarchy in the Flutter framework. A widget is an
273 | immutable description of part of a user interface. Widgets can be inflated into
274 | elements, which manage the underlying render tree.
275 |
276 | ## Constructors
277 |
278 | ### Widget({Key? key})
279 |
280 | Initializes key for subclasses.
281 |
282 | ## Properties
283 |
284 | ### hashCode → int
285 | The hash code for this object.
286 |
287 | ### key → Key?
288 | Controls how one widget replaces another widget in the tree.
289 |
290 | ### runtimeType → Type
291 | A representation of the runtime type of the object.
292 |
293 | ## Methods
294 |
295 | ### createElement() → Element
296 | Inflates this configuration to a concrete instance.
297 |
298 | ### debugDescribeChildren() → List<DiagnosticsNode>
299 | Returns a list of DiagnosticsNode objects describing this node's children.
300 |
301 | ### debugFillProperties(DiagnosticPropertiesBuilder properties) → void
302 | Add additional properties associated with the node.
303 |
304 | ## Examples
305 |
306 | ```dart
307 | class MyWidget extends StatelessWidget {
308 | @override
309 | Widget build(BuildContext context) {
310 | return Container(
311 | child: Text('Hello World'),
312 | );
313 | }
314 | }
315 | ```
316 |
317 | ## See Also
318 |
319 | - StatelessWidget
320 | - StatefulWidget
321 | - InheritedWidget
322 | """
323 |
324 | # Test truncation
325 | truncated = truncator.truncate_to_limit(sample_content, 500)
326 | print("Original length:", len(sample_content))
327 | print("Truncated length:", len(truncated))
328 | print("\nTruncated content:")
329 | print(truncated)
```
--------------------------------------------------------------------------------
/tests/test_token_management.py:
--------------------------------------------------------------------------------
```python
1 | """Tests for token management functionality."""
2 |
3 | import pytest
4 | from unittest.mock import Mock, patch, MagicMock
5 | from flutter_mcp.token_manager import TokenManager
6 | from flutter_mcp.truncation import DocumentTruncator
7 | from flutter_mcp.server import process_documentation
8 | import json
9 |
10 |
11 | class TestTokenManager:
12 | """Test TokenManager functionality."""
13 |
14 | def test_approximate_tokens(self):
15 | """Test word-based token approximation."""
16 | manager = TokenManager()
17 |
18 | # Test various text samples
19 | assert manager.approximate_tokens("Hello world") == 2 # 2 words * 1.3 = 2.6 -> 2
20 | assert manager.approximate_tokens("The quick brown fox") == 5 # 4 * 1.3 = 5.2 -> 5
21 | assert manager.approximate_tokens("") == 0
22 | assert manager.approximate_tokens(" ") == 0
23 |
24 | # Test with punctuation and special characters
25 | text = "Hello, world! How are you?"
26 | expected = int(5 * 1.3) # 5 words
27 | assert manager.approximate_tokens(text) == expected
28 |
29 | def test_count_tokens_approximation(self):
30 | """Test count_tokens in approximation mode."""
31 | manager = TokenManager()
32 | manager.set_accurate_mode(False)
33 |
34 | text = "This is a test sentence with several words"
35 | tokens = manager.count_tokens(text)
36 | expected = int(8 * 1.3) # 8 words
37 | assert tokens == expected
38 |
39 | @patch('flutter_mcp.token_manager.tiktoken')
40 | def test_accurate_tokens(self, mock_tiktoken):
41 | """Test accurate token counting with tiktoken."""
42 | # Mock tiktoken encoder
43 | mock_encoder = Mock()
44 | mock_encoder.encode.return_value = [1, 2, 3, 4, 5] # 5 tokens
45 | mock_tiktoken.get_encoding.return_value = mock_encoder
46 |
47 | manager = TokenManager()
48 | tokens = manager.accurate_tokens("Test text")
49 |
50 | assert tokens == 5
51 | mock_tiktoken.get_encoding.assert_called_once_with("cl100k_base")
52 | mock_encoder.encode.assert_called_once_with("Test text")
53 |
54 | def test_mode_switching(self):
55 | """Test switching between accurate and approximation modes."""
56 | manager = TokenManager()
57 |
58 | # Default should be approximation
59 | assert manager.get_mode() == "approximation"
60 |
61 | # Switch to accurate
62 | manager.set_accurate_mode(True)
63 | assert manager.get_mode() == "accurate"
64 |
65 | # Switch back
66 | manager.set_accurate_mode(False)
67 | assert manager.get_mode() == "approximation"
68 |
69 |
70 | class TestDocumentTruncator:
71 | """Test DocumentTruncator functionality."""
72 |
73 | def test_detect_sections(self):
74 | """Test markdown section detection."""
75 | truncator = DocumentTruncator()
76 |
77 | content = """# Widget
78 |
79 | ## Description
80 | This is a description.
81 |
82 | ## Constructors
83 | Here are constructors.
84 |
85 | ## Properties
86 | Some properties.
87 |
88 | ## Methods
89 | Various methods.
90 |
91 | ## Examples
92 | Code examples here.
93 | """
94 | sections = truncator._detect_sections(content)
95 |
96 | assert len(sections) == 6 # Including pre-section content
97 | assert "Description" in sections
98 | assert "Constructors" in sections
99 | assert "Properties" in sections
100 | assert "Methods" in sections
101 | assert "Examples" in sections
102 |
103 | def test_truncate_no_truncation_needed(self):
104 | """Test that content within limit is not truncated."""
105 | truncator = DocumentTruncator()
106 |
107 | content = "Short content"
108 | result = truncator.truncate_to_limit(content, 10000)
109 |
110 | assert result == content # Should be unchanged
111 |
112 | def test_simple_truncation(self):
113 | """Test simple truncation when no sections detected."""
114 | truncator = DocumentTruncator()
115 |
116 | # Create content that will exceed token limit
117 | content = "word " * 1000 # 5000 characters, ~1250 tokens
118 | result = truncator.truncate_to_limit(content, 100) # Very low limit
119 |
120 | assert len(result) < len(content)
121 | assert result.endswith("...")
122 | assert "\n\n---\n*Note: Documentation has been truncated" not in result # Simple truncation
123 |
124 | def test_section_based_truncation(self):
125 | """Test priority-based section truncation."""
126 | truncator = DocumentTruncator()
127 |
128 | content = """# Widget
129 |
130 | ## Description
131 | This is a very important description that should be kept.
132 |
133 | ## Constructors
134 | Constructor information here.
135 |
136 | ## Properties
137 | Property details.
138 |
139 | ## Methods
140 | Method information.
141 |
142 | ## Examples
143 | """ + "Example code " * 100 + """
144 |
145 | ## See Also
146 | References and links.
147 | """
148 |
149 | # Truncate to a limit that should keep only high-priority sections
150 | result = truncator.truncate_to_limit(content, 200) # ~800 characters
151 |
152 | assert "Description" in result
153 | assert "very important description" in result
154 | assert "See Also" not in result # Low priority
155 | assert "*Note: Documentation has been truncated" in result
156 |
157 |
158 | class TestServerIntegration:
159 | """Test token management integration with server."""
160 |
161 | @patch('flutter_mcp.server.token_manager')
162 | @patch('flutter_mcp.server.truncator')
163 | def test_process_documentation_with_tokens(self, mock_truncator, mock_token_manager):
164 | """Test process_documentation with token limit."""
165 | # Mock token counting
166 | mock_token_manager.count_tokens.side_effect = [5000, 2000] # Before and after truncation
167 |
168 | # Mock truncation
169 | mock_truncator.truncate_to_limit.return_value = "Truncated content"
170 |
171 | # Mock BeautifulSoup
172 | with patch('flutter_mcp.server.BeautifulSoup') as mock_bs:
173 | mock_soup = Mock()
174 | mock_soup.find.return_value = None
175 | mock_soup.find_all.return_value = []
176 | mock_bs.return_value = mock_soup
177 |
178 | result = process_documentation("<html></html>", "TestClass", tokens=2000)
179 |
180 | assert isinstance(result, dict)
181 | assert result["content"] == "Truncated content"
182 | assert result["token_count"] == 2000
183 | assert result["original_tokens"] == 5000
184 | assert result["truncated"] == True
185 | assert "truncation_note" in result
186 |
187 | # Verify truncation was called with correct parameters
188 | mock_truncator.truncate_to_limit.assert_called_once()
189 | args = mock_truncator.truncate_to_limit.call_args[0]
190 | assert args[1] == 2000 # Token limit
191 |
192 | @patch('flutter_mcp.server.token_manager')
193 | def test_process_documentation_without_truncation(self, mock_token_manager):
194 | """Test process_documentation when no truncation needed."""
195 | # Mock token counting - content fits within limit
196 | mock_token_manager.count_tokens.return_value = 1000
197 |
198 | # Mock BeautifulSoup
199 | with patch('flutter_mcp.server.BeautifulSoup') as mock_bs:
200 | mock_soup = Mock()
201 | mock_soup.find.return_value = None
202 | mock_soup.find_all.return_value = []
203 | mock_bs.return_value = mock_soup
204 |
205 | result = process_documentation("<html></html>", "TestClass", tokens=5000)
206 |
207 | assert isinstance(result, dict)
208 | assert result["token_count"] == 1000
209 | assert result["original_tokens"] == 1000
210 | assert result["truncated"] == False
211 | assert result["truncation_note"] == ""
212 |
213 |
214 | class TestCacheIntegration:
215 | """Test token count storage in cache."""
216 |
217 | def test_cache_with_token_count(self):
218 | """Test that cache stores and retrieves token counts."""
219 | from flutter_mcp.cache import SQLiteCache
220 | import tempfile
221 | import os
222 |
223 | # Create temporary cache
224 | with tempfile.TemporaryDirectory() as temp_dir:
225 | cache_path = os.path.join(temp_dir, "test_cache.db")
226 | cache = SQLiteCache(cache_path)
227 |
228 | # Test data with token count
229 | test_data = {
230 | "content": "Test content",
231 | "_cached_token_count": 42
232 | }
233 |
234 | # Store in cache
235 | cache.set("test_key", test_data, ttl=3600, token_count=42)
236 |
237 | # Retrieve from cache
238 | result = cache.get("test_key")
239 |
240 | assert result is not None
241 | assert result["content"] == "Test content"
242 | assert result["_cached_token_count"] == 42
243 |
244 | # Test statistics
245 | stats = cache.get_stats()
246 | assert stats["entries_with_token_counts"] == 1
247 | assert stats["total_cached_tokens"] == 42
248 |
249 | def test_cache_backward_compatibility(self):
250 | """Test that old cache entries without token counts still work."""
251 | from flutter_mcp.cache import SQLiteCache
252 | import tempfile
253 | import os
254 | import sqlite3
255 |
256 | # Create temporary cache
257 | with tempfile.TemporaryDirectory() as temp_dir:
258 | cache_path = os.path.join(temp_dir, "test_cache.db")
259 |
260 | # Manually create old-style cache entry
261 | conn = sqlite3.connect(cache_path)
262 | conn.execute("""
263 | CREATE TABLE IF NOT EXISTS doc_cache (
264 | key TEXT PRIMARY KEY NOT NULL,
265 | value TEXT NOT NULL,
266 | created_at INTEGER NOT NULL,
267 | expires_at INTEGER NOT NULL
268 | )
269 | """)
270 |
271 | import time
272 | import json
273 | now = int(time.time())
274 | conn.execute(
275 | "INSERT INTO doc_cache (key, value, created_at, expires_at) VALUES (?, ?, ?, ?)",
276 | ("old_key", json.dumps({"content": "Old content"}), now, now + 3600)
277 | )
278 | conn.commit()
279 | conn.close()
280 |
281 | # Now use the cache (should migrate schema)
282 | cache = SQLiteCache(cache_path)
283 |
284 | # Should be able to retrieve old entry
285 | result = cache.get("old_key")
286 | assert result is not None
287 | assert result["content"] == "Old content"
288 | assert "_cached_token_count" not in result # No token count for old entry
289 |
290 | # Should be able to add new entry with token count
291 | cache.set("new_key", {"content": "New content"}, ttl=3600, token_count=100)
292 |
293 | # Verify both entries exist
294 | stats = cache.get_stats()
295 | assert stats["total_entries"] == 2
296 | assert stats["entries_with_token_counts"] == 1
297 | assert stats["total_cached_tokens"] == 100
298 |
299 |
300 | if __name__ == "__main__":
301 | pytest.main([__file__, "-v"])
```
--------------------------------------------------------------------------------
/docs/TOOL-CONSOLIDATION-PLAN.md:
--------------------------------------------------------------------------------
```markdown
1 | # Tool Consolidation Implementation Plan
2 |
3 | ## Overview
4 |
5 | This document outlines the plan to consolidate Flutter MCP's 5 tools into 2-3 simplified tools, following Context7's successful pattern. This will make the server easier to use for AI assistants while maintaining all functionality.
6 |
7 | ## Goals
8 |
9 | 1. **Simplify from 5 tools to 2-3 tools**
10 | 2. **Match Context7's UX patterns**
11 | 3. **Add token limiting (10,000 default)**
12 | 4. **Maintain backwards compatibility**
13 | 5. **Improve discoverability**
14 |
15 | ## Current State (5 Tools)
16 |
17 | ```python
18 | 1. get_flutter_docs(class_name, library, max_tokens)
19 | 2. search_flutter_docs(query)
20 | 3. get_pub_package_info(package_name)
21 | 4. process_flutter_mentions(text)
22 | 5. health_check()
23 | ```
24 |
25 | ## Target State (2 Primary Tools + 1 Optional)
26 |
27 | ```python
28 | 1. flutter_search(query, limit=10)
29 | 2. flutter_docs(identifier, topic=None, max_tokens=10000)
30 | 3. flutter_status() [optional]
31 | ```
32 |
33 | ## Implementation Timeline
34 |
35 | ### Phase 1: Core Implementation (Current)
36 |
37 | - [x] Create unified `flutter_search` tool
38 | - [x] Create unified `flutter_docs` tool
39 | - [x] Add backwards compatibility layer
40 | - [ ] Update tests
41 | - [ ] Fix duplicate implementations
42 | - [ ] Handle process_flutter_mentions
43 |
44 | ### Phase 2: Polish & Documentation
45 |
46 | - [ ] Update README and examples
47 | - [ ] Add deprecation warnings
48 | - [ ] Update npm wrapper
49 | - [ ] Create migration guide
50 |
51 | ### Phase 3: Cleanup (Future)
52 |
53 | - [ ] Remove deprecated tools
54 | - [ ] Optimize performance
55 | - [ ] Add natural language activation
56 |
57 | ## Tool Specifications
58 |
59 | ### 1. flutter_search
60 |
61 | **Purpose**: Universal search across Flutter/Dart ecosystem
62 |
63 | **Input**:
64 | ```python
65 | query: str # Natural language or identifier
66 | limit: int = 10 # Max results to return
67 | ```
68 |
69 | **Output**:
70 | ```json
71 | {
72 | "query": "state management",
73 | "results": [
74 | {
75 | "id": "pub:provider",
76 | "type": "package",
77 | "title": "provider",
78 | "description": "State management library",
79 | "relevance": 0.95,
80 | "doc_size": "large"
81 | },
82 | {
83 | "id": "flutter:widgets.StatefulWidget",
84 | "type": "class",
85 | "title": "StatefulWidget",
86 | "description": "Widget with mutable state",
87 | "relevance": 0.87,
88 | "doc_size": "medium"
89 | }
90 | ],
91 | "metadata": {
92 | "total_results": 25,
93 | "search_time_ms": 145
94 | }
95 | }
96 | ```
97 |
98 | ### 2. flutter_docs
99 |
100 | **Purpose**: Fetch documentation with smart resolution and filtering
101 |
102 | **Input**:
103 | ```python
104 | identifier: str # "Container", "pub:dio", "dart:async.Future"
105 | topic: Optional[str] # "examples", "constructors", "getting-started"
106 | max_tokens: int = 10000 # Token limit for response
107 | ```
108 |
109 | **Output**:
110 | ```json
111 | {
112 | "identifier": "provider",
113 | "type": "package",
114 | "content": "# provider\n\n...",
115 | "metadata": {
116 | "source": "live",
117 | "tokens": 8234,
118 | "truncated": false,
119 | "version": "6.1.2"
120 | }
121 | }
122 | ```
123 |
124 | ### 3. flutter_status
125 |
126 | **Purpose**: Health check and cache statistics
127 |
128 | **Output**:
129 | ```json
130 | {
131 | "status": "healthy",
132 | "cache": {
133 | "entries": 42,
134 | "size_mb": 12.3,
135 | "hit_rate": 0.87
136 | },
137 | "scrapers": {
138 | "flutter_docs": "operational",
139 | "pub_dev": "operational"
140 | }
141 | }
142 | ```
143 |
144 | ## Implementation Details
145 |
146 | ### Identifier Resolution
147 |
148 | ```python
149 | # Unified identifier format:
150 | "Container" # Flutter widget (auto-detect)
151 | "material.AppBar" # Library-qualified
152 | "pub:provider" # Explicit package
153 | "dart:async.Future" # Dart core library
154 | "provider" # Auto-detect as package
155 | ```
156 |
157 | ### Topic Filtering
158 |
159 | ```python
160 | # Class topics:
161 | "constructors", "properties", "methods", "examples", "summary"
162 |
163 | # Package topics:
164 | "getting-started", "examples", "api", "changelog", "installation"
165 | ```
166 |
167 | ### Smart Truncation
168 |
169 | - Default: 10,000 tokens
170 | - Preserves section structure
171 | - Prioritizes important content
172 | - Indicates truncation in response
173 |
174 | ## Backwards Compatibility
175 |
176 | Existing tools will be maintained but marked as deprecated:
177 |
178 | ```python
179 | @mcp.tool()
180 | async def get_flutter_docs(class_name, library="widgets", max_tokens=None):
181 | """[Deprecated] Use flutter_docs instead."""
182 | # Internally calls flutter_docs
183 | ```
184 |
185 | ## Migration Guide for Users
186 |
187 | ### Before:
188 | ```python
189 | # Searching
190 | results = await search_flutter_docs("state management")
191 |
192 | # Getting docs
193 | doc = await get_flutter_docs("Container", "widgets")
194 | ```
195 |
196 | ### After:
197 | ```python
198 | # Searching
199 | results = await flutter_search("state management")
200 |
201 | # Getting docs
202 | doc = await flutter_docs("Container")
203 | doc = await flutter_docs("widgets.Container", topic="examples")
204 | ```
205 |
206 | ## Success Metrics
207 |
208 | 1. **Reduced complexity**: 5 tools → 2 tools (60% reduction)
209 | 2. **Improved discoverability**: Natural language queries
210 | 3. **Better token management**: Default limits prevent overflow
211 | 4. **Backwards compatible**: No breaking changes
212 |
213 | ## Status Updates
214 |
215 | ### 2025-06-27 - Started Implementation
216 | - Created implementation plan
217 | - Beginning tool development
218 |
219 | ### 2025-06-28 - Implementation Progress
220 |
221 | #### What's Been Implemented
222 |
223 | 1. **New Unified Tools Created**:
224 | - ✅ `flutter_docs()` - Unified documentation fetching tool (lines 571-683 and 1460+)
225 | - ✅ `flutter_search()` - Enhanced search tool (lines 802-836 and 2342+)
226 | - ✅ `flutter_status()` - Health check tool (line 2773+)
227 |
228 | 2. **Backwards Compatibility Layer**:
229 | - ✅ Deprecated tools now call new implementations internally
230 | - ✅ `get_flutter_docs()` wrapper implemented (lines 686-727)
231 | - ✅ `search_flutter_docs()` wrapper implemented (lines 840-869)
232 | - ✅ Deprecation warnings added via logger
233 |
234 | 3. **Helper Functions Added**:
235 | - ✅ `resolve_identifier()` - Smart identifier type detection (lines 100-150)
236 | - ✅ `filter_by_topic()` - Topic-based content filtering (lines 153-236)
237 | - ✅ `to_unified_id()` - Convert to unified ID format (lines 239-265)
238 | - ✅ `from_unified_id()` - Parse unified ID format (lines 267-296)
239 | - ✅ `estimate_doc_size()` - Document size estimation (lines 298-320)
240 | - ✅ `rank_results()` - Result relevance ranking (lines 322-370)
241 |
242 | #### Current Issues Found
243 |
244 | 1. **Duplicate Implementations**:
245 | - ⚠️ `flutter_docs()` defined twice (lines 571 and 1460)
246 | - ⚠️ `flutter_search()` defined twice (lines 802 and 2342)
247 | - Need to remove duplicate definitions and consolidate
248 |
249 | 2. **Missing Implementations**:
250 | - ❌ `process_flutter_mentions()` not yet integrated into new tools
251 | - ❌ Topic filtering not fully integrated into `flutter_docs()`
252 | - ❌ Version-specific queries not fully implemented
253 |
254 | 3. **Test Coverage**:
255 | - ❌ Tests still use old tool names (test_tools.py)
256 | - ❌ No tests for new unified tools yet
257 | - ❌ No migration examples in tests
258 |
259 | #### Examples of New Tool Usage
260 |
261 | ##### 1. flutter_docs - Unified Documentation
262 |
263 | ```python
264 | # Old way (deprecated)
265 | doc = await get_flutter_docs("Container", "widgets")
266 |
267 | # New way - auto-detection
268 | doc = await flutter_docs("Container")
269 |
270 | # New way - with library
271 | doc = await flutter_docs("material.AppBar")
272 |
273 | # New way - Dart core library
274 | doc = await flutter_docs("dart:async.Future")
275 |
276 | # New way - Pub package
277 | doc = await flutter_docs("pub:provider")
278 |
279 | # New way - with topic filtering (when implemented)
280 | doc = await flutter_docs("Container", topic="constructors")
281 | doc = await flutter_docs("pub:dio", topic="examples")
282 | ```
283 |
284 | ##### 2. flutter_search - Enhanced Search
285 |
286 | ```python
287 | # Old way (deprecated)
288 | results = await search_flutter_docs("state management")
289 |
290 | # New way - basic search
291 | results = await flutter_search("state management")
292 |
293 | # New way - with limit
294 | results = await flutter_search("navigation", limit=5)
295 |
296 | # New way - with type filtering (when implemented)
297 | results = await flutter_search("http", types=["package"])
298 | results = await flutter_search("widget", types=["flutter", "dart"])
299 | ```
300 |
301 | ##### 3. flutter_status - Health Check
302 |
303 | ```python
304 | # New tool (no old equivalent)
305 | status = await flutter_status()
306 | # Returns:
307 | # {
308 | # "status": "healthy",
309 | # "cache": {
310 | # "entries": 42,
311 | # "size_mb": 12.3,
312 | # "hit_rate": 0.87
313 | # },
314 | # "scrapers": {
315 | # "flutter_docs": "operational",
316 | # "pub_dev": "operational"
317 | # }
318 | # }
319 | ```
320 |
321 | #### Migration Examples for Users
322 |
323 | ##### Basic Migration
324 |
325 | ```python
326 | # Before: Multiple tools with different interfaces
327 | doc1 = await get_flutter_docs("Container", "widgets")
328 | doc2 = await get_pub_package_info("provider")
329 | search = await search_flutter_docs("navigation")
330 |
331 | # After: Unified interface
332 | doc1 = await flutter_docs("Container")
333 | doc2 = await flutter_docs("pub:provider")
334 | search = await flutter_search("navigation")
335 | ```
336 |
337 | ##### Advanced Migration
338 |
339 | ```python
340 | # Before: Manual library specification
341 | material_doc = await get_flutter_docs("AppBar", "material")
342 | dart_doc = await get_flutter_docs("Future", "dart:async") # Didn't work well
343 |
344 | # After: Smart resolution
345 | material_doc = await flutter_docs("material.AppBar")
346 | dart_doc = await flutter_docs("dart:async.Future")
347 |
348 | # Before: Limited search
349 | results = await search_flutter_docs("state") # Only 5-10 results
350 |
351 | # After: Configurable search
352 | results = await flutter_search("state", limit=20)
353 | # Future: Type filtering
354 | results = await flutter_search("state", types=["package", "concept"])
355 | ```
356 |
357 | #### Challenges and Decisions Made
358 |
359 | 1. **Identifier Resolution**:
360 | - Decision: Use prefixes for explicit types (`pub:`, `dart:`)
361 | - Challenge: Auto-detecting Flutter vs Dart classes
362 | - Solution: Common widget list + library patterns
363 |
364 | 2. **Backwards Compatibility**:
365 | - Decision: Keep old tools as wrappers
366 | - Challenge: Transforming response formats
367 | - Solution: Internal `_impl` functions + format converters
368 |
369 | 3. **Topic Filtering**:
370 | - Decision: Optional `topic` parameter
371 | - Challenge: Different topics for classes vs packages
372 | - Solution: Type-aware topic extraction
373 |
374 | 4. **Duplicate Code**:
375 | - Issue: Functions defined multiple times
376 | - Cause: Possibly from incremental development
377 | - Action Needed: Clean up and consolidate
378 |
379 | #### Next Steps
380 |
381 | 1. **Immediate Actions**:
382 | - Remove duplicate function definitions
383 | - Implement topic filtering in `flutter_docs()`
384 | - Update tests to use new tools
385 | - Fix `process_flutter_mentions` integration
386 |
387 | 2. **Documentation Updates**:
388 | - Update README with new tool examples
389 | - Create migration guide
390 | - Update npm wrapper documentation
391 |
392 | 3. **Future Enhancements**:
393 | - Natural language activation patterns
394 | - Version-specific documentation
395 | - Cookbook recipe integration
396 | - Stack Overflow integration
397 |
398 | ### 2025-06-28 - Implementation Complete! 🎉
399 |
400 | #### Final Status:
401 |
402 | 1. **All Duplicate Implementations Fixed**:
403 | - Removed duplicate `flutter_docs` (kept the more complete one)
404 | - Removed duplicate `flutter_search` (kept the one with parallel search)
405 | - Cleaned up code structure
406 |
407 | 2. **Tool Consolidation Complete**:
408 | - **2 Primary Tools**: `flutter_search` and `flutter_docs`
409 | - **1 Optional Tool**: `flutter_status`
410 | - **All 5 original tools** maintained with deprecation notices
411 | - `process_flutter_mentions` updated to use new tools internally
412 |
413 | 3. **Key Features Implemented**:
414 | - ✅ Smart identifier resolution (auto-detects type)
415 | - ✅ Topic filtering for focused documentation
416 | - ✅ Token limiting with default 10,000
417 | - ✅ Parallel search across multiple sources
418 | - ✅ Unified ID format (flutter:, dart:, pub:, concept:)
419 | - ✅ Backwards compatibility maintained
420 |
421 | 4. **Next Steps**:
422 | - Update tests for new tool signatures
423 | - Update main README with examples
424 | - Monitor usage and gather feedback
425 | - Consider removing deprecated tools in v2.0
426 |
427 | ---
428 |
429 | *Implementation completed on 2025-06-28. This document serves as the record of the tool consolidation effort.*
```
--------------------------------------------------------------------------------
/docs/api-reference.md:
--------------------------------------------------------------------------------
```markdown
1 | # Flutter MCP Server API Reference
2 |
3 | ## Overview
4 |
5 | The Flutter MCP (Model Context Protocol) server provides AI assistants with seamless access to Flutter, Dart, and pub.dev documentation through on-demand web scraping with Redis caching. This ensures you always get the most current documentation while maintaining fast response times.
6 |
7 | ## Activation
8 |
9 | To use the Flutter MCP tools, mention `@flutter_mcp` in your prompt. This activates the Flutter documentation context and makes the following tools available.
10 |
11 | ## Rate Limiting
12 |
13 | All tools respect a rate limit of **2 requests per second** to ensure responsible use of documentation servers. Requests exceeding this limit will be queued automatically.
14 |
15 | ## Caching Behavior
16 |
17 | - **Flutter/Dart API Documentation**: Cached for 24 hours
18 | - **Pub.dev Package Documentation**: Cached for 12 hours
19 | - **Search Results**: Cached for 1 hour
20 |
21 | Cached responses are stored in Redis with appropriate TTLs to balance freshness with performance.
22 |
23 | ## Available Tools
24 |
25 | ### 1. get_flutter_docs
26 |
27 | Retrieves documentation for Flutter and Dart classes, methods, properties, and constructors.
28 |
29 | #### Description
30 | Fetches and processes documentation from api.flutter.dev and api.dart.dev, providing clean, well-formatted documentation with examples and usage information.
31 |
32 | #### Parameters
33 |
34 | | Parameter | Type | Required | Description |
35 | |-----------|------|----------|-------------|
36 | | `query` | string | Yes | The Flutter/Dart API element to search for (e.g., "Container", "setState", "Future.wait") |
37 |
38 | #### Return Format
39 |
40 | ```json
41 | {
42 | "content": "string", // Formatted documentation content
43 | "source_url": "string", // URL of the documentation page
44 | "cached": "boolean" // Whether response was served from cache
45 | }
46 | ```
47 |
48 | #### Example Requests
49 |
50 | **Basic Widget Documentation:**
51 | ```json
52 | {
53 | "query": "Container"
54 | }
55 | ```
56 |
57 | **Method Documentation:**
58 | ```json
59 | {
60 | "query": "setState"
61 | }
62 | ```
63 |
64 | **Dart Core Library:**
65 | ```json
66 | {
67 | "query": "Future.wait"
68 | }
69 | ```
70 |
71 | #### Example Responses
72 |
73 | **Success Response:**
74 | ```json
75 | {
76 | "content": "# Container class\n\nA convenience widget that combines common painting, positioning, and sizing widgets.\n\n## Description\n\nA container first surrounds the child with padding (inflated by any borders present in the decoration) and then applies additional constraints to the padded extent...\n\n## Example\n\n```dart\nContainer(\n margin: const EdgeInsets.all(10.0),\n color: Colors.amber[600],\n width: 48.0,\n height: 48.0,\n)\n```",
77 | "source_url": "https://api.flutter.dev/flutter/widgets/Container-class.html",
78 | "cached": false
79 | }
80 | ```
81 |
82 | **Error Response:**
83 | ```json
84 | {
85 | "error": "Documentation not found for 'NonExistentWidget'. Try searching for a valid Flutter/Dart API element."
86 | }
87 | ```
88 |
89 | #### Common Use Cases
90 |
91 | 1. **Widget Documentation**: Get detailed information about Flutter widgets
92 | - `Container`, `Column`, `Row`, `Scaffold`, `AppBar`
93 |
94 | 2. **State Management**: Understanding Flutter's state management APIs
95 | - `setState`, `StatefulWidget`, `State`, `InheritedWidget`
96 |
97 | 3. **Dart Core Libraries**: Access Dart language features
98 | - `Future`, `Stream`, `List`, `Map`, `String`
99 |
100 | 4. **Material Design Components**: Material Design widgets
101 | - `MaterialApp`, `ThemeData`, `IconButton`, `TextField`
102 |
103 | ### 2. get_pub_package_docs
104 |
105 | Retrieves documentation for packages published on pub.dev.
106 |
107 | #### Description
108 | Fetches package information, README content, and metadata from pub.dev using the official API, providing comprehensive package documentation.
109 |
110 | #### Parameters
111 |
112 | | Parameter | Type | Required | Description |
113 | |-----------|------|----------|-------------|
114 | | `package_name` | string | Yes | The exact name of the package on pub.dev |
115 |
116 | #### Return Format
117 |
118 | ```json
119 | {
120 | "content": "string", // Formatted package documentation
121 | "metadata": {
122 | "name": "string",
123 | "version": "string",
124 | "description": "string",
125 | "homepage": "string",
126 | "repository": "string",
127 | "documentation": "string",
128 | "pub_url": "string"
129 | },
130 | "cached": "boolean"
131 | }
132 | ```
133 |
134 | #### Example Requests
135 |
136 | **Popular Package:**
137 | ```json
138 | {
139 | "package_name": "provider"
140 | }
141 | ```
142 |
143 | **HTTP Client Package:**
144 | ```json
145 | {
146 | "package_name": "dio"
147 | }
148 | ```
149 |
150 | #### Example Responses
151 |
152 | **Success Response:**
153 | ```json
154 | {
155 | "content": "# provider\n\nA wrapper around InheritedWidget to make them easier to use and more reusable.\n\n## Features\n\n- Simplifies InheritedWidget usage\n- Supports multiple providers\n- Lazy loading of values...\n\n## Getting started\n\n```yaml\ndependencies:\n provider: ^6.1.0\n```\n\n## Usage\n\n```dart\nimport 'package:provider/provider.dart';\n\nvoid main() {\n runApp(\n ChangeNotifierProvider(\n create: (context) => Counter(),\n child: MyApp(),\n ),\n );\n}\n```",
156 | "metadata": {
157 | "name": "provider",
158 | "version": "6.1.0",
159 | "description": "A wrapper around InheritedWidget to make them easier to use and more reusable.",
160 | "homepage": "https://github.com/rrousselGit/provider",
161 | "repository": "https://github.com/rrousselGit/provider",
162 | "documentation": "https://pub.dev/documentation/provider/latest/",
163 | "pub_url": "https://pub.dev/packages/provider"
164 | },
165 | "cached": false
166 | }
167 | ```
168 |
169 | **Package Not Found:**
170 | ```json
171 | {
172 | "error": "Package 'non-existent-package' not found on pub.dev"
173 | }
174 | ```
175 |
176 | #### Common Use Cases
177 |
178 | 1. **State Management Packages**:
179 | - `provider`, `riverpod`, `bloc`, `getx`, `mobx`
180 |
181 | 2. **HTTP/Networking**:
182 | - `dio`, `http`, `retrofit`, `chopper`
183 |
184 | 3. **Database/Storage**:
185 | - `sqflite`, `hive`, `shared_preferences`, `path_provider`
186 |
187 | 4. **UI/Animation**:
188 | - `animations`, `lottie`, `shimmer`, `cached_network_image`
189 |
190 | ### 3. search_pub_packages
191 |
192 | Searches for packages on pub.dev based on keywords or functionality.
193 |
194 | #### Description
195 | Searches pub.dev for packages matching your query, returning a list of relevant packages with their descriptions and metadata. Useful for discovering packages for specific functionality.
196 |
197 | #### Parameters
198 |
199 | | Parameter | Type | Required | Description |
200 | |-----------|------|----------|-------------|
201 | | `query` | string | Yes | Search terms to find packages (e.g., "state management", "http client", "animations") |
202 | | `max_results` | integer | No | Maximum number of results to return (default: 10, max: 30) |
203 |
204 | #### Return Format
205 |
206 | ```json
207 | {
208 | "results": [
209 | {
210 | "name": "string",
211 | "description": "string",
212 | "version": "string",
213 | "publisher": "string",
214 | "score": "number",
215 | "url": "string"
216 | }
217 | ],
218 | "total_found": "integer",
219 | "cached": "boolean"
220 | }
221 | ```
222 |
223 | #### Example Requests
224 |
225 | **Search for State Management:**
226 | ```json
227 | {
228 | "query": "state management",
229 | "max_results": 5
230 | }
231 | ```
232 |
233 | **Search for HTTP Packages:**
234 | ```json
235 | {
236 | "query": "http client"
237 | }
238 | ```
239 |
240 | #### Example Responses
241 |
242 | **Success Response:**
243 | ```json
244 | {
245 | "results": [
246 | {
247 | "name": "provider",
248 | "description": "A wrapper around InheritedWidget to make them easier to use and more reusable.",
249 | "version": "6.1.0",
250 | "publisher": "flutter.dev",
251 | "score": 99,
252 | "url": "https://pub.dev/packages/provider"
253 | },
254 | {
255 | "name": "riverpod",
256 | "description": "A simple way to access state while robust and testable.",
257 | "version": "2.4.0",
258 | "publisher": null,
259 | "score": 96,
260 | "url": "https://pub.dev/packages/riverpod"
261 | }
262 | ],
263 | "total_found": 156,
264 | "cached": false
265 | }
266 | ```
267 |
268 | **No Results:**
269 | ```json
270 | {
271 | "results": [],
272 | "total_found": 0,
273 | "cached": false,
274 | "message": "No packages found matching 'very-specific-nonexistent-query'"
275 | }
276 | ```
277 |
278 | #### Common Use Cases
279 |
280 | 1. **Finding Alternatives**: Search for packages providing similar functionality
281 | - "state management" → provider, bloc, riverpod, getx
282 | - "navigation" → go_router, auto_route, beamer
283 |
284 | 2. **Specific Features**: Search for packages implementing specific features
285 | - "camera", "barcode scanner", "pdf viewer", "charts"
286 |
287 | 3. **Platform Integration**: Find packages for platform-specific features
288 | - "firebase", "google maps", "notifications", "biometric"
289 |
290 | ## Error Handling
291 |
292 | All tools implement consistent error handling:
293 |
294 | ### Common Error Scenarios
295 |
296 | 1. **Network Errors**
297 | ```json
298 | {
299 | "error": "Failed to fetch documentation: Network timeout"
300 | }
301 | ```
302 |
303 | 2. **Not Found Errors**
304 | ```json
305 | {
306 | "error": "Documentation not found for 'InvalidQuery'. Please check the spelling and try again."
307 | }
308 | ```
309 |
310 | 3. **Rate Limit Errors**
311 | ```json
312 | {
313 | "error": "Rate limit exceeded. Please wait before making another request."
314 | }
315 | ```
316 |
317 | 4. **Invalid Parameters**
318 | ```json
319 | {
320 | "error": "Invalid parameter: max_results must be between 1 and 30"
321 | }
322 | ```
323 |
324 | ## Best Practices
325 |
326 | ### 1. Efficient Querying
327 | - Use specific class/method names for `get_flutter_docs`
328 | - Use exact package names for `get_pub_package_docs`
329 | - Use descriptive keywords for `search_pub_packages`
330 |
331 | ### 2. Cache Utilization
332 | - Repeated requests for the same documentation will be served from cache
333 | - Cache TTLs ensure documentation stays relatively fresh
334 | - No need to manually refresh unless you suspect documentation has just been updated
335 |
336 | ### 3. Error Recovery
337 | - If a query fails, check spelling and formatting
338 | - For Flutter/Dart docs, try both class name and full method signature
339 | - For packages, ensure the exact package name from pub.dev
340 |
341 | ### 4. Rate Limit Compliance
342 | - The server automatically queues requests to respect rate limits
343 | - Avoid making rapid successive requests for different items
344 | - Batch related queries when possible
345 |
346 | ## Integration Examples
347 |
348 | ### Example 1: Getting Widget Documentation
349 | ```python
350 | # User prompt: "@flutter_mcp Show me how to use Container widget"
351 |
352 | # MCP tool call
353 | {
354 | "tool": "get_flutter_docs",
355 | "parameters": {
356 | "query": "Container"
357 | }
358 | }
359 |
360 | # Returns comprehensive Container documentation with examples
361 | ```
362 |
363 | ### Example 2: Finding and Learning About Packages
364 | ```python
365 | # User prompt: "@flutter_mcp I need a good HTTP client package"
366 |
367 | # First, search for packages
368 | {
369 | "tool": "search_pub_packages",
370 | "parameters": {
371 | "query": "http client",
372 | "max_results": 5
373 | }
374 | }
375 |
376 | # Then get detailed docs for a specific package
377 | {
378 | "tool": "get_pub_package_docs",
379 | "parameters": {
380 | "package_name": "dio"
381 | }
382 | }
383 | ```
384 |
385 | ### Example 3: Learning Flutter Concepts
386 | ```python
387 | # User prompt: "@flutter_mcp Explain setState and state management"
388 |
389 | # Get setState documentation
390 | {
391 | "tool": "get_flutter_docs",
392 | "parameters": {
393 | "query": "setState"
394 | }
395 | }
396 |
397 | # Search for state management packages
398 | {
399 | "tool": "search_pub_packages",
400 | "parameters": {
401 | "query": "state management"
402 | }
403 | }
404 | ```
405 |
406 | ## Troubleshooting
407 |
408 | ### Documentation Not Found
409 | - Verify the exact class/method name from Flutter documentation
410 | - Try variations (e.g., "setState" vs "State.setState")
411 | - Check if it's a Dart core library element (e.g., "Future" instead of "flutter.Future")
412 |
413 | ### Package Not Found
414 | - Ensure exact package name as it appears on pub.dev
415 | - Use search_pub_packages first to find the correct name
416 | - Package names are case-sensitive
417 |
418 | ### Slow Responses
419 | - First request for documentation may take longer (not cached)
420 | - Subsequent requests should be faster (served from cache)
421 | - Check Redis connection if consistently slow
422 |
423 | ### Incomplete Documentation
424 | - Some newer APIs may have limited documentation
425 | - Third-party package docs depend on package author's README
426 | - Consider checking the source repository for more details
427 |
428 | ## Version Information
429 |
430 | - **API Version**: 1.0.0
431 | - **Supported Flutter Docs**: Latest stable version
432 | - **Supported Dart Docs**: Latest stable version
433 | - **Pub.dev API**: v2
434 |
435 | ## Support
436 |
437 | For issues, feature requests, or contributions:
438 | - GitHub: [flutter-docs-mcp](https://github.com/yourusername/flutter-docs-mcp)
439 | - Issues: Report bugs or request features via GitHub Issues
440 | - PRs: Contributions welcome following the project guidelines
```
--------------------------------------------------------------------------------
/examples/token_management_demo.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | Token Management Demo for Flutter MCP
4 |
5 | This example demonstrates how token management works in Flutter MCP,
6 | showing both approximation and truncation features.
7 | """
8 |
9 | import asyncio
10 | import json
11 | from flutter_mcp.token_manager import TokenManager, count_tokens, get_mode
12 | from flutter_mcp.truncation import DocumentTruncator
13 |
14 |
15 | def print_section(title):
16 | """Print a formatted section header."""
17 | print(f"\n{'=' * 60}")
18 | print(f" {title}")
19 | print('=' * 60)
20 |
21 |
22 | async def demo_token_counting():
23 | """Demonstrate token counting functionality."""
24 | print_section("Token Counting Demo")
25 |
26 | # Sample Flutter documentation
27 | sample_doc = """# Container
28 |
29 | A convenience widget that combines common painting, positioning, and sizing widgets.
30 |
31 | ## Description
32 |
33 | A container first surrounds the child with padding (inflated by any borders present in the decoration) and then applies additional constraints to the padded extent (incorporating the width and height as constraints, if either is non-null). The container is then surrounded by additional empty space described from the margin.
34 |
35 | ## Constructors
36 |
37 | ### Container({Key? key, AlignmentGeometry? alignment, EdgeInsetsGeometry? padding, Color? color, Decoration? decoration, Decoration? foregroundDecoration, double? width, double? height, BoxConstraints? constraints, EdgeInsetsGeometry? margin, Matrix4? transform, AlignmentGeometry? transformAlignment, Widget? child, Clip clipBehavior = Clip.none})
38 |
39 | Creates a widget that combines common painting, positioning, and sizing widgets.
40 |
41 | ## Properties
42 |
43 | - alignment → AlignmentGeometry?
44 | Align the child within the container.
45 |
46 | - child → Widget?
47 | The child contained by the container.
48 |
49 | - clipBehavior → Clip
50 | The clip behavior when Container.decoration is not null.
51 |
52 | - color → Color?
53 | The color to paint behind the child.
54 |
55 | - constraints → BoxConstraints?
56 | Additional constraints to apply to the child.
57 |
58 | ## Methods
59 |
60 | - build(BuildContext context) → Widget
61 | Describes the part of the user interface represented by this widget.
62 |
63 | - createElement() → StatelessElement
64 | Creates a StatelessElement to manage this widget's location in the tree.
65 |
66 | - debugDescribeChildren() → List<DiagnosticsNode>
67 | Returns a list of DiagnosticsNode objects describing this node's children.
68 |
69 | ## Examples
70 |
71 | ```dart
72 | Container(
73 | width: 200,
74 | height: 200,
75 | color: Colors.blue,
76 | child: Center(
77 | child: Text('Hello Flutter!'),
78 | ),
79 | )
80 | ```
81 |
82 | ## See Also
83 |
84 | - Padding, which adds padding to a child widget.
85 | - DecoratedBox, which applies a decoration to a child widget.
86 | - Transform, which applies a transformation to a child widget.
87 | """
88 |
89 | # Count tokens using approximation
90 | print(f"\nCurrent mode: {get_mode()}")
91 | approx_tokens = count_tokens(sample_doc)
92 | print(f"Approximate token count: {approx_tokens}")
93 | print(f"Character count: {len(sample_doc)}")
94 | print(f"Word count: {len(sample_doc.split())}")
95 | print(f"Ratio (tokens/words): {approx_tokens / len(sample_doc.split()):.2f}")
96 |
97 |
98 | async def demo_truncation():
99 | """Demonstrate document truncation."""
100 | print_section("Document Truncation Demo")
101 |
102 | # Create a longer document
103 | long_doc = """# ListView
104 |
105 | A scrollable list of widgets arranged linearly.
106 |
107 | ## Description
108 |
109 | ListView is the most commonly used scrolling widget. It displays its children one after another in the scroll direction. In the cross axis, the children are required to fill the ListView.
110 |
111 | If non-null, the itemExtent forces the children to have the given extent in the scroll direction. If non-null, the prototypeItem forces the children to have the same extent as the given widget in the scroll direction.
112 |
113 | Specifying an itemExtent or an prototypeItem is more efficient than letting the children determine their own extent because the scrolling machinery can make use of the foreknowledge of the children's extent to save work, for example when the scroll position changes drastically.
114 |
115 | ## Constructors
116 |
117 | ### ListView({Key? key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController? controller, bool? primary, ScrollPhysics? physics, bool shrinkWrap = false, EdgeInsetsGeometry? padding, double? itemExtent, Widget? prototypeItem, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, bool addSemanticIndexes = true, double? cacheExtent, List<Widget> children = const <Widget>[], int? semanticChildCount, DragStartBehavior dragStartBehavior = DragStartBehavior.start, ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, String? restorationId, Clip clipBehavior = Clip.hardEdge})
118 |
119 | Creates a scrollable, linear array of widgets from an explicit List.
120 |
121 | ### ListView.builder({Key? key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController? controller, bool? primary, ScrollPhysics? physics, bool shrinkWrap = false, EdgeInsetsGeometry? padding, double? itemExtent, Widget? prototypeItem, required NullableIndexedWidgetBuilder itemBuilder, ChildIndexGetter? findChildIndexCallback, int? itemCount, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, bool addSemanticIndexes = true, double? cacheExtent, int? semanticChildCount, DragStartBehavior dragStartBehavior = DragStartBehavior.start, ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, String? restorationId, Clip clipBehavior = Clip.hardEdge})
122 |
123 | Creates a scrollable, linear array of widgets that are created on demand.
124 |
125 | ### ListView.separated({Key? key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController? controller, bool? primary, ScrollPhysics? physics, bool shrinkWrap = false, EdgeInsetsGeometry? padding, required NullableIndexedWidgetBuilder itemBuilder, ChildIndexGetter? findChildIndexCallback, required IndexedWidgetBuilder separatorBuilder, required int itemCount, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, bool addSemanticIndexes = true, double? cacheExtent, DragStartBehavior dragStartBehavior = DragStartBehavior.start, ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, String? restorationId, Clip clipBehavior = Clip.hardEdge})
126 |
127 | Creates a scrollable, linear array of widgets with a custom separator.
128 |
129 | ## Properties
130 |
131 | ### Essential Properties
132 |
133 | - children → List<Widget>
134 | The widgets to display in the list.
135 |
136 | - controller → ScrollController?
137 | An object that can be used to control the position to which this scroll view is scrolled.
138 |
139 | - itemBuilder → NullableIndexedWidgetBuilder
140 | Called to build children for the list with a builder.
141 |
142 | - itemCount → int?
143 | The number of items to build when using ListView.builder.
144 |
145 | - scrollDirection → Axis
146 | The axis along which the scroll view scrolls.
147 |
148 | ### Performance Properties
149 |
150 | - cacheExtent → double?
151 | The viewport has an area before and after the visible area to cache items that are about to become visible when the user scrolls.
152 |
153 | - itemExtent → double?
154 | If non-null, forces the children to have the given extent in the scroll direction.
155 |
156 | - prototypeItem → Widget?
157 | If non-null, forces the children to have the same extent as the given widget in the scroll direction.
158 |
159 | - shrinkWrap → bool
160 | Whether the extent of the scroll view in the scrollDirection should be determined by the contents being viewed.
161 |
162 | ### Behavior Properties
163 |
164 | - physics → ScrollPhysics?
165 | How the scroll view should respond to user input.
166 |
167 | - primary → bool?
168 | Whether this is the primary scroll view associated with the parent PrimaryScrollController.
169 |
170 | - reverse → bool
171 | Whether the scroll view scrolls in the reading direction.
172 |
173 | ## Methods
174 |
175 | ### Core Methods
176 |
177 | - build(BuildContext context) → Widget
178 | Describes the part of the user interface represented by this widget.
179 |
180 | - buildChildLayout(BuildContext context) → Widget
181 | Subclasses should override this method to build the layout model.
182 |
183 | - buildSlivers(BuildContext context) → List<Widget>
184 | Build the list of widgets to place inside the viewport.
185 |
186 | - buildViewport(BuildContext context, ViewportOffset offset, AxisDirection axisDirection, List<Widget> slivers) → Widget
187 | Build the viewport.
188 |
189 | ### Utility Methods
190 |
191 | - debugFillProperties(DiagnosticPropertiesBuilder properties) → void
192 | Add additional properties associated with the node.
193 |
194 | - getDirection(BuildContext context) → AxisDirection
195 | Returns the AxisDirection in which the scroll view scrolls.
196 |
197 | ## Examples
198 |
199 | ### Basic ListView
200 |
201 | ```dart
202 | ListView(
203 | children: <Widget>[
204 | ListTile(
205 | leading: Icon(Icons.map),
206 | title: Text('Map'),
207 | ),
208 | ListTile(
209 | leading: Icon(Icons.photo_album),
210 | title: Text('Album'),
211 | ),
212 | ListTile(
213 | leading: Icon(Icons.phone),
214 | title: Text('Phone'),
215 | ),
216 | ],
217 | )
218 | ```
219 |
220 | ### ListView.builder Example
221 |
222 | ```dart
223 | ListView.builder(
224 | itemCount: 100,
225 | itemBuilder: (BuildContext context, int index) {
226 | return ListTile(
227 | title: Text('Item $index'),
228 | subtitle: Text('Subtitle for item $index'),
229 | leading: CircleAvatar(
230 | child: Text('$index'),
231 | ),
232 | );
233 | },
234 | )
235 | ```
236 |
237 | ### ListView.separated Example
238 |
239 | ```dart
240 | ListView.separated(
241 | itemCount: 50,
242 | itemBuilder: (BuildContext context, int index) {
243 | return Container(
244 | height: 50,
245 | color: Colors.amber[colorCodes[index]],
246 | child: Center(child: Text('Entry $index')),
247 | );
248 | },
249 | separatorBuilder: (BuildContext context, int index) => const Divider(),
250 | )
251 | ```
252 |
253 | ### Performance Optimized ListView
254 |
255 | ```dart
256 | ListView.builder(
257 | itemExtent: 60.0, // Fixed height for performance
258 | cacheExtent: 250.0, // Cache more items
259 | itemCount: items.length,
260 | itemBuilder: (context, index) {
261 | return ListTile(
262 | title: Text(items[index].title),
263 | subtitle: Text(items[index].subtitle),
264 | );
265 | },
266 | )
267 | ```
268 |
269 | ## Common Issues and Solutions
270 |
271 | ### Performance Issues
272 |
273 | 1. **Use ListView.builder for long lists**: Don't use the default constructor with many children.
274 | 2. **Set itemExtent when possible**: Improves scrolling performance significantly.
275 | 3. **Use const constructors**: For static content, use const widgets.
276 |
277 | ### Layout Issues
278 |
279 | 1. **Unbounded height error**: Wrap in Expanded or give explicit height.
280 | 2. **Horizontal ListView**: Set scrollDirection and wrap in Container with height.
281 |
282 | ## See Also
283 |
284 | - SingleChildScrollView, which is a scrollable widget that has a single child.
285 | - PageView, which is a scrolling list that works page by page.
286 | - GridView, which is a scrollable, 2D array of widgets.
287 | - CustomScrollView, which is a scrollable widget that creates custom scroll effects using slivers.
288 | - ListBody, which arranges its children in a similar manner, but without scrolling.
289 | - ScrollNotification and NotificationListener, which can be used to watch the scroll position without using a ScrollController.
290 | """
291 |
292 | truncator = DocumentTruncator()
293 | token_manager = TokenManager()
294 |
295 | # Test different token limits
296 | limits = [1000, 2000, 5000]
297 |
298 | for limit in limits:
299 | print(f"\n{'─' * 40}")
300 | print(f"Truncating to {limit} tokens:")
301 | print('─' * 40)
302 |
303 | truncated = truncator.truncate_to_limit(long_doc, limit)
304 | actual_tokens = token_manager.count_tokens(truncated)
305 |
306 | print(f"Original length: {len(long_doc)} characters")
307 | print(f"Truncated length: {len(truncated)} characters")
308 | print(f"Target tokens: {limit}")
309 | print(f"Actual tokens: {actual_tokens}")
310 |
311 | # Show which sections were kept
312 | sections_kept = []
313 | for section in ["Description", "Constructors", "Properties", "Methods", "Examples", "Common Issues", "See Also"]:
314 | if f"## {section}" in truncated:
315 | sections_kept.append(section)
316 |
317 | print(f"Sections kept: {', '.join(sections_kept)}")
318 |
319 | # Show if truncation notice was added
320 | if "*Note: Documentation has been truncated" in truncated:
321 | print("✓ Truncation notice added")
322 |
323 |
324 | async def demo_real_usage():
325 | """Demonstrate real-world usage with Flutter MCP."""
326 | print_section("Real Usage Example")
327 |
328 | print("\nExample 1: Default usage (no token limit)")
329 | print("─" * 40)
330 | print("await get_flutter_docs('Container')")
331 | print("→ Returns full documentation (8000 token default)")
332 |
333 | print("\nExample 2: Limited tokens for constrained context")
334 | print("─" * 40)
335 | print("await get_flutter_docs('Container', tokens=2000)")
336 | print("→ Returns essential information only")
337 |
338 | print("\nExample 3: Search with token limit")
339 | print("─" * 40)
340 | print("await search_flutter_docs('navigation', tokens=3000)")
341 | print("→ Each result gets proportional share of tokens")
342 |
343 | print("\nExample 4: Processing mentions with limits")
344 | print("─" * 40)
345 | print("await process_flutter_mentions('@flutter_mcp ListView @flutter_mcp GridView', tokens=2000)")
346 | print("→ Each mention gets ~1000 tokens")
347 |
348 | print("\nResponse format with token info:")
349 | print("─" * 40)
350 | response = {
351 | "content": "# Container\n\n## Description\n...",
352 | "source": "live",
353 | "class": "Container",
354 | "library": "widgets",
355 | "token_count": 1998,
356 | "original_tokens": 5234,
357 | "truncated": True,
358 | "truncation_note": "Documentation limited to 2000 tokens. Some sections omitted for brevity."
359 | }
360 | print(json.dumps(response, indent=2))
361 |
362 |
363 | async def main():
364 | """Run all demos."""
365 | print("\n╔═══════════════════════════════════════════════════════════╗")
366 | print("║ Flutter MCP Token Management Demo ║")
367 | print("╚═══════════════════════════════════════════════════════════╝")
368 |
369 | await demo_token_counting()
370 | await demo_truncation()
371 | await demo_real_usage()
372 |
373 | print("\n✅ Demo complete!")
374 |
375 |
376 | if __name__ == "__main__":
377 | asyncio.run(main())
```
--------------------------------------------------------------------------------
/src/flutter_mcp/error_handling.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Error handling utilities for Flutter MCP Server.
3 |
4 | This module provides comprehensive error handling, retry logic, and user-friendly
5 | error responses for the Flutter documentation server.
6 | """
7 |
8 | import asyncio
9 | import random
10 | import time
11 | from typing import Optional, Dict, List, Any, Callable
12 | from functools import wraps
13 | import httpx
14 | import structlog
15 |
16 | logger = structlog.get_logger()
17 |
18 | # Error handling constants
19 | MAX_RETRIES = 3
20 | BASE_RETRY_DELAY = 1.0 # seconds
21 | MAX_RETRY_DELAY = 16.0 # seconds
22 | DEFAULT_TIMEOUT = 30.0 # seconds
23 | CONNECTION_TIMEOUT = 10.0 # seconds
24 |
25 |
26 | class NetworkError(Exception):
27 | """Custom exception for network-related errors"""
28 | pass
29 |
30 |
31 | class DocumentationNotFoundError(Exception):
32 | """Custom exception when documentation is not found"""
33 | pass
34 |
35 |
36 | class RateLimitError(Exception):
37 | """Custom exception for rate limit violations"""
38 | pass
39 |
40 |
41 | class CacheError(Exception):
42 | """Custom exception for cache-related errors"""
43 | pass
44 |
45 |
46 | def calculate_backoff_delay(attempt: int) -> float:
47 | """Calculate exponential backoff delay with jitter."""
48 | delay = min(
49 | BASE_RETRY_DELAY * (2 ** attempt) + random.uniform(0, 1),
50 | MAX_RETRY_DELAY
51 | )
52 | return delay
53 |
54 |
55 | def with_retry(max_retries: int = MAX_RETRIES, retry_on: tuple = None):
56 | """
57 | Decorator for adding retry logic with exponential backoff.
58 |
59 | Args:
60 | max_retries: Maximum number of retry attempts
61 | retry_on: Tuple of exception types to retry on (default: network errors)
62 | """
63 | if retry_on is None:
64 | retry_on = (httpx.TimeoutException, httpx.ConnectError, httpx.NetworkError)
65 |
66 | def decorator(func):
67 | @wraps(func)
68 | async def wrapper(*args, **kwargs):
69 | last_exception = None
70 |
71 | for attempt in range(max_retries):
72 | try:
73 | return await func(*args, **kwargs)
74 | except retry_on as e:
75 | last_exception = e
76 | if attempt < max_retries - 1:
77 | delay = calculate_backoff_delay(attempt)
78 | logger.warning(
79 | "retrying_request",
80 | function=func.__name__,
81 | attempt=attempt + 1,
82 | max_retries=max_retries,
83 | delay=delay,
84 | error=str(e),
85 | error_type=type(e).__name__
86 | )
87 | await asyncio.sleep(delay)
88 | else:
89 | raise NetworkError(
90 | f"Network error after {max_retries} attempts: {str(e)}"
91 | ) from e
92 | except httpx.HTTPStatusError as e:
93 | # Don't retry on 4xx errors (client errors)
94 | if 400 <= e.response.status_code < 500:
95 | raise
96 | # Retry on 5xx errors (server errors)
97 | last_exception = e
98 | if attempt < max_retries - 1 and e.response.status_code >= 500:
99 | delay = calculate_backoff_delay(attempt)
100 | logger.warning(
101 | "retrying_server_error",
102 | function=func.__name__,
103 | attempt=attempt + 1,
104 | max_retries=max_retries,
105 | delay=delay,
106 | status_code=e.response.status_code
107 | )
108 | await asyncio.sleep(delay)
109 | else:
110 | raise
111 |
112 | # Should never reach here, but just in case
113 | if last_exception:
114 | raise last_exception
115 |
116 | return wrapper
117 | return decorator
118 |
119 |
120 | async def safe_http_get(
121 | url: str,
122 | headers: Optional[Dict] = None,
123 | timeout: float = DEFAULT_TIMEOUT,
124 | max_retries: int = MAX_RETRIES
125 | ) -> httpx.Response:
126 | """
127 | Safely perform HTTP GET request with proper error handling and retries.
128 |
129 | Args:
130 | url: URL to fetch
131 | headers: Optional HTTP headers
132 | timeout: Request timeout in seconds
133 | max_retries: Maximum number of retry attempts
134 |
135 | Returns:
136 | HTTP response object
137 |
138 | Raises:
139 | NetworkError: For network-related failures after retries
140 | httpx.HTTPStatusError: For HTTP errors
141 | """
142 | if headers is None:
143 | headers = {}
144 |
145 | headers.setdefault(
146 | "User-Agent",
147 | "Flutter-MCP-Docs/1.0 (github.com/flutter-mcp/flutter-mcp)"
148 | )
149 |
150 | @with_retry(max_retries=max_retries)
151 | async def _get():
152 | async with httpx.AsyncClient(
153 | timeout=httpx.Timeout(timeout, connect=CONNECTION_TIMEOUT),
154 | follow_redirects=True,
155 | limits=httpx.Limits(max_connections=10, max_keepalive_connections=5)
156 | ) as client:
157 | response = await client.get(url, headers=headers)
158 | response.raise_for_status()
159 | return response
160 |
161 | return await _get()
162 |
163 |
164 | def format_error_response(
165 | error_type: str,
166 | message: str,
167 | suggestions: Optional[List[str]] = None,
168 | context: Optional[Dict] = None
169 | ) -> Dict[str, Any]:
170 | """
171 | Format consistent error responses with helpful information.
172 |
173 | Args:
174 | error_type: Type of error (e.g., "not_found", "network_error")
175 | message: Human-readable error message
176 | suggestions: List of helpful suggestions for the user
177 | context: Additional context information
178 |
179 | Returns:
180 | Formatted error response dictionary
181 | """
182 | from datetime import datetime
183 |
184 | response = {
185 | "error": True,
186 | "error_type": error_type,
187 | "message": message,
188 | "timestamp": datetime.utcnow().isoformat()
189 | }
190 |
191 | if suggestions:
192 | response["suggestions"] = suggestions
193 |
194 | if context:
195 | response["context"] = context
196 |
197 | return response
198 |
199 |
200 | def get_error_suggestions(error_type: str, context: Dict = None) -> List[str]:
201 | """
202 | Get context-aware suggestions based on error type.
203 |
204 | Args:
205 | error_type: Type of error
206 | context: Error context (e.g., class name, library, etc.)
207 |
208 | Returns:
209 | List of helpful suggestions
210 | """
211 | suggestions_map = {
212 | "not_found": [
213 | "Check if the name is spelled correctly",
214 | "Verify that the item exists in the specified library",
215 | "Try searching with search_flutter_docs() for similar items",
216 | "Common libraries: widgets, material, cupertino, painting, rendering"
217 | ],
218 | "network_error": [
219 | "Check your internet connection",
220 | "The documentation server may be temporarily unavailable",
221 | "Try again in a few moments",
222 | "Check if you can access https://api.flutter.dev in your browser"
223 | ],
224 | "timeout": [
225 | "The server is taking too long to respond",
226 | "Try again with a simpler query",
227 | "The documentation server might be under heavy load",
228 | "Check https://status.flutter.dev/ for service status"
229 | ],
230 | "rate_limited": [
231 | "You've made too many requests in a short time",
232 | "Wait a few minutes before retrying",
233 | "Consider implementing local caching for frequently accessed docs",
234 | "Space out your requests to avoid rate limits"
235 | ],
236 | "parse_error": [
237 | "The documentation format may have changed",
238 | "Try using a different query format",
239 | "Report this issue if it persists",
240 | "The server response was not in the expected format"
241 | ],
242 | "cache_error": [
243 | "The local cache encountered an error",
244 | "The request will proceed without caching",
245 | "Consider restarting the server if this persists",
246 | "Check disk space and permissions for the cache directory"
247 | ]
248 | }
249 |
250 | base_suggestions = suggestions_map.get(error_type, [
251 | "An unexpected error occurred",
252 | "Please try again",
253 | "If the problem persists, check the server logs"
254 | ])
255 |
256 | # Add context-specific suggestions
257 | if context:
258 | if "class" in context and "library" in context:
259 | base_suggestions.insert(0,
260 | f"Verify '{context['class']}' exists in the '{context['library']}' library"
261 | )
262 | elif "package" in context:
263 | base_suggestions.insert(0,
264 | f"Verify package name '{context['package']}' is correct"
265 | )
266 |
267 | return base_suggestions
268 |
269 |
270 | async def with_error_handling(
271 | operation: Callable,
272 | operation_name: str,
273 | context: Dict = None,
274 | fallback_value: Any = None
275 | ) -> Any:
276 | """
277 | Execute an operation with comprehensive error handling.
278 |
279 | Args:
280 | operation: Async callable to execute
281 | operation_name: Name of the operation for logging
282 | context: Context information for error messages
283 | fallback_value: Value to return on error (if None, error is returned)
284 |
285 | Returns:
286 | Operation result or error response
287 | """
288 | try:
289 | return await operation()
290 |
291 | except httpx.HTTPStatusError as e:
292 | status_code = e.response.status_code
293 | logger.error(
294 | f"{operation_name}_http_error",
295 | status_code=status_code,
296 | url=str(e.request.url),
297 | **context or {}
298 | )
299 |
300 | if status_code == 404:
301 | error_type = "not_found"
302 | message = f"Resource not found (HTTP 404)"
303 | elif status_code == 429:
304 | error_type = "rate_limited"
305 | message = "Rate limit exceeded"
306 | elif 500 <= status_code < 600:
307 | error_type = "server_error"
308 | message = f"Server error (HTTP {status_code})"
309 | else:
310 | error_type = "http_error"
311 | message = f"HTTP error {status_code}"
312 |
313 | suggestions = get_error_suggestions(error_type, context)
314 |
315 | if fallback_value is not None:
316 | return fallback_value
317 |
318 | return format_error_response(
319 | error_type,
320 | message,
321 | suggestions=suggestions,
322 | context={
323 | **(context or {}),
324 | "status_code": status_code,
325 | "url": str(e.request.url)
326 | }
327 | )
328 |
329 | except NetworkError as e:
330 | logger.error(
331 | f"{operation_name}_network_error",
332 | error=str(e),
333 | **context or {}
334 | )
335 |
336 | if fallback_value is not None:
337 | return fallback_value
338 |
339 | return format_error_response(
340 | "network_error",
341 | "Network connection error",
342 | suggestions=get_error_suggestions("network_error"),
343 | context=context
344 | )
345 |
346 | except asyncio.TimeoutError:
347 | logger.error(
348 | f"{operation_name}_timeout",
349 | **context or {}
350 | )
351 |
352 | if fallback_value is not None:
353 | return fallback_value
354 |
355 | return format_error_response(
356 | "timeout",
357 | "Request timed out",
358 | suggestions=get_error_suggestions("timeout"),
359 | context={
360 | **(context or {}),
361 | "timeout": DEFAULT_TIMEOUT
362 | }
363 | )
364 |
365 | except Exception as e:
366 | logger.error(
367 | f"{operation_name}_unexpected_error",
368 | error=str(e),
369 | error_type=type(e).__name__,
370 | **context or {}
371 | )
372 |
373 | if fallback_value is not None:
374 | return fallback_value
375 |
376 | return format_error_response(
377 | "unexpected_error",
378 | f"An unexpected error occurred: {str(e)}",
379 | suggestions=[
380 | "This is an unexpected error",
381 | "Please try again",
382 | "If the problem persists, report it with the error details"
383 | ],
384 | context={
385 | **(context or {}),
386 | "error_type": type(e).__name__
387 | }
388 | )
389 |
390 |
391 | class CircuitBreaker:
392 | """
393 | Circuit breaker pattern for handling repeated failures.
394 |
395 | Prevents cascading failures by temporarily disabling operations
396 | that are consistently failing.
397 | """
398 |
399 | def __init__(
400 | self,
401 | failure_threshold: int = 5,
402 | recovery_timeout: float = 60.0,
403 | expected_exception: type = Exception
404 | ):
405 | self.failure_threshold = failure_threshold
406 | self.recovery_timeout = recovery_timeout
407 | self.expected_exception = expected_exception
408 | self.failure_count = 0
409 | self.last_failure_time = None
410 | self.state = "closed" # closed, open, half-open
411 |
412 | async def call(self, func, *args, **kwargs):
413 | """Execute function with circuit breaker protection."""
414 | if self.state == "open":
415 | if time.time() - self.last_failure_time > self.recovery_timeout:
416 | self.state = "half-open"
417 | logger.info("circuit_breaker_half_open", function=func.__name__)
418 | else:
419 | raise NetworkError("Circuit breaker is OPEN - service temporarily disabled")
420 |
421 | try:
422 | result = await func(*args, **kwargs)
423 | if self.state == "half-open":
424 | self.state = "closed"
425 | self.failure_count = 0
426 | logger.info("circuit_breaker_closed", function=func.__name__)
427 | return result
428 |
429 | except self.expected_exception as e:
430 | self.failure_count += 1
431 | self.last_failure_time = time.time()
432 |
433 | if self.failure_count >= self.failure_threshold:
434 | self.state = "open"
435 | logger.error(
436 | "circuit_breaker_open",
437 | function=func.__name__,
438 | failure_count=self.failure_count
439 | )
440 |
441 | raise
```
--------------------------------------------------------------------------------
/tests/test_truncation.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """Test cases for the smart truncation algorithm."""
3 |
4 | import pytest
5 | from src.flutter_mcp.truncation import (
6 | SmartTruncator, AdaptiveTruncator, ContentPriority,
7 | DocumentationSection, truncate_flutter_docs
8 | )
9 |
10 |
11 | def create_sample_documentation():
12 | """Create a sample Flutter documentation for testing."""
13 | return """# Container
14 |
15 | ## Description
16 | A convenience widget that combines common painting, positioning, and sizing widgets.
17 | Container is a very commonly used widget in Flutter applications. It provides a way to
18 | customize the appearance and layout of child widgets. The Container widget can be used
19 | to add padding, margins, borders, background color, and many other styling options
20 | to its child widget.
21 |
22 | ## Constructors
23 |
24 | ### Container({Key? key, AlignmentGeometry? alignment, EdgeInsetsGeometry? padding, Color? color, Decoration? decoration, Decoration? foregroundDecoration, double? width, double? height, BoxConstraints? constraints, EdgeInsetsGeometry? margin, Matrix4? transform, AlignmentGeometry? transformAlignment, Widget? child, Clip clipBehavior = Clip.none})
25 | ```dart
26 | Container({
27 | Key? key,
28 | this.alignment,
29 | this.padding,
30 | this.color,
31 | this.decoration,
32 | this.foregroundDecoration,
33 | double? width,
34 | double? height,
35 | BoxConstraints? constraints,
36 | this.margin,
37 | this.transform,
38 | this.transformAlignment,
39 | this.child,
40 | this.clipBehavior = Clip.none,
41 | })
42 | ```
43 | Creates a widget that combines common painting, positioning, and sizing widgets.
44 |
45 | ### Container.fixed({required double width, required double height, Widget? child})
46 | ```dart
47 | Container.fixed({
48 | required double width,
49 | required double height,
50 | Widget? child,
51 | })
52 | ```
53 | Creates a container with fixed dimensions.
54 |
55 | ## Properties
56 |
57 | - **alignment**: How to align the child within the container
58 | - **padding**: Empty space to inscribe inside the decoration
59 | - **color**: The color to paint behind the child
60 | - **decoration**: The decoration to paint behind the child
61 | - **foregroundDecoration**: The decoration to paint in front of the child
62 | - **width**: Container width constraint
63 | - **height**: Container height constraint
64 | - **constraints**: Additional constraints to apply to the child
65 | - **margin**: Empty space to surround the decoration and child
66 | - **transform**: The transformation matrix to apply before painting
67 | - **transformAlignment**: The alignment of the origin
68 | - **child**: The child contained by the container
69 | - **clipBehavior**: How to clip the contents
70 |
71 | ## Methods
72 |
73 | ### build(BuildContext context)
74 | ```dart
75 | @override
76 | Widget build(BuildContext context) {
77 | Widget? current = child;
78 |
79 | if (child == null && (constraints == null || !constraints!.isTight)) {
80 | current = LimitedBox(
81 | maxWidth: 0.0,
82 | maxHeight: 0.0,
83 | child: ConstrainedBox(constraints: const BoxConstraints.expand()),
84 | );
85 | }
86 |
87 | if (alignment != null)
88 | current = Align(alignment: alignment!, child: current);
89 |
90 | final EdgeInsetsGeometry? effectivePadding = _paddingIncludingDecoration;
91 | if (effectivePadding != null)
92 | current = Padding(padding: effectivePadding, child: current);
93 |
94 | if (color != null)
95 | current = ColoredBox(color: color!, child: current);
96 |
97 | if (clipBehavior != Clip.none) {
98 | assert(decoration != null);
99 | current = ClipPath(
100 | clipper: _DecorationClipper(
101 | textDirection: Directionality.maybeOf(context),
102 | decoration: decoration!,
103 | ),
104 | clipBehavior: clipBehavior,
105 | child: current,
106 | );
107 | }
108 |
109 | if (decoration != null)
110 | current = DecoratedBox(decoration: decoration!, child: current);
111 |
112 | if (foregroundDecoration != null) {
113 | current = DecoratedBox(
114 | decoration: foregroundDecoration!,
115 | position: DecorationPosition.foreground,
116 | child: current,
117 | );
118 | }
119 |
120 | if (constraints != null)
121 | current = ConstrainedBox(constraints: constraints!, child: current);
122 |
123 | if (margin != null)
124 | current = Padding(padding: margin!, child: current);
125 |
126 | if (transform != null)
127 | current = Transform(transform: transform!, alignment: transformAlignment, child: current);
128 |
129 | return current!;
130 | }
131 | ```
132 | Describes the part of the user interface represented by this widget.
133 |
134 | ### debugFillProperties(DiagnosticPropertiesBuilder properties)
135 | ```dart
136 | @override
137 | void debugFillProperties(DiagnosticPropertiesBuilder properties) {
138 | super.debugFillProperties(properties);
139 | properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment, showName: false, defaultValue: null));
140 | properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
141 | properties.add(DiagnosticsProperty<Clip>('clipBehavior', clipBehavior, defaultValue: Clip.none));
142 | // ... more properties
143 | }
144 | ```
145 | Add additional properties associated with the node.
146 |
147 | ### createElement()
148 | ```dart
149 | @override
150 | StatelessElement createElement() => StatelessElement(this);
151 | ```
152 | Creates a StatelessElement to manage this widget's location in the tree.
153 |
154 | ### toStringShort()
155 | ```dart
156 | @override
157 | String toStringShort() {
158 | return key == null ? '$runtimeType' : '$runtimeType-$key';
159 | }
160 | ```
161 | A brief description of this object, usually just the runtimeType and hashCode.
162 |
163 | ## Code Examples
164 |
165 | #### Example 1:
166 | ```dart
167 | Container(
168 | width: 200,
169 | height: 200,
170 | color: Colors.blue,
171 | child: Center(
172 | child: Text(
173 | 'Hello World',
174 | style: TextStyle(color: Colors.white, fontSize: 24),
175 | ),
176 | ),
177 | )
178 | ```
179 |
180 | #### Example 2:
181 | ```dart
182 | Container(
183 | margin: EdgeInsets.all(20.0),
184 | padding: EdgeInsets.all(10.0),
185 | decoration: BoxDecoration(
186 | border: Border.all(color: Colors.black, width: 2.0),
187 | borderRadius: BorderRadius.circular(10.0),
188 | gradient: LinearGradient(
189 | colors: [Colors.blue, Colors.green],
190 | begin: Alignment.topLeft,
191 | end: Alignment.bottomRight,
192 | ),
193 | ),
194 | child: Text('Decorated Container'),
195 | )
196 | ```
197 |
198 | #### Example 3:
199 | ```dart
200 | Container(
201 | transform: Matrix4.rotationZ(0.1),
202 | child: Container(
203 | width: 100,
204 | height: 100,
205 | color: Colors.red,
206 | child: Center(
207 | child: Text('Rotated'),
208 | ),
209 | ),
210 | )
211 | ```
212 |
213 | #### Example 4:
214 | ```dart
215 | Container(
216 | constraints: BoxConstraints(
217 | minWidth: 100,
218 | maxWidth: 200,
219 | minHeight: 50,
220 | maxHeight: 100,
221 | ),
222 | color: Colors.amber,
223 | child: Text(
224 | 'This text will wrap based on the constraints',
225 | overflow: TextOverflow.ellipsis,
226 | ),
227 | )
228 | ```
229 |
230 | #### Example 5:
231 | ```dart
232 | Container(
233 | decoration: BoxDecoration(
234 | color: Colors.white,
235 | boxShadow: [
236 | BoxShadow(
237 | color: Colors.grey.withOpacity(0.5),
238 | spreadRadius: 5,
239 | blurRadius: 7,
240 | offset: Offset(0, 3),
241 | ),
242 | ],
243 | ),
244 | child: Padding(
245 | padding: EdgeInsets.all(20.0),
246 | child: Text('Container with shadow'),
247 | ),
248 | )
249 | ```
250 | """
251 |
252 |
253 | class TestSmartTruncator:
254 | """Test the basic SmartTruncator functionality."""
255 |
256 | def test_no_truncation_needed(self):
257 | """Test that small documents are not truncated."""
258 | truncator = SmartTruncator(max_tokens=10000)
259 | doc = "# Small Doc\n\nThis is a small document."
260 |
261 | result = truncator.truncate_documentation(doc, "SmallClass")
262 | assert result == doc
263 |
264 | def test_basic_truncation(self):
265 | """Test basic truncation of large documents."""
266 | truncator = SmartTruncator(max_tokens=500)
267 | doc = create_sample_documentation()
268 |
269 | result = truncator.truncate_documentation(doc, "Container")
270 |
271 | # Should be shorter than original
272 | assert len(result) < len(doc)
273 |
274 | # Should still contain critical sections
275 | assert "## Description" in result
276 | assert "## Constructors" in result
277 |
278 | # Should have truncation notice
279 | assert "truncated" in result.lower()
280 |
281 | def test_section_parsing(self):
282 | """Test that sections are parsed correctly."""
283 | truncator = SmartTruncator()
284 | doc = create_sample_documentation()
285 |
286 | sections = truncator._parse_documentation(doc, "Container")
287 |
288 | # Check that we have sections of different types
289 | section_names = [s.name for s in sections]
290 | assert any("description" in name for name in section_names)
291 | assert any("constructor" in name for name in section_names)
292 | assert any("property" in name for name in section_names)
293 | assert any("method" in name for name in section_names)
294 | assert any("example" in name for name in section_names)
295 |
296 | def test_priority_assignment(self):
297 | """Test that priorities are assigned correctly."""
298 | truncator = SmartTruncator()
299 | doc = create_sample_documentation()
300 |
301 | sections = truncator._parse_documentation(doc, "Container")
302 |
303 | # Description should be CRITICAL
304 | desc_sections = [s for s in sections if s.name == "description"]
305 | assert len(desc_sections) > 0
306 | assert desc_sections[0].priority == ContentPriority.CRITICAL
307 |
308 | # Constructor signatures should be CRITICAL
309 | const_sig_sections = [s for s in sections if "constructor_sig" in s.name]
310 | assert len(const_sig_sections) > 0
311 | assert all(s.priority == ContentPriority.CRITICAL for s in const_sig_sections)
312 |
313 | # build method should be HIGH priority
314 | build_sections = [s for s in sections if "method_build" in s.name]
315 | assert len(build_sections) > 0
316 | assert build_sections[0].priority == ContentPriority.HIGH
317 |
318 | def test_code_truncation(self):
319 | """Test that code is truncated intelligently."""
320 | truncator = SmartTruncator()
321 |
322 | code = """```dart
323 | Container(
324 | width: 200,
325 | height: 200,
326 | child: Column(
327 | children: [
328 | Text('Line 1'),
329 | Text('Line 2'),
330 | Text('Line 3'),
331 | ],
332 | ),
333 | )
334 | ```"""
335 |
336 | truncated = truncator._truncate_code(code, 100)
337 |
338 | # Should be shorter
339 | assert len(truncated) < len(code)
340 |
341 | # Should try to balance braces
342 | open_braces = truncated.count('{')
343 | close_braces = truncated.count('}')
344 | assert abs(open_braces - close_braces) <= 2
345 |
346 |
347 | class TestAdaptiveTruncator:
348 | """Test the AdaptiveTruncator with different strategies."""
349 |
350 | def test_balanced_strategy(self):
351 | """Test balanced truncation strategy."""
352 | truncator = AdaptiveTruncator(max_tokens=1000)
353 | doc = create_sample_documentation()
354 |
355 | result, metadata = truncator.truncate_with_strategy(
356 | doc, "Container", "widgets", "balanced"
357 | )
358 |
359 | # Should have a mix of content
360 | assert "## Description" in result
361 | assert "## Constructors" in result
362 | assert "## Properties" in result
363 | assert metadata["strategy_used"] == "balanced"
364 |
365 | def test_signatures_strategy(self):
366 | """Test signatures-focused truncation strategy."""
367 | truncator = AdaptiveTruncator(max_tokens=800)
368 | doc = create_sample_documentation()
369 |
370 | result, metadata = truncator.truncate_with_strategy(
371 | doc, "Container", "widgets", "signatures"
372 | )
373 |
374 | # Should prioritize method signatures
375 | assert "```dart" in result # Code blocks
376 | assert metadata["strategy_used"] == "signatures"
377 |
378 | def test_minimal_strategy(self):
379 | """Test minimal truncation strategy."""
380 | truncator = AdaptiveTruncator(max_tokens=400)
381 | doc = create_sample_documentation()
382 |
383 | result, metadata = truncator.truncate_with_strategy(
384 | doc, "Container", "widgets", "minimal"
385 | )
386 |
387 | # Should be very short
388 | assert len(result) < 2000 # Much shorter than original
389 | assert metadata["strategy_used"] == "minimal"
390 |
391 | # Should still have the most essential parts
392 | assert "Container" in result
393 | assert "## Description" in result
394 |
395 | def test_truncation_metadata(self):
396 | """Test that truncation metadata is accurate."""
397 | truncator = AdaptiveTruncator(max_tokens=500)
398 | doc = create_sample_documentation()
399 |
400 | result, metadata = truncator.truncate_with_strategy(
401 | doc, "Container", "widgets", "balanced"
402 | )
403 |
404 | assert "original_length" in metadata
405 | assert "truncated_length" in metadata
406 | assert "compression_ratio" in metadata
407 | assert "was_truncated" in metadata
408 |
409 | assert metadata["original_length"] == len(doc)
410 | assert metadata["truncated_length"] == len(result)
411 | assert 0 < metadata["compression_ratio"] < 1
412 | assert metadata["was_truncated"] is True
413 |
414 |
415 | class TestUtilityFunctions:
416 | """Test the utility functions."""
417 |
418 | def test_truncate_flutter_docs_function(self):
419 | """Test the convenience function."""
420 | doc = create_sample_documentation()
421 |
422 | result = truncate_flutter_docs(
423 | doc,
424 | "Container",
425 | max_tokens=500,
426 | strategy="minimal"
427 | )
428 |
429 | assert len(result) < len(doc)
430 | assert "Container" in result
431 | assert "truncated" in result.lower()
432 |
433 |
434 | def test_high_priority_widgets():
435 | """Test that high-priority widgets are recognized."""
436 | truncator = SmartTruncator()
437 |
438 | # Test some high-priority widgets
439 | for widget in ["Container", "Scaffold", "Row", "Column"]:
440 | assert widget in truncator.widget_priorities.HIGH_PRIORITY_WIDGETS
441 |
442 |
443 | def test_token_estimation():
444 | """Test token estimation accuracy."""
445 | section = DocumentationSection(
446 | name="test",
447 | content="This is a test content with some words.",
448 | priority=ContentPriority.MEDIUM
449 | )
450 |
451 | # Rough estimation check
452 | assert 5 <= section.token_estimate <= 15 # Should be around 10 tokens
453 |
454 |
455 | if __name__ == "__main__":
456 | # Run a simple demonstration
457 | print("Smart Truncation Algorithm Demo")
458 | print("=" * 50)
459 |
460 | doc = create_sample_documentation()
461 | print(f"Original document length: {len(doc)} characters")
462 |
463 | for max_tokens in [500, 1000, 2000]:
464 | print(f"\nTruncating to {max_tokens} tokens:")
465 | result = truncate_flutter_docs(doc, "Container", max_tokens)
466 | print(f" Result length: {len(result)} characters")
467 | print(f" Compression ratio: {len(result)/len(doc):.2%}")
468 |
469 | # Check what sections survived
470 | sections = []
471 | if "## Description" in result:
472 | sections.append("Description")
473 | if "## Constructors" in result:
474 | sections.append("Constructors")
475 | if "## Properties" in result:
476 | sections.append("Properties")
477 | if "## Methods" in result:
478 | sections.append("Methods")
479 | if "## Code Examples" in result:
480 | sections.append("Examples")
481 |
482 | print(f" Sections kept: {', '.join(sections)}")
```
--------------------------------------------------------------------------------
/examples/truncation_demo.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | Demonstration of the smart truncation algorithm for Flutter documentation.
4 |
5 | This script shows how the truncation algorithm preserves the most important
6 | information when dealing with token limits.
7 | """
8 |
9 | import asyncio
10 | from src.flutter_mcp.truncation import AdaptiveTruncator, ContentPriority
11 |
12 |
13 | def create_mock_flutter_doc():
14 | """Create a mock Flutter documentation that simulates a real large doc."""
15 | return """# ListView
16 |
17 | ## Description
18 | A scrollable list of widgets arranged linearly. ListView is the most commonly used
19 | scrolling widget. It displays its children one after another in the scroll direction.
20 | In the cross axis, the children are required to fill the ListView.
21 |
22 | There are four options for constructing a ListView:
23 |
24 | 1. The default constructor takes an explicit List<Widget> of children. This constructor
25 | is appropriate for list views with a small number of children because constructing
26 | the List requires doing work for every child that could possibly be displayed in
27 | the list view instead of just those children that are actually visible.
28 |
29 | 2. The ListView.builder constructor takes an IndexedWidgetBuilder, which builds the
30 | children on demand. This constructor is appropriate for list views with a large
31 | (or infinite) number of children because the builder is called only for those
32 | children that are actually visible.
33 |
34 | 3. The ListView.separated constructor takes two IndexedWidgetBuilders: itemBuilder
35 | builds child items on demand, and separatorBuilder similarly builds separator
36 | children which appear in between the child items. This constructor is appropriate
37 | for list views with a fixed number of children.
38 |
39 | 4. The ListView.custom constructor takes a SliverChildDelegate, which provides the
40 | ability to customize additional aspects of the child model.
41 |
42 | ## Constructors
43 |
44 | ### ListView({Key? key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController? controller, bool? primary, ScrollPhysics? physics, bool shrinkWrap = false, EdgeInsetsGeometry? padding, double? itemExtent, Widget? prototypeItem, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, bool addSemanticIndexes = true, double? cacheExtent, List<Widget> children = const <Widget>[], int? semanticChildCount, DragStartBehavior dragStartBehavior = DragStartBehavior.start, ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, String? restorationId, Clip clipBehavior = Clip.hardEdge})
45 | ```dart
46 | ListView({
47 | Key? key,
48 | Axis scrollDirection = Axis.vertical,
49 | bool reverse = false,
50 | ScrollController? controller,
51 | bool? primary,
52 | ScrollPhysics? physics,
53 | bool shrinkWrap = false,
54 | EdgeInsetsGeometry? padding,
55 | this.itemExtent,
56 | this.prototypeItem,
57 | bool addAutomaticKeepAlives = true,
58 | bool addRepaintBoundaries = true,
59 | bool addSemanticIndexes = true,
60 | double? cacheExtent,
61 | List<Widget> children = const <Widget>[],
62 | int? semanticChildCount,
63 | DragStartBehavior dragStartBehavior = DragStartBehavior.start,
64 | ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
65 | String? restorationId,
66 | Clip clipBehavior = Clip.hardEdge,
67 | })
68 | ```
69 | Creates a scrollable, linear array of widgets from an explicit List.
70 |
71 | ### ListView.builder({Key? key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController? controller, bool? primary, ScrollPhysics? physics, bool shrinkWrap = false, EdgeInsetsGeometry? padding, double? itemExtent, Widget? prototypeItem, required IndexedWidgetBuilder itemBuilder, int? itemCount, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, bool addSemanticIndexes = true, double? cacheExtent, int? semanticChildCount, DragStartBehavior dragStartBehavior = DragStartBehavior.start, ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, String? restorationId, Clip clipBehavior = Clip.hardEdge})
72 | ```dart
73 | ListView.builder({
74 | Key? key,
75 | Axis scrollDirection = Axis.vertical,
76 | bool reverse = false,
77 | ScrollController? controller,
78 | bool? primary,
79 | ScrollPhysics? physics,
80 | bool shrinkWrap = false,
81 | EdgeInsetsGeometry? padding,
82 | this.itemExtent,
83 | this.prototypeItem,
84 | required IndexedWidgetBuilder itemBuilder,
85 | int? itemCount,
86 | bool addAutomaticKeepAlives = true,
87 | bool addRepaintBoundaries = true,
88 | bool addSemanticIndexes = true,
89 | double? cacheExtent,
90 | int? semanticChildCount,
91 | DragStartBehavior dragStartBehavior = DragStartBehavior.start,
92 | ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
93 | String? restorationId,
94 | Clip clipBehavior = Clip.hardEdge,
95 | })
96 | ```
97 | Creates a scrollable, linear array of widgets that are created on demand.
98 |
99 | ### ListView.separated({Key? key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController? controller, bool? primary, ScrollPhysics? physics, bool shrinkWrap = false, EdgeInsetsGeometry? padding, required IndexedWidgetBuilder itemBuilder, required IndexedWidgetBuilder separatorBuilder, required int itemCount, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, bool addSemanticIndexes = true, double? cacheExtent, DragStartBehavior dragStartBehavior = DragStartBehavior.start, ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, String? restorationId, Clip clipBehavior = Clip.hardEdge})
100 | ```dart
101 | ListView.separated({
102 | Key? key,
103 | Axis scrollDirection = Axis.vertical,
104 | bool reverse = false,
105 | ScrollController? controller,
106 | bool? primary,
107 | ScrollPhysics? physics,
108 | bool shrinkWrap = false,
109 | EdgeInsetsGeometry? padding,
110 | required IndexedWidgetBuilder itemBuilder,
111 | required IndexedWidgetBuilder separatorBuilder,
112 | required int itemCount,
113 | bool addAutomaticKeepAlives = true,
114 | bool addRepaintBoundaries = true,
115 | bool addSemanticIndexes = true,
116 | double? cacheExtent,
117 | DragStartBehavior dragStartBehavior = DragStartBehavior.start,
118 | ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
119 | String? restorationId,
120 | Clip clipBehavior = Clip.hardEdge,
121 | })
122 | ```
123 | Creates a scrollable, linear array of widgets with a custom separator.
124 |
125 | ## Properties
126 |
127 | - **scrollDirection**: The axis along which the scroll view scrolls
128 | - **reverse**: Whether the scroll view scrolls in the reading direction
129 | - **controller**: An object that can be used to control the position to which this scroll view is scrolled
130 | - **primary**: Whether this is the primary scroll view associated with the parent
131 | - **physics**: How the scroll view should respond to user input
132 | - **shrinkWrap**: Whether the extent of the scroll view should be determined by the contents
133 | - **padding**: The amount of space by which to inset the children
134 | - **itemExtent**: The extent of each item in the scroll direction
135 | - **prototypeItem**: A prototype item to use for measuring item extent
136 | - **addAutomaticKeepAlives**: Whether to wrap each child in an AutomaticKeepAlive
137 | - **addRepaintBoundaries**: Whether to wrap each child in a RepaintBoundary
138 | - **addSemanticIndexes**: Whether to wrap each child in an IndexedSemantics
139 | - **cacheExtent**: The viewport has an area before and after the visible area to cache items
140 | - **semanticChildCount**: The number of children that will contribute semantic information
141 | - **dragStartBehavior**: Determines the way that drag start behavior is handled
142 | - **keyboardDismissBehavior**: Defines how the scroll view dismisses the keyboard
143 | - **restorationId**: Restoration ID to save and restore the scroll offset
144 | - **clipBehavior**: The content will be clipped (or not) according to this option
145 |
146 | ## Methods
147 |
148 | ### build(BuildContext context)
149 | ```dart
150 | @override
151 | Widget build(BuildContext context) {
152 | final List<Widget> slivers = buildSlivers(context);
153 | final AxisDirection axisDirection = getDirection(context);
154 |
155 | final ScrollController? scrollController = primary
156 | ? PrimaryScrollController.of(context)
157 | : controller;
158 | final Scrollable scrollable = Scrollable(
159 | dragStartBehavior: dragStartBehavior,
160 | axisDirection: axisDirection,
161 | controller: scrollController,
162 | physics: physics,
163 | scrollBehavior: scrollBehavior,
164 | semanticChildCount: semanticChildCount,
165 | restorationId: restorationId,
166 | viewportBuilder: (BuildContext context, ViewportOffset offset) {
167 | return buildViewport(context, offset, axisDirection, slivers);
168 | },
169 | );
170 | final Widget scrollableResult = primary && scrollController != null
171 | ? PrimaryScrollController.none(child: scrollable)
172 | : scrollable;
173 |
174 | if (keyboardDismissBehavior == ScrollViewKeyboardDismissBehavior.onDrag) {
175 | return NotificationListener<ScrollUpdateNotification>(
176 | child: scrollableResult,
177 | onNotification: (ScrollUpdateNotification notification) {
178 | final FocusScopeNode focusScope = FocusScope.of(context);
179 | if (notification.dragDetails != null && focusScope.hasFocus) {
180 | focusScope.unfocus();
181 | }
182 | return false;
183 | },
184 | );
185 | } else {
186 | return scrollableResult;
187 | }
188 | }
189 | ```
190 | Describes the part of the user interface represented by this widget.
191 |
192 | ### buildSlivers(BuildContext context)
193 | ```dart
194 | @override
195 | List<Widget> buildSlivers(BuildContext context) {
196 | Widget sliver = childrenDelegate.build(context);
197 | EdgeInsetsGeometry? effectivePadding = padding;
198 | if (padding == null) {
199 | final MediaQueryData? mediaQuery = MediaQuery.maybeOf(context);
200 | if (mediaQuery != null) {
201 | final EdgeInsets mediaQueryHorizontalPadding =
202 | mediaQuery.padding.copyWith(top: 0.0, bottom: 0.0);
203 | final EdgeInsets mediaQueryVerticalPadding =
204 | mediaQuery.padding.copyWith(left: 0.0, right: 0.0);
205 | effectivePadding = scrollDirection == Axis.vertical
206 | ? mediaQueryVerticalPadding
207 | : mediaQueryHorizontalPadding;
208 | sliver = MediaQuery(
209 | data: mediaQuery.copyWith(
210 | padding: scrollDirection == Axis.vertical
211 | ? mediaQueryHorizontalPadding
212 | : mediaQueryVerticalPadding,
213 | ),
214 | child: sliver,
215 | );
216 | }
217 | }
218 |
219 | if (effectivePadding != null)
220 | sliver = SliverPadding(padding: effectivePadding, sliver: sliver);
221 | return <Widget>[ sliver ];
222 | }
223 | ```
224 | Build the list of widgets to place inside the viewport.
225 |
226 | ### buildViewport(BuildContext context, ViewportOffset offset, AxisDirection axisDirection, List<Widget> slivers)
227 | ```dart
228 | @override
229 | Widget buildViewport(
230 | BuildContext context,
231 | ViewportOffset offset,
232 | AxisDirection axisDirection,
233 | List<Widget> slivers,
234 | ) {
235 | if (shrinkWrap) {
236 | return ShrinkWrappingViewport(
237 | axisDirection: axisDirection,
238 | offset: offset,
239 | slivers: slivers,
240 | clipBehavior: clipBehavior,
241 | );
242 | }
243 | return Viewport(
244 | axisDirection: axisDirection,
245 | offset: offset,
246 | slivers: slivers,
247 | cacheExtent: cacheExtent,
248 | center: center,
249 | anchor: anchor,
250 | clipBehavior: clipBehavior,
251 | );
252 | }
253 | ```
254 | Build the viewport.
255 |
256 | ### debugFillProperties(DiagnosticPropertiesBuilder properties)
257 | ```dart
258 | @override
259 | void debugFillProperties(DiagnosticPropertiesBuilder properties) {
260 | super.debugFillProperties(properties);
261 | properties.add(EnumProperty<Axis>('scrollDirection', scrollDirection));
262 | properties.add(FlagProperty('reverse', value: reverse, ifTrue: 'reversed', showName: true));
263 | properties.add(DiagnosticsProperty<ScrollController>('controller', controller, showName: false, defaultValue: null));
264 | properties.add(FlagProperty('primary', value: primary, ifTrue: 'using primary controller', showName: true));
265 | properties.add(DiagnosticsProperty<ScrollPhysics>('physics', physics, showName: false, defaultValue: null));
266 | properties.add(FlagProperty('shrinkWrap', value: shrinkWrap, ifTrue: 'shrink-wrapping', showName: true));
267 | }
268 | ```
269 | Add additional properties associated with the node.
270 |
271 | ## Code Examples
272 |
273 | #### Example 1:
274 | ```dart
275 | ListView(
276 | children: <Widget>[
277 | ListTile(
278 | leading: Icon(Icons.map),
279 | title: Text('Map'),
280 | ),
281 | ListTile(
282 | leading: Icon(Icons.photo_album),
283 | title: Text('Album'),
284 | ),
285 | ListTile(
286 | leading: Icon(Icons.phone),
287 | title: Text('Phone'),
288 | ),
289 | ],
290 | )
291 | ```
292 |
293 | #### Example 2:
294 | ```dart
295 | ListView.builder(
296 | itemCount: items.length,
297 | itemBuilder: (BuildContext context, int index) {
298 | return ListTile(
299 | title: Text('Item ${items[index]}'),
300 | onTap: () {
301 | print('Tapped on item $index');
302 | },
303 | );
304 | },
305 | )
306 | ```
307 |
308 | #### Example 3:
309 | ```dart
310 | ListView.separated(
311 | itemCount: items.length,
312 | itemBuilder: (BuildContext context, int index) {
313 | return Container(
314 | height: 50,
315 | color: Colors.amber[colorCodes[index]],
316 | child: Center(child: Text('Entry ${items[index]}')),
317 | );
318 | },
319 | separatorBuilder: (BuildContext context, int index) => const Divider(),
320 | )
321 | ```
322 |
323 | #### Example 4:
324 | ```dart
325 | ListView(
326 | scrollDirection: Axis.horizontal,
327 | children: <Widget>[
328 | Container(
329 | width: 160.0,
330 | color: Colors.red,
331 | ),
332 | Container(
333 | width: 160.0,
334 | color: Colors.blue,
335 | ),
336 | Container(
337 | width: 160.0,
338 | color: Colors.green,
339 | ),
340 | Container(
341 | width: 160.0,
342 | color: Colors.yellow,
343 | ),
344 | Container(
345 | width: 160.0,
346 | color: Colors.orange,
347 | ),
348 | ],
349 | )
350 | ```
351 |
352 | #### Example 5:
353 | ```dart
354 | ListView.builder(
355 | physics: BouncingScrollPhysics(),
356 | itemCount: 100,
357 | itemBuilder: (context, index) {
358 | return Card(
359 | child: Padding(
360 | padding: EdgeInsets.all(16.0),
361 | child: Text(
362 | 'Item $index',
363 | style: Theme.of(context).textTheme.headlineSmall,
364 | ),
365 | ),
366 | );
367 | },
368 | )
369 | ```
370 | """
371 |
372 |
373 | def demonstrate_truncation_strategies():
374 | """Demonstrate different truncation strategies."""
375 |
376 | # Create truncator
377 | truncator = AdaptiveTruncator(max_tokens=1000)
378 |
379 | # Get sample documentation
380 | doc = create_mock_flutter_doc()
381 |
382 | print("Flutter Documentation Smart Truncation Demo")
383 | print("=" * 60)
384 | print(f"\nOriginal document size: {len(doc)} characters")
385 | print(f"Estimated tokens: ~{len(doc) // 4}")
386 |
387 | strategies = ["balanced", "signatures", "examples", "minimal"]
388 |
389 | for strategy in strategies:
390 | print(f"\n\n{strategy.upper()} STRATEGY (max 1000 tokens)")
391 | print("-" * 60)
392 |
393 | truncated, metadata = truncator.truncate_with_strategy(
394 | doc, "ListView", "widgets", strategy
395 | )
396 |
397 | print(f"Result size: {len(truncated)} characters")
398 | print(f"Compression ratio: {metadata['compression_ratio']:.1%}")
399 | print(f"Was truncated: {metadata['was_truncated']}")
400 |
401 | # Show what sections survived
402 | sections_kept = []
403 | if "## Description" in truncated:
404 | sections_kept.append("Description")
405 | if "## Constructors" in truncated:
406 | sections_kept.append("Constructors")
407 | if "## Properties" in truncated:
408 | sections_kept.append("Properties")
409 | if "## Methods" in truncated:
410 | sections_kept.append("Methods")
411 | if "## Code Examples" in truncated:
412 | sections_kept.append("Examples")
413 |
414 | print(f"Sections kept: {', '.join(sections_kept)}")
415 |
416 | # Show a preview
417 | print("\nPreview (first 500 chars):")
418 | print("-" * 40)
419 | print(truncated[:500] + "...")
420 |
421 |
422 | def demonstrate_progressive_truncation():
423 | """Show how the algorithm progressively removes content."""
424 |
425 | doc = create_mock_flutter_doc()
426 |
427 | print("\n\nPROGRESSIVE TRUNCATION DEMO")
428 | print("=" * 60)
429 | print("Showing how content is removed as token limit decreases:\n")
430 |
431 | token_limits = [8000, 4000, 2000, 1000, 500, 250]
432 |
433 | for limit in token_limits:
434 | truncator = AdaptiveTruncator(max_tokens=limit)
435 | truncated, metadata = truncator.truncate_with_strategy(
436 | doc, "ListView", "widgets", "balanced"
437 | )
438 |
439 | # Count what survived
440 | sections = {
441 | "Description": "## Description" in truncated,
442 | "Constructors": "## Constructors" in truncated,
443 | "Properties": "## Properties" in truncated,
444 | "Methods": "## Methods" in truncated,
445 | "Examples": "## Code Examples" in truncated,
446 | "Constructor sigs": "```dart" in truncated and "ListView(" in truncated,
447 | "Method sigs": "```dart" in truncated and "build(" in truncated,
448 | }
449 |
450 | kept = [name for name, present in sections.items() if present]
451 |
452 | print(f"Token limit: {limit:>4} | Size: {len(truncated):>5} chars | "
453 | f"Kept: {', '.join(kept)}")
454 |
455 |
456 | def demonstrate_priority_system():
457 | """Show how the priority system works."""
458 |
459 | print("\n\nPRIORITY SYSTEM DEMO")
460 | print("=" * 60)
461 | print("Showing how different content is prioritized:\n")
462 |
463 | truncator = SmartTruncator()
464 |
465 | # Show priority assignments
466 | print("Content Priorities:")
467 | print("-" * 40)
468 |
469 | priorities = [
470 | ("Class description", ContentPriority.CRITICAL),
471 | ("Constructor signatures", ContentPriority.CRITICAL),
472 | ("build() method", ContentPriority.HIGH),
473 | ("Common properties (child, padding)", ContentPriority.HIGH),
474 | ("Other methods", ContentPriority.MEDIUM),
475 | ("Code examples (first 2)", ContentPriority.MEDIUM),
476 | ("Less common properties", ContentPriority.MEDIUM),
477 | ("Private methods", ContentPriority.LOW),
478 | ("Additional examples", ContentPriority.LOW),
479 | ("Inherited members", ContentPriority.MINIMAL),
480 | ]
481 |
482 | for content, priority in priorities:
483 | print(f"{content:<35} -> {priority.name} (priority {priority.value})")
484 |
485 | print("\n\nWhen truncating, content is removed in reverse priority order:")
486 | print("1. MINIMAL content is removed first")
487 | print("2. Then LOW priority content")
488 | print("3. Then MEDIUM priority content")
489 | print("4. HIGH priority content is kept if possible")
490 | print("5. CRITICAL content is always kept")
491 |
492 |
493 | def main():
494 | """Run all demonstrations."""
495 | demonstrate_truncation_strategies()
496 | demonstrate_progressive_truncation()
497 | demonstrate_priority_system()
498 |
499 | print("\n\nCONCLUSION")
500 | print("=" * 60)
501 | print("The smart truncation algorithm ensures that even when severe")
502 | print("token limits are imposed, the most useful information is preserved:")
503 | print("- Constructor signatures for instantiation")
504 | print("- Key method signatures (build, createState)")
505 | print("- Essential properties (child, children)")
506 | print("- Class descriptions for understanding purpose")
507 | print("\nThis makes the documentation useful even when heavily truncated.")
508 |
509 |
510 | if __name__ == "__main__":
511 | main()
```
--------------------------------------------------------------------------------
/docs/planning/initial-vision.md:
--------------------------------------------------------------------------------
```markdown
1 | # Building an MCP Server for Flutter/Dart Documentation in 2025
2 |
3 | The Model Context Protocol (MCP) has become the "USB-C of AI applications" with adoption by OpenAI, Microsoft, and Google DeepMind in 2025. This comprehensive guide provides everything you need to build a Flutter/Dart documentation MCP server similar to Context7, leveraging the latest Python SDK and best practices for deployment and distribution.
4 |
5 | ## MCP fundamentals and current state
6 |
7 | The Model Context Protocol provides a standardized way to connect AI models with external data sources and tools. **Major platforms including OpenAI's ChatGPT Desktop, Microsoft VS Code, and Google DeepMind adopted MCP between November 2024 and March 2025**, establishing it as the de facto standard for AI-tool integration. The protocol operates through three core primitives: tools (model-controlled functions), resources (application-controlled data), and prompts (user-controlled templates). Official documentation lives at modelcontextprotocol.io, with the specification at github.com/modelcontextprotocol/specification. The current protocol revision is 2024-11-05 with active updates through June 2025.
8 |
9 | MCP servers communicate via multiple transport mechanisms including STDIO for local processes, HTTP Server-Sent Events for remote connections, and WebSockets for real-time bidirectional communication. **The Python SDK has emerged as the most popular implementation with 14.8k GitHub stars**, offering both a high-level FastMCP framework and low-level protocol access. The ecosystem now includes over 1,000 open-source connectors and pre-built servers for everything from GitHub and Docker to PostgreSQL and Stripe.
10 |
11 | ## Python SDK implementation guide
12 |
13 | The Python MCP SDK offers two approaches: FastMCP for rapid development and the low-level SDK for maximum control. **FastMCP's decorator-based API makes creating an MCP server remarkably simple**, requiring just a few lines of code to expose Python functions as AI-accessible tools.
14 |
15 | Installation uses the modern `uv` package manager (recommended) or traditional pip:
16 | ```bash
17 | # Using uv (recommended)
18 | curl -LsSf https://astral.sh/uv/install.sh | sh
19 | uv init mcp-server-flutter-docs
20 | cd mcp-server-flutter-docs
21 | uv add "mcp[cli]"
22 |
23 | # Alternative with pip
24 | pip install "mcp[cli]"
25 | ```
26 |
27 | Here's a minimal FastMCP server structure:
28 | ```python
29 | from mcp.server.fastmcp import FastMCP
30 |
31 | mcp = FastMCP("Flutter Docs Server")
32 |
33 | @mcp.tool()
34 | def search_flutter_docs(query: str, topic: str = None) -> str:
35 | """Search Flutter documentation for specific topics"""
36 | # Implementation here
37 | return search_results
38 |
39 | @mcp.resource("flutter://{class_name}")
40 | def get_flutter_class_docs(class_name: str) -> str:
41 | """Get documentation for a Flutter class"""
42 | # Fetch and return class documentation
43 | return class_documentation
44 |
45 | if __name__ == "__main__":
46 | mcp.run()
47 | ```
48 |
49 | **The MCP Inspector tool provides instant visual testing** during development:
50 | ```bash
51 | mcp dev server.py
52 | ```
53 |
54 | For production deployments, the SDK supports context management, lifecycle hooks, and advanced features like image handling and async operations. Integration with Claude Desktop requires adding your server to the configuration file at `~/Library/Application Support/Claude/claude_desktop_config.json`:
55 | ```json
56 | {
57 | "mcpServers": {
58 | "flutter-docs": {
59 | "command": "uv",
60 | "args": ["--directory", "/path/to/project", "run", "server.py"]
61 | }
62 | }
63 | }
64 | ```
65 |
66 | ## Learning from Context7's architecture
67 |
68 | Context7, developed by Upstash, demonstrates best practices for building documentation MCP servers. **Their architecture employs a sophisticated multi-stage pipeline**: ingestion (indexing from GitHub), processing (parsing, enriching with LLMs, cleaning, vectorizing), ranking and filtering (proprietary relevance algorithm), and caching (Redis for sub-second responses).
69 |
70 | The technical implementation uses TypeScript with the official MCP SDK, supports multiple transport protocols (stdio, HTTP, SSE), and distributes via npm as `@upstash/context7-mcp`. **Key innovations include leveraging the llms.txt standard for AI-optimized documentation**, implementing semantic search with vector embeddings, and maintaining version-specific documentation for accuracy.
71 |
72 | Context7's success stems from recognizing that raw documentation isn't sufficient for LLMs - it requires processing, enrichment, and intelligent serving. Their stateless design enables horizontal scaling, while aggressive caching ensures performance. The system strips noise to focus on code examples and essential information, providing exactly what LLMs need for effective code generation.
73 |
74 | ## Accessing Flutter and Dart documentation
75 |
76 | Flutter and Dart documentation presents unique challenges as **neither api.flutter.dev nor api.dart.dev offers official programmatic APIs**. Following Context7's proven approach, we'll implement on-demand web scraping to fetch documentation in real-time, ensuring users always get the most current information.
77 |
78 | **Pub.dev provides the only official API endpoints** for package information:
79 | ```python
80 | import httpx
81 | import gzip
82 |
83 | async def get_all_packages():
84 | """Fetch all package names from pub.dev"""
85 | packages = []
86 | url = "https://pub.dev/api/package-names"
87 |
88 | async with httpx.AsyncClient() as client:
89 | while url:
90 | response = await client.get(url)
91 | data = response.json()
92 | packages.extend(data["packages"])
93 | url = data.get("nextUrl")
94 |
95 | return packages
96 |
97 | async def get_package_info(package_name: str):
98 | """Get detailed package information"""
99 | url = f"https://pub.dev/api/packages/{package_name}"
100 | async with httpx.AsyncClient() as client:
101 | response = await client.get(url)
102 | return response.json()
103 | ```
104 |
105 | **Documentation sources for on-demand fetching**:
106 | - **api.flutter.dev**: Official Flutter API reference with predictable URL patterns
107 | - **api.dart.dev**: Core Dart library documentation
108 | - **Flutter cookbook**: Examples and best practices
109 |
110 | **Context7-inspired processing pipeline**:
111 | 1. **Parse**: Extract code snippets and examples from HTML
112 | 2. **Enrich**: Add explanations using LLMs for better context
113 | 3. **Clean**: Remove navigation, ads, and irrelevant content
114 | 4. **Cache**: Store in Redis for sub-second subsequent responses
115 |
116 | This on-demand approach allows us to claim "supports ALL pub.dev packages" - a powerful marketing message that resonates with developers.
117 |
118 | ## Building your documentation MCP server
119 |
120 | Following Context7's successful architecture, we'll build an on-demand documentation server that fetches and processes documentation in real-time.
121 |
122 | ### Core MCP Server Implementation
123 |
124 | ```python
125 | from mcp.server.fastmcp import FastMCP
126 | import httpx
127 | import redis
128 | from bs4 import BeautifulSoup
129 | import asyncio
130 | from typing import Optional, Dict
131 | import structlog
132 | import re
133 |
134 | mcp = FastMCP("Flutter Docs Server", dependencies=["httpx", "redis", "beautifulsoup4", "structlog"])
135 |
136 | # Redis for caching - Context7 style
137 | redis_client = redis.Redis(decode_responses=True, host='localhost', port=6379)
138 | logger = structlog.get_logger()
139 |
140 | # Rate limiter for respectful scraping
141 | class RateLimiter:
142 | def __init__(self, calls_per_second: float = 2.0):
143 | self.semaphore = asyncio.Semaphore(1)
144 | self.min_interval = 1.0 / calls_per_second
145 | self.last_call = 0
146 |
147 | async def acquire(self):
148 | async with self.semaphore:
149 | elapsed = asyncio.get_event_loop().time() - self.last_call
150 | if elapsed < self.min_interval:
151 | await asyncio.sleep(self.min_interval - elapsed)
152 | self.last_call = asyncio.get_event_loop().time()
153 |
154 | rate_limiter = RateLimiter()
155 |
156 | @mcp.tool()
157 | async def get_flutter_docs(class_name: str, library: str = "widgets") -> Dict:
158 | """Get Flutter class documentation on-demand"""
159 | # Check cache first
160 | cache_key = f"flutter:{library}:{class_name}"
161 | cached = redis_client.get(cache_key)
162 |
163 | if cached:
164 | logger.info("cache_hit", class_name=class_name, library=library)
165 | return {"source": "cache", "content": cached}
166 |
167 | # Rate-limited fetch from Flutter docs
168 | await rate_limiter.acquire()
169 | url = f"https://api.flutter.dev/flutter/{library}/{class_name}-class.html"
170 |
171 | try:
172 | async with httpx.AsyncClient() as client:
173 | response = await client.get(
174 | url,
175 | headers={"User-Agent": "Flutter-MCP-Docs/1.0 (github.com/yourname/flutter-mcp)"}
176 | )
177 | response.raise_for_status()
178 |
179 | # Process HTML - Context7 style pipeline
180 | content = await process_documentation(response.text, class_name)
181 |
182 | # Cache for 24 hours
183 | redis_client.setex(cache_key, 86400, content)
184 |
185 | logger.info("docs_fetched", class_name=class_name, library=library)
186 | return {"source": "live", "content": content}
187 |
188 | except httpx.HTTPError as e:
189 | logger.error("fetch_error", class_name=class_name, error=str(e))
190 | return {"error": f"Could not fetch docs for {class_name}: {str(e)}"}
191 |
192 | @mcp.tool()
193 | async def get_pub_package_docs(package_name: str) -> Dict:
194 | """Get any pub.dev package documentation on-demand"""
195 | cache_key = f"pub:{package_name}"
196 | cached = redis_client.get(cache_key)
197 |
198 | if cached:
199 | return {"source": "cache", "content": cached}
200 |
201 | # Use official pub.dev API
202 | async with httpx.AsyncClient() as client:
203 | response = await client.get(f"https://pub.dev/api/packages/{package_name}")
204 | if response.status_code == 200:
205 | package_data = response.json()
206 | # Process and enrich package documentation
207 | content = format_package_docs(package_data)
208 | redis_client.setex(cache_key, 86400, content)
209 | return {"source": "live", "content": content}
210 |
211 | return {"error": f"Package {package_name} not found"}
212 |
213 | async def process_documentation(html: str, class_name: str) -> str:
214 | """Context7-style documentation processing pipeline"""
215 | soup = BeautifulSoup(html, 'html.parser')
216 |
217 | # 1. Parse - Extract key sections
218 | description = soup.find('section', class_='desc')
219 | constructors = soup.find_all('section', class_='constructor')
220 | properties = soup.find_all('dl', class_='properties')
221 | methods = soup.find_all('section', class_='method')
222 |
223 | # 2. Clean - Remove navigation, scripts, styles
224 | for element in soup.find_all(['script', 'style', 'nav', 'header', 'footer']):
225 | element.decompose()
226 |
227 | # 3. Enrich - Format for AI consumption
228 | markdown = f"""# {class_name}
229 |
230 | ## Description
231 | {clean_text(description) if description else 'No description available'}
232 |
233 | ## Constructors
234 | {format_constructors(constructors)}
235 |
236 | ## Properties
237 | {format_properties(properties)}
238 |
239 | ## Methods
240 | {format_methods(methods)}
241 |
242 | ## Code Examples
243 | {extract_code_examples(soup)}
244 | """
245 |
246 | return markdown
247 | ```
248 |
249 | ### Key Implementation Features
250 |
251 | **1. Smart Caching Strategy**: Like Context7, cache processed documentation:
252 |
253 | ```python
254 | def get_cache_key(doc_type: str, identifier: str, version: str = None) -> str:
255 | """Generate cache keys for different documentation types"""
256 | if version:
257 | return f"{doc_type}:{identifier}:{version}"
258 | return f"{doc_type}:{identifier}"
259 |
260 | # Cache TTL strategy
261 | CACHE_DURATIONS = {
262 | "flutter_api": 86400, # 24 hours for stable APIs
263 | "pub_package": 43200, # 12 hours for packages (may update more frequently)
264 | "cookbook": 604800, # 7 days for examples
265 | "stackoverflow": 3600, # 1 hour for community content
266 | }
267 | ```
268 |
269 | **2. Intelligent URL Pattern Detection**:
270 |
271 | ```python
272 | def resolve_flutter_url(query: str) -> Optional[str]:
273 | """Intelligently resolve documentation URLs from queries"""
274 | # Common Flutter class patterns
275 | patterns = {
276 | r"^(\w+)$": "https://api.flutter.dev/flutter/widgets/{0}-class.html",
277 | r"^material\.(\w+)$": "https://api.flutter.dev/flutter/material/{0}-class.html",
278 | r"^cupertino\.(\w+)$": "https://api.flutter.dev/flutter/cupertino/{0}-class.html",
279 | r"^dart:(\w+)\.(\w+)$": "https://api.dart.dev/stable/dart-{0}/{1}-class.html",
280 | }
281 |
282 | for pattern, url_template in patterns.items():
283 | if match := re.match(pattern, query, re.IGNORECASE):
284 | return url_template.format(*match.groups())
285 |
286 | return None
287 | ```
288 |
289 | **3. Fallback Search When Direct URL Fails**:
290 |
291 | ```python
292 | @mcp.tool()
293 | async def search_flutter_docs(query: str) -> Dict:
294 | """Search across all Flutter/Dart documentation sources"""
295 | results = []
296 |
297 | # Try direct URL resolution first
298 | if url := resolve_flutter_url(query):
299 | if doc := await fetch_and_process(url):
300 | results.append(doc)
301 |
302 | # Search pub.dev packages
303 | pub_results = await search_pub_packages(query)
304 | results.extend(pub_results[:5]) # Top 5 packages
305 |
306 | # Search Flutter cookbook
307 | cookbook_results = await search_cookbook(query)
308 | results.extend(cookbook_results[:3])
309 |
310 | return {
311 | "query": query,
312 | "results": results,
313 | "total": len(results)
314 | }
315 | ```
316 |
317 | ## Implementation Timeline
318 |
319 | **MVP - 4 Hours (Context7-style):**
320 | 1. Basic FastMCP server with Redis caching
321 | 2. On-demand Flutter API documentation fetching
322 | 3. Simple HTML to Markdown processing
323 | 4. Test with Claude Desktop
324 |
325 | **Week 1 - Core Features:**
326 | - Add pub.dev package support ("supports ALL packages!")
327 | - Implement smart URL resolution
328 | - Add rate limiting and error handling
329 | - Create search functionality
330 |
331 | **Week 2 - Polish & Launch:**
332 | - Add Flutter cookbook integration
333 | - Implement Context7-style enrichment
334 | - Write comprehensive documentation
335 | - Package for npm/pip distribution
336 | - Launch on r/FlutterDev
337 |
338 | **Future Enhancements:**
339 | - Stack Overflow integration
340 | - Version-specific documentation
341 | - Example code extraction
342 | - Performance metrics dashboard
343 |
344 | ## Deployment and distribution strategies
345 |
346 | Package your MCP server for easy distribution across multiple platforms. **Like Context7, the server is lightweight and fetches documentation on-demand**, ensuring fast installation and minimal disk usage.
347 |
348 | ### Distribution Methods
349 |
350 | Create a lightweight Python package:
351 | ```toml
352 | # pyproject.toml
353 | [project]
354 | name = "mcp-flutter-docs"
355 | version = "1.0.0"
356 | dependencies = [
357 | "mcp[cli]",
358 | "httpx",
359 | "beautifulsoup4",
360 | "aiofiles"
361 | ]
362 |
363 | [project.scripts]
364 | mcp-flutter-docs = "mcp_flutter_docs.server:main"
365 | ```
366 |
367 | Docker image with Redis:
368 | ```dockerfile
369 | FROM python:3.11-slim
370 | WORKDIR /app
371 |
372 | # Install Redis (or use external Redis service)
373 | RUN apt-get update && apt-get install -y redis-server
374 |
375 | COPY requirements.txt .
376 | RUN pip install -r requirements.txt
377 | COPY . .
378 |
379 | # Start Redis and the MCP server
380 | CMD redis-server --daemonize yes && python server.py
381 | ```
382 |
383 | **Provide multiple installation methods** in your documentation:
384 | ```json
385 | // Claude Desktop configuration
386 | {
387 | "mcpServers": {
388 | "flutter-docs": {
389 | "command": "npx",
390 | "args": ["-y", "@yourorg/mcp-flutter-docs"]
391 | }
392 | }
393 | }
394 |
395 | // Docker alternative
396 | {
397 | "mcpServers": {
398 | "flutter-docs": {
399 | "command": "docker",
400 | "args": ["run", "-i", "--rm", "ghcr.io/yourorg/mcp-flutter-docs:latest"]
401 | }
402 | }
403 | }
404 | ```
405 |
406 | Use semantic versioning strictly: MAJOR for breaking changes, MINOR for new features, PATCH for bug fixes. Automate releases with GitHub Actions and maintain comprehensive documentation including installation instructions, tool descriptions, and troubleshooting guides.
407 |
408 | ## Marketing on Reddit effectively
409 |
410 | Reddit marketing requires patience and authentic community participation. **The Flutter community on r/FlutterDev (154k members) values technical depth, open-source contributions, and genuine problem-solving** over promotional content.
411 |
412 | Follow the 80/20 rule religiously: 80% genuine community participation, 20% promotion. Build credibility over 6-8 weeks before launching:
413 | - Weeks 1-2: Join communities, start answering technical questions
414 | - Weeks 3-4: Share expertise without mentioning your project
415 | - Weeks 5-6: Subtle mentions in relevant contexts
416 | - Weeks 7+: Direct promotion with established credibility
417 |
418 | **Technical founders posting personally outperform marketing teams consistently**. Frame your launch as a learning experience:
419 | ```
420 | Title: "Built an MCP server to integrate Claude with Flutter development - lessons from 6 months of daily use"
421 |
422 | Body:
423 | - Personal intro as Flutter developer
424 | - Problem statement (AI coding limitations)
425 | - Technical approach with architecture decisions
426 | - Real usage examples with screenshots
427 | - GitHub link and documentation
428 | - Request for feedback and contributions
429 | ```
430 |
431 | Avoid common mistakes: new accounts with no karma, obvious marketing language, copy-paste promotion across subreddits, and ignoring community norms. Target r/FlutterDev for primary launch, r/programming for technical deep-dive, and r/SideProject for direct promotion.
432 |
433 | ## Common pitfalls and debugging strategies
434 |
435 | MCP server development presents unique challenges. **Transport mismatches cause the most common issues** - ensure your server supports the transport your client expects. Debug with MCP Inspector first before testing with actual clients.
436 |
437 | Handle errors gracefully in FastMCP:
438 | ```python
439 | @mcp.tool()
440 | def risky_operation(data: str) -> str:
441 | try:
442 | if not data:
443 | raise ValueError("Data cannot be empty")
444 | # Process data
445 | return result
446 | except ValueError as e:
447 | # FastMCP converts to proper MCP error response
448 | raise e
449 | except Exception as e:
450 | # Log for debugging
451 | logger.error(f"Unexpected error: {e}")
452 | raise Exception("Operation failed")
453 | ```
454 |
455 | **Common debugging issues include**:
456 | - Permission errors: Ensure proper file access and API keys
457 | - Transport conflicts: Match server and client transport types
458 | - Serialization problems: Validate JSON schema compliance
459 | - Memory leaks: Implement proper cleanup in lifecycle hooks
460 | - Rate limiting: Add retry logic with exponential backoff
461 |
462 | Monitor your server with structured logging:
463 | ```python
464 | import structlog
465 |
466 | logger = structlog.get_logger()
467 |
468 | @mcp.tool()
469 | async def fetch_docs(query: str) -> str:
470 | logger.info("fetch_docs.start", query=query)
471 | try:
472 | result = await process_query(query)
473 | logger.info("fetch_docs.success", query=query, result_length=len(result))
474 | return result
475 | except Exception as e:
476 | logger.error("fetch_docs.error", query=query, error=str(e))
477 | raise
478 | ```
479 |
480 | ## Conclusion
481 |
482 | Building an MCP server for Flutter/Dart documentation following Context7's proven approach combines simplicity with effectiveness. The on-demand web scraping architecture provides the best of both worlds - always up-to-date documentation with fast cached responses.
483 |
484 | **Key Advantages of the Context7-Style Approach**:
485 | - **Always Current**: Real-time fetching ensures documentation is never outdated
486 | - **Lightweight**: Small package size with minimal dependencies
487 | - **Marketing Power**: "Supports ALL pub.dev packages" - no limitations
488 | - **Fast Responses**: Redis caching provides sub-second performance after first fetch
489 | - **Simple Architecture**: No complex ingestion pipelines or database management
490 |
491 | By following Context7's successful model and adapting it for the Flutter ecosystem, we can deliver immediate value to developers. The 4-hour MVP timeline demonstrates that effective tools don't require months of development - they require understanding real developer needs and implementing proven patterns.
492 |
493 | Focus on solving the core problem: making Flutter documentation instantly accessible within AI coding assistants. Start with the basic on-demand fetching, add intelligent caching, and ship quickly. The Flutter community's enthusiasm for developer tools combined with the MCP ecosystem's rapid growth creates the perfect opportunity for this project.
```