flutter MCP server
Commit Oct 19, 2026Provides AI coding agents with live Flutter, Dart SDK, and pub.dev documentation via scraped markdown and a local search index.
Also known as KEEPEE/flutter-mcp
- Stars
- 0
- Tools
- 6
- Downloads, 30 days
- —
- Latest version
- —
- Last commit
- Oct 19, 2026
- License
- MIT
What is flutter MCP?
flutter provides AI coding agents with live Flutter, Dart SDK, and pub.dev documentation via scraped markdown and a local search index. It exposes 6 tools, such as flutter_docs, flutter_search, flutter_mentions.
Summary generated from the project's README
About flutter MCP server
Providing AI coding agents with live Flutter, Dart SDK, and pub.dev documentation is what this server enables. It replaces outdated model training data with real-time scraped markdown from official sources, ensuring generated code uses current APIs rather than deprecated ones. Developers can use it with standard MCP clients like Claude Desktop, Cursor, or Cline via a local stdio connection.
The server includes a local search index covering approximately 4,500 classes across 46 libraries, which is rebuilt every seven days. It uses a SQLite TTL cache to make repeated lookups instant and includes a politeness layer to respect robots.txt and crawl delays. Users can install and run the server using a single command with uvx, requiring Python 3.10 or newer.
Specific tools allow for resolving identifiers like material.AppBar, performing ranked name searches, and parsing special mentions. The flutter_mentions tool can even resolve version constraints like provider:^6.0.0 to the newest satisfying release from pub.dev. This makes it a specialized utility for mobile and web developers working within the Dart ecosystem.
What you can do with it
- 1Resolve specific API documentationAn AI agent can fetch the exact markdown documentation for a specific Flutter widget or Dart class.
- 2Search for SDK classesA developer can ask an AI to find the correct class or mixin name using a fuzzy search through the local index.
- 3Parse versioned package mentionsThe agent can automatically resolve mentions in text that include specific version constraints for pub.dev packages.
- 4Retrieve package metadataAn AI can look up the README and current version details for any package hosted on pub.dev.
Written from the project's README. Every use case is checked against a quote from it.
flutter MCP tools 6
Detected from the READMEflutter_docsflutter_searchflutter_mentionspub_packageflutter_statushealth_check
| flutter_docs | — | |
| flutter_search | — | |
| flutter_mentions | — | |
| pub_package | — | |
| flutter_status | — | |
| health_check | — |
Configuration
FLUTTER_DOCS_MCP_CACHE_DIRFLUTTER_DOCS_MCP_POLITENESS_DISABLED
| Name | Description | |
|---|---|---|
| FLUTTER_DOCS_MCP_CACHE_DIR | — | |
| FLUTTER_DOCS_MCP_POLITENESS_DISABLED | — |
Automated checks
Ran Nov 14, 2026Full report →Not verified yet. A server needs a score of 80, no serious finding, and enough data for us to check. How checks work · What to fix for more points
- Source repository linked and reachablekeepee/flutter-mcp is public and reachablegithub.com/keepee/flutter-mcp ↗Pass
- Package names this repository as its sourceNot checked
- Build provenance (Sigstore attestation)Not checked
- Open source license (OSI)MITspdx.org/licenses/MIT.html ↗Pass
- Commit or release in the last yearLast commit or release 2026-10-19github.com/keepee/flutter-mcp ↗Pass
- OpenSSF ScorecardNot checked
- Package name not close to a popular packageNot checked
- No critical advisory in the package or its dependenciesNot checked
- No hidden instructions to the model6 tool descriptions and the README read; no override phrases, credential paths or hidden charactersPass
- Destructive tools are labelledNo tool named like delete, drop or execPass
- Remote endpoint answers the MCP handshakeNot checked
README
View on GitHubflutter-mcp
Live Flutter, Dart SDK and pub.dev documentation for AI coding agents. The MCP server inside is published as flutter-docs-mcp; the repository is flutter-mcp.
An MCP (Model Context Protocol) server that gives AI coding agents live Flutter, Dart SDK and pub.dev documentation instead of whatever the model happens to remember. Pages are scraped from the official docs at request time, cached locally, and returned as clean markdown — so generated Flutter code targets real, current APIs rather than deprecated or invented ones.
- Flutter classes — scraped live from api.flutter.dev (widgets, material, cupertino, foundation, services, rendering, …)
- Dart SDK classes — scraped live from api.dart.dev (dart:core, dart:async, dart:collection, dart:io, …)
- pub.dev packages — metadata + README from the pub.dev API
- Local search index over ~4,500 Flutter / Dart SDK classes (46 libraries,
dart:convert/dart:isolate/dart:ffi/dart:typed_data/dart:uiincluded), rebuilt at most every 7 days, with a stale fallback when the network fails - SQLite TTL cache so repeated lookups are instant
- Politeness layer — robots.txt (RFC 9309), per-host throttle incl.
Crawl-delay,Retry-After, conditional GET and request budgets (details)
The six tools
| Tool | What it does |
|---|---|
flutter_docs |
Resolve one identifier (ListView, material.AppBar, dart:async.Future, pub:dio) to a single documentation page as markdown. |
flutter_search |
Ranked name search over the local index of all Flutter + Dart SDK classes, enums, mixins and typedefs. |
flutter_mentions |
Parse every @flutter_mcp … mention in a text and resolve each one — including version constraints like provider:^6.0.0 or dio:>=5.0.0 <6.0.0 — to exactly one docs entry. |
pub_package |
pub.dev package metadata (version, publisher, likes, pub points) plus its README as markdown. |
flutter_status |
Real health check: index size/age, cache stats, live probes of api.flutter.dev and pub.dev, and politeness counters. |
health_check |
Server liveness and version. |
Every tool returns a plain dict. Failures come back as {"error": …, "suggestion": …} — a bad lookup never raises into the MCP layer, and no tool delegates to another tool.
Requirements
- Python 3.10 or newer (developed and tested on 3.12)
uvfor the one-command install below (uvxships with it)- The MCP Python SDK 1.x is pinned (
mcp>=1.2.0,<2.0); this package does not support the mcp 2.x rename ofFastMCP
Install & run
One command — no checkout, no token
uvx --from git+https://github.com/KEEPEE/flutter-mcp.git flutter-docs
This builds the package in an isolated environment and starts the stdio MCP server. It prints nothing on purpose: stdout is the protocol channel. Stop it with Ctrl-C.
After the repository is updated, force uv to re-resolve the commit:
uvx --refresh --from git+https://github.com/KEEPEE/flutter-mcp.git flutter-docs
Local checkout — fastest startup, editable while developing
git clone https://github.com/KEEPEE/flutter-mcp.git
cd flutter-mcp
python3 -m venv .venv
.venv/bin/pip install -e .
.venv/bin/flutter-docs # stdio MCP server
MCP client configuration
Generic stdio client (Claude Desktop, Cursor, Cline, …)
{
"mcpServers": {
"flutter-docs": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/KEEPEE/flutter-mcp.git",
"flutter-docs"
]
}
}
}
With an explicit cache directory (any env you set is passed straight through to the server):
{
"mcpServers": {
"flutter-docs": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/KEEPEE/flutter-mcp.git",
"flutter-docs"
],
"env": {
"FLUTTER_DOCS_MCP_CACHE_DIR": "/tmp/flutter-docs-cache"
}
}
}
}
DeepSeek Harness (cordis-style plugin list)
- id: mcp-flutter-docs
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: flutter-docs
transport: stdio
command: uvx
args:
[
'--from',
'git+https://github.com/KEEPEE/flutter-mcp.git',
'flutter-docs'
]
To skip the build at client start, point command at the console script of a checkout instead — command: /path/to/flutter-mcp/.venv/bin/flutter-docs with args: [].
Tools reference
flutter_docs(identifier, topic=None, max_tokens=8000)
Resolves an identifier to one documentation page. Identifier forms, tried in this order:
| Form | Meaning |
|---|---|
pub:dio, pub:dio:5.11.1 |
pub.dev package: description + README |
dart:async.Future |
Dart SDK class on api.dart.dev (the library keeps its colon) |
material.AppBar, widgets.ListView |
Flutter class on api.flutter.dev, library given |
ListView |
exact match in the local index → widgets/material fallback → pub.dev |
topic keeps only the section whose heading contains it (methods, properties, examples, constructors, …) plus the class title; if nothing matches, the full page is returned with a note listing the available headings. max_tokens is a rough budget (1 token ≈ 4 characters): the markdown is cut at a line boundary.
truncated is true whenever the returned content is not the whole page — a token-budget cut, a topic filter, or both — and truncation then says which, names the sections that were dropped, and gives tokens (returned), source_tokens (the whole page) and tokens_payload (the payload including its own [truncated: …] marker).
Returns {"type", "identifier", "url", "title", "content", "truncated", "tokens", "source_tokens", "max_tokens", "cached"}, plus truncation when something was cut and an optional note.
// flutter_docs({ "identifier": "material.AppBar", "topic": "properties", "max_tokens": 300 })
{
"type": "flutter_class",
"identifier": "material.AppBar",
"url": "https://api.flutter.dev/flutter/material/AppBar-class.html",
"title": "AppBar class",
"content": "# AppBar class\n\nA Material Design app bar.\n\nAn app bar consists of a toolbar and potentially other widgets, such as a\n[TabBar](https://api.flutter.dev/flutter/material/TabBar-class.html) and a [FlexibleSpaceBar](https://… [truncated: showing ~273 of ~7367 estimated tokens]",
"truncated": true,
"tokens": 273,
"source_tokens": 7367,
"max_tokens": 300,
"cached": true,
"truncation": {
"reasons": ["topic_filter", "token_budget"],
"sections_dropped": ["Troubleshooting", "Constructors", "Methods", "Operators", "Static Methods"],
"tokens_returned": 273,
"tokens_source": 7367,
"tokens_payload": 286,
"max_tokens": 300,
"explanation": "topic 'properties' dropped 6 section(s): …; token budget 300: body cut at a line boundary, ~273 of ~7367 estimated tokens returned"
}
}
// flutter_docs({ "identifier": "dart:async.Future", "topic": "methods", "max_tokens": 250 })
{
"type": "dart_class",
"identifier": "dart:async.Future",
"url": "https://api.dart.dev/dart-async/Future-class.html",
"content": "# Future<T> class abstract interface\n\nThe result of an asynchronous computation. … ## Methods\n\nasStream() → Stream<T> …",
"truncated": true,
"cached": true,
"title": "Future< T > class abstract interface"
}
A name that resolves nowhere reports every place it looked:
// flutter_docs({ "identifier": "NopeXYZ" })
{
"error": "could not resolve 'NopeXYZ': flutter widgets: not found (HTTP 404): https://api.flutter.dev/flutter/widgets/NopeXYZ-class.html; flutter material: not found (HTTP 404): …; pub.dev: not found (HTTP 404): …",
"suggestion": "try flutter_search('NopeXYZ') to find the right name"
}
flutter_search(query, limit=8)
Ranks the local index by name similarity (exact > prefix > substring > fuzzy). Returns {"query", "count", "results": [{name, library, kind, url, score}], "index_stale"}.
// flutter_search({ "query": "text field", "limit": 3 })
{
"query": "text field",
"count": 3,
"results": [
{ "name": "TextField", "library": "material", "kind": "class", "url": "https://api.flutter.dev/flutter/material/TextField-class.html", "score": 0.9474 },
{ "name": "TextFormField", "library": "material", "kind": "class", "url": "https://api.flutter.dev/flutter/material/TextFormField-class.html", "score": 0.7826 },
{ "name": "BitField", "library": "foundation", "kind": "class", "url": "https://api.flutter.dev/flutter/foundation/BitField-class.html", "score": 0.6667 }
],
"index_stale": false
}
flutter_mentions(text, max_tokens=4000)
Parses every @flutter_mcp … mention in a text and resolves each one, returning exactly one entry per mention, in document order — a failing mention never removes, duplicates or leaks state into another one.
| Mention | Resolved as |
|---|---|
@flutter_mcp provider |
pub.dev package, latest release |
@flutter_mcp provider:^6.0.0 |
caret constraint → newest published release satisfying it |
@flutter_mcp provider:6.1.5 |
exact published release |
@flutter_mcp dio:>=5.0.0 <6.0.0 |
range → newest published release satisfying it |
@flutter_mcp provider:latest |
explicit latest |
@flutter_mcp pub:dio, pub:dio:5.4.0 |
explicit package form |
@flutter_mcp material.AppBar |
Flutter class, library given |
@flutter_mcp dart:async.Future |
Dart SDK class |
@flutter_mcp Container |
plain name (index → widgets/material → pub.dev) |
Version constraints are answered from pub.dev's own list of published releases. An exact pin that was never published is reported as missing with the nearest published releases — never silently replaced by a different version — and a resolved range always states both the constraint that was requested and the version it resolved to. Pre-releases are excluded unless the constraint mentions one.
Returns {"mentions": n, "max_tokens": …, "results": […]}; {"mentions": 0, …, "note": …} when the text has no mention.
// flutter_mentions({ "text": "Use @flutter_mcp provider:^6.0.0 for state and @flutter_mcp material.AppBar for the bar.", "max_tokens": 120 })
{
"mentions": 2,
"max_tokens": 120,
"results": [
{
"mention": "@flutter_mcp provider:^6.0.0",
"identifier": "provider",
"requested_constraint": "^6.0.0",
"type": "pub_package",
"version": "6.1.5+1",
"url": "https://pub.dev/api/packages/provider/versions/6.1.5+1",
"title": "provider 6.1.5+1",
"content": "A wrapper around InheritedWidget to make them easier to use and more reusable.\n\n\n[truncated: showing ~19 of ~6240 estimated tokens]",
"truncated": true, "tokens": 19, "source_tokens": 6240, "max_tokens": 120,
"cached": true,
"note": "constraint '^6.0.0' resolved to 6.1.5+1: the newest published release satisfying it"
},
{
"mention": "@flutter_mcp material.AppBar",
"identifier": "material.AppBar",
"requested_constraint": null,
"type": "flutter_class",
"version": null,
"url": "https://api.flutter.dev/flutter/material/AppBar-class.html",
"title": "AppBar class",
"content": "# AppBar class\n\nA Material Design app bar. … [truncated: showing ~80 of ~7367 estimated tokens]",
"truncated": true, "tokens": 80, "source_tokens": 7367, "max_tokens": 120,
"cached": true
}
]
}
A mention that resolves nowhere is a per-mention failure, not a missing entry:
// results entry for "@flutter_mcp NotARealThing"
{
"mention": "@flutter_mcp NotARealThing",
"identifier": "NotARealThing",
"type": "not_found",
"version": null,
"error": "could not resolve 'NotARealThing': flutter widgets: not found (HTTP 404): …",
"suggestion": "try flutter_search('NotARealThing') to find the right name"
}
pub_package(package_name, version=None, max_tokens=6000)
pub.dev metadata plus the README. package_name is exact and case-sensitive; version pins a release. Returns {"name", "version", "description", "publisher", "likes", "pub_points", "url", "readme", "truncated", "tokens", "source_tokens", "max_tokens", "cached"} (plus truncation when the README was cut); publisher / likes / pub_points may be null when pub.dev does not report them.
// pub_package({ "package_name": "dio", "max_tokens": 150 })
{
"name": "dio",
"version": "5.11.1",
"description": "A powerful HTTP networking package,\nsupports Interceptors,\nAborting and canceling a request,\nCustom adapters, Transformers, etc.\n",
"publisher": "flutter.cn",
"likes": "8.36k",
"pub_points": 160,
"url": "https://pub.dev/api/packages/dio",
"readme": "# dio\n\n… [truncated: showing ~137 of ~7837 estimated tokens]",
"truncated": true,
"cached": true
}
flutter_status()
Probes api.flutter.dev and pub.dev with a light GET (10 s timeout) and reports the index and cache state. overall is ok, degraded or error. The politeness block is diagnostics only and never changes overall.
// flutter_status()
{
"server": "flutter-docs-mcp",
"version": "0.3.0",
"checks": {
"search_index": { "status": "ok", "entries": 3503, "built_at": "2026-10-06T08:46:45.893682+00:00", "stale": false },
"cache": { "status": "ok", "entries": 5, "expired": 0 },
"api_flutter_dev": { "status": "ok", "http_status": 200 },
"pub_dev": { "status": "ok", "http_status": 200 }
},
"overall": "ok",
"politeness": {
"status": "ok", "disabled": false,
"requests": 3, "robots_requests": 2, "robots_rows": 2, "robots_fetches": 2, "robots_cache_hits": 1,
"blocked_by_robots": 0, "throttle_waits": 3, "throttle_sleep_s": 1.203,
"host_delays": { "api.flutter.dev": 0.0, "pub.dev": 0.0 },
"budgets": { "fetch:api.flutter.dev": [2, 6] },
"budget_denied": 0, "conditional": 0, "revalidated_304": 0,
"retries_429": 0, "retries_transport": 0, "stalls": 0, "errors": 0
}
}
health_check()
{"status": "ok", "server": "flutter-docs-mcp", "version": "0.3.0"} — no network, no cache; safe as a liveness probe.
Replacing the legacy flutter-mcp-server
This server exists so the third-party flutter-mcp-server (0.1.1, upstream
flutter-mcp/flutter-mcp) can be
removed from our MCP client configuration. We do not maintain or patch that
package; the three defects below are what we observed in its code while
checking whether it could cover our use case, and they are the reason we wrote
this one instead:
flutter_docscrashes on the pub.dev path.server.py:1562callscreate_truncator(tokens), but that package'struncation.py:250definescreate_truncator()with no parameters, so any call that passestokensfails withError executing tool flutter_docs: create_truncator() takes 0 positional arguments but 1 was given. The same block then calls atruncator.truncate(content)method that does not exist.process_flutter_mentionsinvents results. Its version-constrained pub branch does notcontinueafter resolving the package, so execution falls through into the shared "process the result fromflutter_docs" block with the previous iteration'sdoc_resultstill bound. The output contains a phantom second entry (type: flutter_widget, empty content) for a mention that was never in the text.- Its Flutter-class path needs a writable Flutter SDK. It shells out to a
local
/opt/fluttercheckout, whosebin/cacheis read-only on our hosts, so Flutter class lookups fail there. This server scrapes api.flutter.dev and api.dart.dev instead and needs no Flutter installation.
flutter_mentions is the replacement for process_flutter_mentions: same
mention grammar, one result per mention, and version constraints resolved
against pub.dev's published release list. The truncation contract is also
stricter here — truncated, tokens and source_tokens describe the payload
that was actually returned (see flutter_docs).
Caching & politeness
Cache
Everything lives in one directory: ~/.cache/flutter-docs-mcp by default, overridable with FLUTTER_DOCS_MCP_CACHE_DIR.
| File | Contents | TTL |
|---|---|---|
cache.db |
fetched pages (parsed result + raw body + ETag / Last-Modified), the search index, pub.dev data |
docs 7 days, pub.dev 1 day, index 7 days |
robots.db |
the politeness layer's robots.txt cache | 7 days per host |
Delete the files to force a full refresh. A cache problem is never a tool failure: if the directory is unwritable the server simply runs without a cache and says so in flutter_status().checks.cache.
Politeness
Every outbound request — fetch_*, the index build and the flutter_status probes — goes through one small internal module, src/flutter_docs_mcp/politeness.py (stdlib + httpx, no extra dependency):
- robots.txt is read and obeyed. Rules are matched per RFC 9309 (
*, trailing$,%2A/%24literals, mergedUser-agentgroups, most-specific match wins,AllowbeatsDisallowon a tie).urllib.robotparseris deliberately not used: on Python < 3.14 it ignores wildcards and would report rules such asDisallow: /pypi/*/jsonas allowed. A disallowed URL returns{"error": "… blocked by robots.txt …"}— no request is made and no exception escapes. Files are cached 7 days per host, including negative results (404 / 403 / 5xx), so a host costs one robots request per week. - Per-host throttle, including
Crawl-delay. Requests to one host are spaced 0.35–0.9 s apart, or by the site's ownCrawl-delaywhen it declares one. A cold index build is ~31 requests, which is exactly where that delay is felt — that is the point. 429/503/504are handled.Retry-Afteris honoured to the second; without it the per-host delay is escalated and the request is retried once. A response that stalls past the read timeout escalates the delay instead of being retried blindly.- Conditional GET.
ETag/Last-Modifiedand the raw body are stored next to the cached result, so refreshing an expired entry costs a304instead of a full download. - Request budgets. One
fetch_*call may make at most 6 requests per host — the robots fetch, every retry and every redirect hop all pay. A coldpub_packagelegitimately costs 3 (robots + API JSON + package page). An index build gets 40 layer calls per host; it charges one unit per page and the layer does robots and any hop inside it (measured cold build: 24 units on api.flutter.dev, 7 on api.dart.dev). - Host allowlist. Only
api.flutter.dev,api.dart.devandpub.devcan be contacted, so a malformed identifier or a surprising redirect cannot turn a docs lookup into a request to somebody else's site.
The layer never raises and never changes a tool's return shape. flutter_status reports its counters under the top-level politeness key.
Opt-out
export FLUTTER_DOCS_MCP_POLITENESS_DISABLED=1
This single switch turns off robots.txt, the throttle, retries and conditional GET at once. Use it at your own risk: you take over responsibility for respecting each site's crawling rules, and you are far more likely to be rate-limited or blocked. There is no partial opt-out.
Development
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest -q # 255 offline tests; fixtures in tests/fixtures/
.venv/bin/python scripts/politeness_smoke.py /tmp/flutter-docs-smoke-cache
.venv/bin/python scripts/e2e_mcp_test.py
pytestis fully offline: HTTP is simulated withhttpx.MockTransportand time/jitter are injected, so the suite is deterministic and runs in seconds.scripts/politeness_smoke.py <cache-dir>is a live measurement, not a test: it drives the real tools against the real docs sites and prints what the politeness layer did (requests, robots, throttle, conditional GET, budgets).scripts/e2e_mcp_test.pyspawns the installedflutter-docsconsole script and speaks newline-delimited JSON-RPC to it (initialize→tools/list→ every tool → a negative case →health_check). Exit code 0 means every check passed. SetFLUTTER_DOCS_MCP_E2E_CMDto run a different server command.
Troubleshooting
The first flutter_docs call takes half a minute. That is the cold search-index build: ~47 page fetches across api.flutter.dev (1 index page + 25 libraries) and api.dart.dev (1 index page + 20 libraries), spaced by the per-host throttle (a measured cold run finished in ~26-30 s). It happens once every 7 days; later calls hit the cached index. Delete cache.db and you pay for it again.
request budget exhausted for scope 'fetch:pub.dev'. One fetch_* call hit its cap of 6 requests to that host. Redirect chains, 429 retries and transport retries all consume the budget, so this usually means the site redirected more than expected or the connection kept failing. The tool returns an error dict instead of hammering the host; flutter_status().politeness.budgets shows how much each scope spent.
"partial": true, or fewer search results than expected. An index build ran out of its 40-calls-per-host budget and stopped early, returning a partial index with a partial_reason such as request budget of 40 per host exhausted for index:api.flutter.dev; the index is incomplete. A partial index is not cached, so the next run rebuilds it. flutter_search still works — it just knows fewer classes — and flutter_status().checks.search_index.entries tells you how many it has.
"stale": true / index_stale: true. The rebuild failed (offline, DNS failure, 5xx) and the server fell back to the older cached index instead of failing the lookup.
Nothing is cached and overall is degraded. Read flutter_status().checks.cache.error — an unwritable FLUTTER_DOCS_MCP_CACHE_DIR (read-only mount, missing permission) is the usual cause. Tools keep working without a cache; they are just slower and noisier on the network.
A lookup returns blocked by robots.txt. The site's rules disallow that path for this user agent, and the request was not sent. Fetch the page yourself, or accept the consequences of the opt-out switch above.
Support development
flutter-mcp is built and maintained by Michal in his spare time. If it saves you time or makes your team's docs easier to work with, a coffee (or more) would mean a lot — every contribution helps keep the project moving. 🙏
Pay via Revolut: revolut.me/michal4zvc
flutter MCP questions
What tools does flutter MCP provide?
It exposes 6 tools, including flutter_docs, flutter_search, flutter_mentions, pub_package, flutter_status.
How does the server ensure documentation is current?
The server scrapes pages from official documentation sites at request time and uses a local SQLite TTL cache for repeated lookups.
Can I use specific version constraints for pub.dev packages?
Yes, the server can resolve mentions with caret constraints, exact versions, or ranges to the newest published release satisfying them.