diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d5545ba205..5c832c4db1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -822,6 +822,52 @@ jobs:
sdk/python/dist/*.whl
sdk/python/dist/*.tar.gz
+ # ==============
+ # Build flet-mcp
+ # ==============
+ build_flet_mcp:
+ name: Build flet-mcp Python package
+ runs-on: ubuntu-latest
+ env:
+ PYPI_VER: ${{ needs.build_flet_package.outputs.PYPI_VER }}
+ needs:
+ - python_tests
+ - build_flet_package
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup uv
+ uses: astral-sh/setup-uv@v6
+
+ - name: Build docs search index
+ working-directory: ${{ env.SDK_PYTHON }}/packages/flet
+ run: uv run --group docs mkdocs build
+
+ - name: Build MCP data
+ working-directory: ${{ env.SDK_PYTHON }}
+ run: |
+ uv run --group mcp-build flet mcp build \
+ --examples examples \
+ --docs packages/flet/site/search/search_index.json \
+ --output packages/flet-mcp/src/flet_mcp/data/
+
+ - name: Build Python package
+ working-directory: ${{ env.SDK_PYTHON }}
+ run: |
+ source "$SCRIPTS/common.sh"
+ patch_python_package_versions
+ uv build --package flet-mcp
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: flet-mcp-python-distribution
+ if-no-files-found: error
+ path: |
+ sdk/python/dist/*.whl
+ sdk/python/dist/*.tar.gz
+
# ===============================
# Publish Python packages to PyPI
# ===============================
@@ -838,6 +884,7 @@ jobs:
- build_linux
- build_web
- build_flet_extensions
+ - build_flet_mcp
steps:
- name: Setup uv
uses: astral-sh/setup-uv@v6
@@ -878,7 +925,8 @@ jobs:
flet_rive \
flet_secure_storage \
flet_video \
- flet_webview; do
+ flet_webview \
+ flet_mcp; do
uv publish dist/**/${pkg}-*
done
diff --git a/sdk/python/packages/flet-cli/pyproject.toml b/sdk/python/packages/flet-cli/pyproject.toml
index 4b65881aa8..19f9e1badc 100644
--- a/sdk/python/packages/flet-cli/pyproject.toml
+++ b/sdk/python/packages/flet-cli/pyproject.toml
@@ -14,9 +14,12 @@ dependencies = [
"tomli >= 1.1.0 ; python_version < '3.11'",
"cookiecutter >=2.6.0",
"binaryornot <0.5",
- "chardet <6"
+ "chardet <6",
]
+[project.optional-dependencies]
+mcp = ["flet-mcp"]
+
[project.urls]
Homepage = "https://flet.dev"
Repository = "https://github.com/flet-dev/flet"
diff --git a/sdk/python/packages/flet-cli/src/flet_cli/cli.py b/sdk/python/packages/flet-cli/src/flet_cli/cli.py
index 1a7b3e1a8e..69d9dcc17f 100644
--- a/sdk/python/packages/flet-cli/src/flet_cli/cli.py
+++ b/sdk/python/packages/flet-cli/src/flet_cli/cli.py
@@ -94,6 +94,16 @@ def get_parser() -> argparse.ArgumentParser:
flet_cli.commands.devices.Command.register_to(sp, "devices")
flet_cli.commands.doctor.Command.register_to(sp, "doctor")
+ # Register MCP command only if flet-mcp is installed
+ try:
+ from importlib import import_module
+
+ import_module("flet_mcp")
+ mcp_cmd = import_module("flet_cli.commands.mcp")
+ mcp_cmd.Command.register_to(sp, "mcp")
+ except ImportError:
+ pass
+
# set "run" as the default subparser
set_default_subparser(parser, name="run", index=1)
diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/mcp.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/mcp.py
new file mode 100644
index 0000000000..a4e66c98a1
--- /dev/null
+++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/mcp.py
@@ -0,0 +1,66 @@
+import argparse
+from pathlib import Path
+
+from flet_cli.commands.base import BaseCommand
+
+
+class Command(BaseCommand):
+ """Flet MCP server for LLM agents."""
+
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
+ sub = parser.add_subparsers(dest="mcp_command")
+
+ # `flet mcp build` sub-subcommand
+ build_parser = sub.add_parser("build", help="Build MCP index data")
+ build_parser.add_argument(
+ "--examples",
+ type=Path,
+ help="Path to examples directory",
+ )
+ build_parser.add_argument(
+ "--docs",
+ type=Path,
+ help="Path to search_index.json",
+ )
+ build_parser.add_argument(
+ "--output",
+ type=Path,
+ help="Output directory (default: flet_mcp/data/)",
+ )
+
+ # `flet mcp` (serve) options — applied to the mcp parser itself
+ parser.add_argument(
+ "--transport",
+ choices=["stdio", "streamable-http"],
+ default="stdio",
+ help="MCP transport mode",
+ )
+ parser.add_argument(
+ "--port",
+ type=int,
+ default=8000,
+ help="Port for HTTP transport mode",
+ )
+
+ def handle(self, options: argparse.Namespace) -> None:
+ if options.mcp_command == "build":
+ self._handle_build(options)
+ else:
+ self._handle_serve(options)
+
+ def _handle_serve(self, options: argparse.Namespace) -> None:
+ from flet_mcp.server import mcp
+
+ if options.transport == "streamable-http":
+ mcp.run(transport="streamable-http", port=options.port)
+ else:
+ mcp.run(transport="stdio")
+
+ def _handle_build(self, options: argparse.Namespace) -> None:
+ from flet_mcp.build.indexer import build_all
+
+ build_all(
+ examples_dir=options.examples,
+ docs_index=options.docs,
+ output_dir=options.output,
+ )
diff --git a/sdk/python/packages/flet-mcp/.gitignore b/sdk/python/packages/flet-mcp/.gitignore
new file mode 100644
index 0000000000..5a3d1ff640
--- /dev/null
+++ b/sdk/python/packages/flet-mcp/.gitignore
@@ -0,0 +1,6 @@
+# Build artifacts (generated by `flet mcp build`)
+src/flet_mcp/data/api.json
+src/flet_mcp/data/mcp.db
+
+# Un-ignore the build subpackage (matched by sdk/python/.gitignore "build/" rule)
+!src/flet_mcp/build/
diff --git a/sdk/python/packages/flet-mcp/README.md b/sdk/python/packages/flet-mcp/README.md
new file mode 100644
index 0000000000..2cce422056
--- /dev/null
+++ b/sdk/python/packages/flet-mcp/README.md
@@ -0,0 +1,95 @@
+# flet-mcp
+
+MCP (Model Context Protocol) server that gives LLM agents access to Flet examples, documentation, and API reference.
+
+## Installation
+
+```bash
+pip install flet-mcp
+```
+
+## Usage
+
+### Start the MCP server
+
+```bash
+# stdio transport (default, for use with Claude Desktop, Cursor, etc.)
+flet mcp
+
+# HTTP transport
+flet mcp --transport streamable-http --port 8000
+```
+
+### Configure in Claude Desktop
+
+Add to your `claude_desktop_config.json`:
+
+```json
+{
+ "mcpServers": {
+ "flet": {
+ "command": "flet",
+ "args": ["mcp"]
+ }
+ }
+}
+```
+
+### List available tools
+
+```bash
+fastmcp list packages/flet-mcp/src/flet_mcp/server.py
+```
+
+### Call tools from the command line
+
+```bash
+# Search examples
+fastmcp call packages/flet-mcp/src/flet_mcp/server.py search_examples '{"query": "dropdown"}'
+
+# Get full example code
+fastmcp call packages/flet-mcp/src/flet_mcp/server.py get_example '{"example_id": "controls_dropdown_styled"}'
+
+# Search documentation
+fastmcp call packages/flet-mcp/src/flet_mcp/server.py search_docs '{"query": "TextField validation"}'
+
+# Get control API reference
+fastmcp call packages/flet-mcp/src/flet_mcp/server.py get_control_api '{"name": "TextField"}'
+
+# Find an icon
+fastmcp call packages/flet-mcp/src/flet_mcp/server.py find_icon '{"query": "settings"}'
+
+# Search large enums
+fastmcp call packages/flet-mcp/src/flet_mcp/server.py search_enum_members '{"name": "Icons", "query": "arrow"}'
+
+# Get CLI help
+fastmcp call packages/flet-mcp/src/flet_mcp/server.py get_cli_help '{"command": "run"}'
+```
+
+### Use with Pydantic AI
+
+```python
+from pydantic_ai import Agent
+from pydantic_ai.toolsets import MCPToolset
+from flet_mcp import mcp
+
+agent = Agent("claude-sonnet-4-20250514", toolsets=[MCPToolset(mcp)])
+result = agent.run_sync("Create a Flet app with a login form")
+```
+
+## Tools
+
+| Tool | Description |
+|------|-------------|
+| `search_examples` | Search example projects by keyword |
+| `get_example` | Get full source code for an example |
+| `search_docs` | Search documentation by keyword |
+| `get_doc` | Get full content of a doc section |
+| `list_controls` | List controls and services, with optional filtering |
+| `get_control_api` | Get properties, events, and methods for a control |
+| `get_type_api` | Get fields and methods for a type (ButtonStyle, Padding, etc.) |
+| `get_enum` | Get enum members |
+| `search_enum_members` | Search large enums (Icons, CupertinoIcons) |
+| `enum_has_member` | Check if an enum value exists |
+| `find_icon` | Search Material and Cupertino icons by keyword |
+| `get_cli_help` | Get structured CLI command options |
diff --git a/sdk/python/packages/flet-mcp/pyproject.toml b/sdk/python/packages/flet-mcp/pyproject.toml
new file mode 100644
index 0000000000..b4d13584a8
--- /dev/null
+++ b/sdk/python/packages/flet-mcp/pyproject.toml
@@ -0,0 +1,32 @@
+[project]
+name = "flet-mcp"
+version = "0.1.0"
+description = "Flet MCP server for LLM agents"
+authors = [{ name = "Appveyor Systems Inc.", email = "hello@flet.dev" }]
+license = "Apache-2.0"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+ "fastmcp >=2.0.0",
+ "pyyaml >=6.0",
+]
+
+[project.optional-dependencies]
+build = [
+ "markdownify >=0.14.1",
+ "griffe >=1.6.2",
+ "flet",
+ "flet-cli",
+]
+
+[project.urls]
+Homepage = "https://flet.dev"
+Repository = "https://github.com/flet-dev/flet"
+Documentation = "https://flet.dev/docs"
+
+[build-system]
+requires = ["setuptools"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools.package-data]
+flet_mcp = ["data/*"]
diff --git a/sdk/python/packages/flet-mcp/src/flet_mcp/__init__.py b/sdk/python/packages/flet-mcp/src/flet_mcp/__init__.py
new file mode 100644
index 0000000000..66213528cb
--- /dev/null
+++ b/sdk/python/packages/flet-mcp/src/flet_mcp/__init__.py
@@ -0,0 +1,3 @@
+from flet_mcp.server import mcp
+
+__all__ = ["mcp"]
diff --git a/sdk/python/packages/flet-mcp/src/flet_mcp/api_store.py b/sdk/python/packages/flet-mcp/src/flet_mcp/api_store.py
new file mode 100644
index 0000000000..59f5a32c22
--- /dev/null
+++ b/sdk/python/packages/flet-mcp/src/flet_mcp/api_store.py
@@ -0,0 +1,147 @@
+"""Load and query the Griffe-generated API reference (api.json)."""
+
+from __future__ import annotations
+
+import importlib.resources
+import json
+from pathlib import Path
+from typing import Any
+
+
+class ApiStore:
+ """Lazy-loading store for the bundled api.json data.
+
+ The api.json file uses lists for controls/events/types/enums,
+ each entry having a "name" key. This store builds name-keyed dicts
+ for fast lookup.
+ """
+
+ def __init__(self) -> None:
+ self._raw: dict[str, Any] | None = None
+ self._controls: dict[str, dict] | None = None
+ self._events: dict[str, dict] | None = None
+ self._types: dict[str, dict] | None = None
+ self._enums: dict[str, dict] | None = None
+
+ def _load(self) -> dict[str, Any]:
+ if self._raw is None:
+ ref = importlib.resources.files("flet_mcp").joinpath("data/api.json")
+ self._raw = json.loads(Path(str(ref)).read_text(encoding="utf-8"))
+ # Build name-keyed dicts from lists
+ self._controls = {c["name"]: c for c in self._raw.get("controls", [])}
+ self._events = {e["name"]: e for e in self._raw.get("events", [])}
+ self._types = {t["name"]: t for t in self._raw.get("types", [])}
+ self._enums = {e["name"]: e for e in self._raw.get("enums", [])}
+ return self._raw
+
+ # ------------------------------------------------------------------
+ # Controls
+ # ------------------------------------------------------------------
+
+ def list_controls(
+ self,
+ category: str | None = None,
+ kind: str | None = None,
+ limit: int = 50,
+ ) -> list[dict[str, Any]]:
+ """List controls, optionally filtered by category or kind."""
+ self._load()
+ results: list[dict[str, Any]] = []
+ for name, ctrl in self._controls.items():
+ if kind and ctrl.get("kind") != kind:
+ continue
+ if category and category not in ctrl.get("categories", []):
+ continue
+ results.append(
+ {
+ "name": name,
+ "kind": ctrl.get("kind"),
+ "summary": ctrl.get("summary", ""),
+ "categories": ctrl.get("categories", []),
+ }
+ )
+ if len(results) >= limit:
+ break
+ return results
+
+ def get_control(self, name: str) -> dict[str, Any] | None:
+ """Return full control dict by name, or None."""
+ self._load()
+ return self._controls.get(name)
+
+ # ------------------------------------------------------------------
+ # Types & Events
+ # ------------------------------------------------------------------
+
+ def get_type(self, name: str) -> dict[str, Any] | None:
+ """Return full type dict by name, or None."""
+ self._load()
+ return self._types.get(name) or self._events.get(name)
+
+ # ------------------------------------------------------------------
+ # Enums
+ # ------------------------------------------------------------------
+
+ def get_enum(self, name: str) -> dict[str, Any] | None:
+ """Return enum data. Large enums are truncated with a hint to search."""
+ self._load()
+ enum = self._enums.get(name)
+ if enum is None:
+ return None
+
+ if enum.get("kind") == "large_enum":
+ members = enum.get("members", [])
+ return {
+ "name": name,
+ "kind": "large_enum",
+ "total_members": len(members),
+ "sample_members": members[:10],
+ "note": (
+ f"This enum has {len(members)} members. "
+ "Use search_enum_members() to find specific values."
+ ),
+ }
+
+ return enum
+
+ def search_enum_members(self, name: str, query: str, limit: int = 10) -> list[str]:
+ """Search enum members by case-insensitive substring match."""
+ self._load()
+ enum = self._enums.get(name)
+ if enum is None:
+ return []
+
+ query_lower = query.lower()
+ results: list[str] = []
+ for member in enum.get("members", []):
+ member_name = member["name"] if isinstance(member, dict) else str(member)
+ if query_lower in member_name.lower():
+ results.append(member_name)
+ if len(results) >= limit:
+ break
+ return results
+
+ def enum_has_member(self, name: str, member: str) -> bool:
+ """Check whether an enum contains a specific member."""
+ self._load()
+ enum = self._enums.get(name)
+ if enum is None:
+ return False
+ members = enum.get("members", [])
+ member_lower = member.lower()
+ return any(
+ (m["name"] if isinstance(m, dict) else str(m)).lower() == member_lower
+ for m in members
+ )
+
+ # ------------------------------------------------------------------
+ # CLI
+ # ------------------------------------------------------------------
+
+ def get_cli_help(self, command: str | None = None) -> Any:
+ """Return CLI help. None -> list commands; otherwise command help text."""
+ self._load()
+ cli = self._raw.get("cli", {})
+ if command is None:
+ return list(cli.keys())
+ return cli.get(command)
diff --git a/sdk/python/packages/flet-mcp/src/flet_mcp/build/__init__.py b/sdk/python/packages/flet-mcp/src/flet_mcp/build/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/sdk/python/packages/flet-mcp/src/flet_mcp/build/api.py b/sdk/python/packages/flet-mcp/src/flet_mcp/build/api.py
new file mode 100644
index 0000000000..fbb6cfb438
--- /dev/null
+++ b/sdk/python/packages/flet-mcp/src/flet_mcp/build/api.py
@@ -0,0 +1,528 @@
+"""API reference builder using Griffe.
+
+Scans Flet packages with Griffe to extract controls, events, types, enums,
+and CLI help, then writes a consolidated api.json file.
+"""
+
+from __future__ import annotations
+
+import json
+import logging
+from pathlib import Path
+from typing import Any
+
+import griffe
+
+logger = logging.getLogger(__name__)
+
+DEFAULT_PACKAGES: list[str] = [
+ "flet",
+ "flet_ads",
+ "flet_audio",
+ "flet_audio_recorder",
+ "flet_camera",
+ "flet_charts",
+ "flet_code_editor",
+ "flet_color_pickers",
+ "flet_datatable2",
+ "flet_flashlight",
+ "flet_geolocator",
+ "flet_lottie",
+ "flet_map",
+ "flet_permission_handler",
+ "flet_rive",
+ "flet_secure_storage",
+ "flet_video",
+ "flet_webview",
+]
+
+_CONTROL_BASE_NAMES = {"BaseControl", "LayoutControl", "AdaptiveControl", "Service"}
+
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+
+def _first_line(docstring: griffe.Docstring | None) -> str:
+ if docstring is None:
+ return ""
+ text = docstring.value.strip()
+ return text.split("\n", 1)[0].strip()
+
+
+def _full_docstring(docstring: griffe.Docstring | None) -> str:
+ if docstring is None:
+ return ""
+ return docstring.value.strip()
+
+
+def _annotation_str(annotation: Any) -> str:
+ if annotation is None:
+ return ""
+ return str(annotation)
+
+
+def _default_str(default: Any) -> str:
+ if default is None:
+ return ""
+ return str(default)
+
+
+def _is_control_class(cls: griffe.Class) -> bool:
+ """Check if any base class name matches a known control base."""
+ for base in cls.bases:
+ base_name = str(base).rsplit(".", 1)[-1]
+ if base_name in _CONTROL_BASE_NAMES:
+ return True
+ return False
+
+
+def _is_service(cls: griffe.Class) -> bool:
+ return any("Service" in str(base) for base in cls.bases)
+
+
+def _is_enum(cls: griffe.Class) -> bool:
+ for base in cls.bases:
+ base_name = str(base).rsplit(".", 1)[-1]
+ if base_name == "Enum" or base_name.endswith("Enum"):
+ return True
+ return False
+
+
+def _is_event_class(cls: griffe.Class) -> bool:
+ return any("Event" in str(base).rsplit(".", 1)[-1] for base in cls.bases)
+
+
+def _is_dataclass(cls: griffe.Class) -> bool:
+ for dec in cls.decorators:
+ dec_str = str(dec.value)
+ if "dataclass" in dec_str:
+ return True
+ return False
+
+
+def _has_control_decorator(cls: griffe.Class) -> tuple[list[str], list[str]]:
+ """Return (categories, tags) from @control decorator if present."""
+ categories: list[str] = []
+ tags: list[str] = []
+ for dec in cls.decorators:
+ dec_str = str(dec.value)
+ if "control" not in dec_str:
+ continue
+ # Try to extract keyword args from the decorator's AST node
+ try:
+ node = dec.value
+ # Griffe stores decorator values as Expressions or AST nodes
+ if hasattr(node, "keywords"):
+ for kw in node.keywords:
+ if kw.arg == "categories" and hasattr(kw.value, "elts"):
+ categories = [
+ str(getattr(e, "value", e)) for e in kw.value.elts
+ ]
+ elif kw.arg == "tags" and hasattr(kw.value, "elts"):
+ tags = [str(getattr(e, "value", e)) for e in kw.value.elts]
+ except Exception:
+ pass
+ # Fallback: parse from string representation
+ if not categories and "categories=" in dec_str:
+ categories = _parse_tuple_from_str(dec_str, "categories")
+ if not tags and "tags=" in dec_str:
+ tags = _parse_tuple_from_str(dec_str, "tags")
+ return categories, tags
+
+
+def _parse_tuple_from_str(dec_str: str, key: str) -> list[str]:
+ """Parse a keyword arg tuple from decorator string representation.
+
+ e.g. 'control("TextField", categories=("input", "form"))' -> ["input", "form"]
+ """
+ import re
+
+ pattern = rf"{key}\s*=\s*\(([^)]*)\)"
+ match = re.search(pattern, dec_str)
+ if not match:
+ return []
+ inner = match.group(1)
+ return [s.strip().strip("\"'") for s in inner.split(",") if s.strip().strip("\"'")]
+
+
+def _extract_properties(cls: griffe.Class) -> list[dict[str, Any]]:
+ """Extract dataclass-style fields as properties."""
+ props: list[dict[str, Any]] = []
+ for name, member in cls.members.items():
+ if name.startswith("_"):
+ continue
+ if isinstance(member, griffe.Attribute):
+ annotation = _annotation_str(member.annotation)
+ is_event = (
+ "EventHandler" in annotation or "ControlEventHandler" in annotation
+ )
+ props.append(
+ {
+ "name": name,
+ "type": annotation,
+ "default": _default_str(member.value),
+ "docstring": _full_docstring(member.docstring),
+ "is_event": is_event,
+ }
+ )
+ return props
+
+
+def _extract_methods(cls: griffe.Class) -> list[dict[str, Any]]:
+ """Extract public methods."""
+ methods: list[dict[str, Any]] = []
+ for name, member in cls.members.items():
+ if name.startswith("_"):
+ continue
+ if isinstance(member, griffe.Function):
+ args = []
+ for param in member.parameters:
+ if param.name == "self":
+ continue
+ args.append(
+ {
+ "name": param.name,
+ "type": _annotation_str(param.annotation),
+ "default": _default_str(param.default),
+ }
+ )
+ methods.append(
+ {
+ "name": name,
+ "args": args,
+ "return_type": _annotation_str(member.annotation),
+ "docstring": _full_docstring(member.docstring),
+ }
+ )
+ return methods
+
+
+def _extract_enum_members(cls: griffe.Class) -> list[dict[str, str]]:
+ members: list[dict[str, str]] = []
+ for name, member in cls.members.items():
+ if name.startswith("_"):
+ continue
+ if isinstance(member, griffe.Attribute):
+ entry: dict[str, str] = {"name": name, "value": _default_str(member.value)}
+ doc = _full_docstring(member.docstring)
+ if doc:
+ entry["docstring"] = doc
+ members.append(entry)
+ return members
+
+
+# ---------------------------------------------------------------------------
+# Module walker
+# ---------------------------------------------------------------------------
+
+
+def _walk_module(
+ module: griffe.Module,
+ controls: list[dict],
+ events: list[dict],
+ types: list[dict],
+ enums: list[dict],
+) -> None:
+ """Recursively walk a griffe Module and classify its members."""
+ for name, obj in module.members.items():
+ if isinstance(obj, griffe.Module):
+ _walk_module(obj, controls, events, types, enums)
+
+ elif isinstance(obj, griffe.Class):
+ try:
+ if _is_enum(obj):
+ members = _extract_enum_members(obj)
+ entry: dict[str, Any] = {
+ "name": obj.name,
+ "module": obj.canonical_path.rsplit(".", 1)[0],
+ "docstring": _full_docstring(obj.docstring),
+ "members": members,
+ }
+ if len(members) > 50:
+ entry["kind"] = "large_enum"
+ enums.append(entry)
+
+ elif _is_control_class(obj):
+ categories, tags = _has_control_decorator(obj)
+ props = _extract_properties(obj)
+ event_fields = [p for p in props if p.get("is_event")]
+ regular_props = [p for p in props if not p.get("is_event")]
+ # Remove is_event flag from output
+ for p in regular_props:
+ p.pop("is_event", None)
+ for e in event_fields:
+ e.pop("is_event", None)
+
+ kind = "service" if _is_service(obj) else "control"
+ controls.append(
+ {
+ "name": obj.name,
+ "module": obj.canonical_path.rsplit(".", 1)[0],
+ "kind": kind,
+ "summary": _first_line(obj.docstring),
+ "bases": [str(b) for b in obj.bases],
+ "categories": categories,
+ "tags": tags,
+ "properties": regular_props,
+ "events": event_fields,
+ "methods": _extract_methods(obj),
+ }
+ )
+
+ elif _is_event_class(obj):
+ fields = _extract_properties(obj)
+ for f in fields:
+ f.pop("is_event", None)
+ events.append(
+ {
+ "name": obj.name,
+ "module": obj.canonical_path.rsplit(".", 1)[0],
+ "docstring": _full_docstring(obj.docstring),
+ "fields": fields,
+ }
+ )
+
+ elif _is_dataclass(obj):
+ fields = _extract_properties(obj)
+ for f in fields:
+ f.pop("is_event", None)
+ class_methods = []
+ for mname, member in obj.members.items():
+ if mname.startswith("_"):
+ continue
+ if isinstance(member, griffe.Function) and (
+ "classmethod" in member.labels
+ or "staticmethod" in member.labels
+ ):
+ args = []
+ for param in member.parameters:
+ if param.name in ("self", "cls"):
+ continue
+ args.append(
+ {
+ "name": param.name,
+ "type": _annotation_str(param.annotation),
+ "default": _default_str(param.default),
+ }
+ )
+ class_methods.append(
+ {
+ "name": mname,
+ "args": args,
+ "return_type": _annotation_str(member.annotation),
+ "docstring": _full_docstring(member.docstring),
+ }
+ )
+ types.append(
+ {
+ "name": obj.name,
+ "module": obj.canonical_path.rsplit(".", 1)[0],
+ "docstring": _full_docstring(obj.docstring),
+ "fields": fields,
+ "methods": class_methods,
+ }
+ )
+ except Exception as exc:
+ logger.warning("Error processing class %s: %s", name, exc)
+
+
+# ---------------------------------------------------------------------------
+# CLI help extraction
+# ---------------------------------------------------------------------------
+
+
+def _extract_cli_help() -> dict[str, Any]:
+ """Try to extract structured CLI help from flet_cli."""
+ import argparse as _argparse
+
+ cli_help: dict[str, Any] = {}
+
+ def _parse_parser(parser: _argparse.ArgumentParser) -> dict[str, Any]:
+ """Extract structured option data from an ArgumentParser."""
+ options: list[dict[str, Any]] = []
+ for action in parser._actions:
+ if isinstance(action, _argparse._HelpAction):
+ continue
+ if isinstance(action, _argparse._SubParsersAction):
+ continue
+
+ opt: dict[str, Any] = {}
+
+ if action.option_strings:
+ opt["name"] = action.option_strings[-1] # --long form
+ if len(action.option_strings) > 1:
+ opt["alias"] = action.option_strings[0] # -short form
+ else:
+ opt["name"] = action.dest
+ opt["positional"] = True
+
+ # Type
+ if isinstance(
+ action, (_argparse._StoreTrueAction, _argparse._StoreFalseAction)
+ ):
+ opt["type"] = "bool"
+ elif isinstance(action, _argparse._CountAction):
+ opt["type"] = "count"
+ elif action.type is not None:
+ opt["type"] = action.type.__name__
+ else:
+ opt["type"] = "str"
+
+ if action.nargs in ("*", "+"):
+ opt["multiple"] = True
+
+ if action.default is not None and action.default != _argparse.SUPPRESS:
+ opt["default"] = action.default
+
+ if action.required:
+ opt["required"] = True
+
+ if action.choices:
+ opt["choices"] = list(action.choices)
+
+ if action.help and action.help != _argparse.SUPPRESS:
+ opt["help"] = action.help
+
+ options.append(opt)
+
+ result: dict[str, Any] = {"options": options}
+ if parser.description:
+ result["description"] = parser.description
+ return result
+
+ try:
+ from flet_cli.cli import get_parser
+
+ parser = get_parser()
+ cli_help["flet"] = _parse_parser(parser)
+ for action in parser._subparsers._actions:
+ if hasattr(action, "choices") and action.choices:
+ for cmd_name, cmd_parser in action.choices.items():
+ try:
+ cmd_data = _parse_parser(cmd_parser)
+ if cmd_parser.description:
+ cmd_data["description"] = cmd_parser.description
+ cli_help[cmd_name] = cmd_data
+ except Exception:
+ pass
+ except Exception as exc:
+ logger.warning("Could not extract CLI help: %s", exc)
+ return cli_help
+
+
+# ---------------------------------------------------------------------------
+# Icon enum injection
+# ---------------------------------------------------------------------------
+
+
+def _inject_icon_enums(enums: list[dict]) -> None:
+ """Add Icons and CupertinoIcons as large_enum entries from JSON files."""
+ try:
+ import importlib.resources
+
+ material_path = (
+ importlib.resources.files("flet") / "controls" / "material" / "icons.json"
+ )
+ cupertino_path = (
+ importlib.resources.files("flet")
+ / "controls"
+ / "cupertino"
+ / "cupertino_icons.json"
+ )
+
+ with importlib.resources.as_file(material_path) as p:
+ material_icons: dict[str, int] = json.loads(p.read_text("utf-8"))
+ with importlib.resources.as_file(cupertino_path) as p:
+ cupertino_icons: dict[str, int] = json.loads(p.read_text("utf-8"))
+
+ enums.append(
+ {
+ "name": "Icons",
+ "module": "flet.controls.material.icons",
+ "docstring": "Material Design icon constants.",
+ "kind": "large_enum",
+ "members": [
+ {"name": name, "value": str(val)}
+ for name, val in material_icons.items()
+ ],
+ }
+ )
+ enums.append(
+ {
+ "name": "CupertinoIcons",
+ "module": "flet.controls.cupertino.cupertino_icons",
+ "docstring": "Cupertino (iOS-style) icon constants.",
+ "kind": "large_enum",
+ "members": [
+ {"name": name, "value": str(val)}
+ for name, val in cupertino_icons.items()
+ ],
+ }
+ )
+ logger.info(
+ "Injected Icons (%d members) and CupertinoIcons (%d members)",
+ len(material_icons),
+ len(cupertino_icons),
+ )
+ except Exception as exc:
+ logger.warning("Could not inject icon enums: %s", exc)
+
+
+# ---------------------------------------------------------------------------
+# Public API
+# ---------------------------------------------------------------------------
+
+
+def build_api(output_path: Path, packages: list[str] | None = None) -> dict[str, Any]:
+ """Build the API reference JSON from installed Flet packages.
+
+ Parameters:
+ output_path: Where to write the resulting api.json.
+ packages: List of package names to scan. Defaults to all known Flet packages.
+
+ Returns:
+ A stats dict with counts per category.
+ """
+ if packages is None:
+ packages = DEFAULT_PACKAGES
+
+ controls: list[dict] = []
+ events: list[dict] = []
+ types: list[dict] = []
+ enums: list[dict] = []
+
+ for pkg in packages:
+ try:
+ module = griffe.load(pkg)
+ except Exception as exc:
+ logger.warning("Failed to load package %s: %s", pkg, exc)
+ continue
+ _walk_module(module, controls, events, types, enums)
+
+ # Inject Icons and CupertinoIcons from JSON files (not Python Enums)
+ _inject_icon_enums(enums)
+
+ cli_help = _extract_cli_help()
+
+ api_data: dict[str, Any] = {
+ "controls": controls,
+ "events": events,
+ "types": types,
+ "enums": enums,
+ "cli": cli_help,
+ }
+
+ output_path.parent.mkdir(parents=True, exist_ok=True)
+ output_path.write_text(json.dumps(api_data, indent=2), encoding="utf-8")
+
+ stats = {
+ "controls": len(controls),
+ "events": len(events),
+ "types": len(types),
+ "enums": len(enums),
+ "cli_commands": len(cli_help),
+ }
+ logger.info("API build stats: %s", stats)
+ return stats
diff --git a/sdk/python/packages/flet-mcp/src/flet_mcp/build/docs.py b/sdk/python/packages/flet-mcp/src/flet_mcp/build/docs.py
new file mode 100644
index 0000000000..c331421ec6
--- /dev/null
+++ b/sdk/python/packages/flet-mcp/src/flet_mcp/build/docs.py
@@ -0,0 +1,132 @@
+"""Docs indexer. Parses search_index.json produced by mkdocs and indexes
+the entries into an SQLite FTS5 database."""
+
+from __future__ import annotations
+
+import json
+import logging
+import re
+import sqlite3
+from pathlib import Path
+
+from markdownify import markdownify
+
+logger = logging.getLogger(__name__)
+
+_HTML_TAG_RE = re.compile(r"<[^>]+>")
+_WHITESPACE_RE = re.compile(r"\s{3,}")
+_LOCATION_SPLIT_RE = re.compile(r"[/\-_\.#]")
+# mkdocstrings labels like class-attribute, instance-attribute
+_MKDOCS_LABEL_RE = re.compile(
+ r"\s*(?:class-attribute|instance-attribute|property|cached-property|"
+ r"writable|static-method|class-method|module-attribute)",
+ re.IGNORECASE,
+)
+
+
+def _create_tables(conn: sqlite3.Connection) -> None:
+ conn.executescript(
+ """
+ CREATE TABLE IF NOT EXISTS docs (
+ location TEXT PRIMARY KEY,
+ location_text TEXT,
+ title TEXT,
+ content TEXT
+ );
+ CREATE VIRTUAL TABLE IF NOT EXISTS docs_fts USING fts5(
+ title, location_text, content,
+ tokenize='porter unicode61'
+ );
+ """
+ )
+
+
+def _html_to_markdown(html: str) -> str:
+ """Convert HTML to markdown and clean up."""
+ md = markdownify(html, strip=["img"])
+ md = _HTML_TAG_RE.sub("", md)
+ md = _WHITESPACE_RE.sub("\n\n", md)
+ return md.strip()
+
+
+def _strip_html(text: str) -> str:
+ text = _MKDOCS_LABEL_RE.sub("", text)
+ return _HTML_TAG_RE.sub("", text).strip()
+
+
+def _location_to_text(location: str) -> str:
+ """Turn a location string into a space-separated searchable string.
+
+ Example:
+ "ads/bannerad/#flet_ads.banner_ad.BannerAd.on_paid"
+ -> "ads bannerad flet_ads banner_ad BannerAd on_paid"
+ """
+ parts = _LOCATION_SPLIT_RE.split(location)
+ return " ".join(p for p in parts if p)
+
+
+def index_docs(conn: sqlite3.Connection, search_index_path: Path) -> int:
+ """Index mkdocs search_index.json into *conn*.
+
+ Returns the number of doc entries indexed.
+ """
+ _create_tables(conn)
+
+ raw = json.loads(search_index_path.read_text(encoding="utf-8"))
+ entries: list[dict] = raw.get("docs", [])
+
+ count = 0
+ root_title: str = ""
+ section_title: str = ""
+ for entry in entries:
+ location: str = entry.get("location", "")
+ title: str = entry.get("title", "")
+ text: str = entry.get("text", "")
+
+ has_anchor = "#" in location
+ clean_title = _strip_html(title)
+
+ # ---- new page (no anchor) ----
+ if not has_anchor:
+ root_title = clean_title
+ section_title = ""
+ if not text:
+ continue
+
+ # ---- empty-text anchor = section group heading ----
+ if has_anchor and not text:
+ anchor = location.split("#", 1)[1] if "#" in location else ""
+ # Only track well-known API doc section groups as intermediate titles
+ if anchor.endswith(("-properties", "-methods", "-events")):
+ section_title = clean_title
+ else:
+ # Reset section title — this is a peer heading, not a group
+ section_title = ""
+ continue
+
+ # ---- entry with content: build composite title ----
+ parts = [root_title]
+ if section_title:
+ parts.append(section_title)
+ parts.append(clean_title)
+ composite_title = " - ".join(parts)
+
+ # ---- process content ----
+ location_text = _location_to_text(location)
+ content = _html_to_markdown(text)
+
+ # ---- insert ----
+ conn.execute(
+ "INSERT OR REPLACE INTO docs (location, location_text, title, content) "
+ "VALUES (?, ?, ?, ?)",
+ (location, location_text, composite_title, content),
+ )
+ conn.execute(
+ "INSERT INTO docs_fts (title, location_text, content) VALUES (?, ?, ?)",
+ (composite_title, location_text, content),
+ )
+
+ count += 1
+
+ conn.commit()
+ return count
diff --git a/sdk/python/packages/flet-mcp/src/flet_mcp/build/examples.py b/sdk/python/packages/flet-mcp/src/flet_mcp/build/examples.py
new file mode 100644
index 0000000000..276659a92e
--- /dev/null
+++ b/sdk/python/packages/flet-mcp/src/flet_mcp/build/examples.py
@@ -0,0 +1,172 @@
+"""Examples indexer. Walks a directory tree looking for pyproject.toml files
+with [tool.flet.metadata] and indexes them into an SQLite database."""
+
+from __future__ import annotations
+
+import json
+import logging
+import re
+import sqlite3
+import sys
+from pathlib import Path
+
+if sys.version_info >= (3, 11):
+ import tomllib
+else:
+ import tomli as tomllib
+
+logger = logging.getLogger(__name__)
+
+_CODE_EXTENSIONS = {".py", ".toml"}
+_TEXT_EXTENSIONS = {".py", ".toml", ".md", ".txt"}
+
+
+def _create_tables(conn: sqlite3.Connection) -> None:
+ conn.executescript(
+ """
+ CREATE TABLE IF NOT EXISTS examples (
+ id TEXT PRIMARY KEY,
+ location TEXT,
+ metadata TEXT
+ );
+ CREATE TABLE IF NOT EXISTS example_files (
+ example_id TEXT,
+ filename TEXT,
+ content TEXT,
+ FOREIGN KEY (example_id) REFERENCES examples(id)
+ );
+ CREATE VIRTUAL TABLE IF NOT EXISTS examples_fts USING fts5(
+ title, description, tags, controls,
+ layout_pattern, features, search_text, code,
+ tokenize='porter unicode61'
+ );
+ """
+ )
+
+
+def _derive_id(relative_path: Path) -> str:
+ return re.sub(r"[\\/]", "_", str(relative_path))
+
+
+def _read_text_files(directory: Path) -> dict[str, str]:
+ """Read all indexable text files in *directory* (non-recursive)."""
+ files: dict[str, str] = {}
+ for p in sorted(directory.iterdir()):
+ if p.is_file() and p.suffix in _TEXT_EXTENSIONS:
+ try:
+ files[p.name] = p.read_text(encoding="utf-8", errors="replace")
+ except OSError as exc:
+ logger.warning("Cannot read %s: %s", p, exc)
+ return files
+
+
+def index_examples(conn: sqlite3.Connection, examples_dir: Path) -> int:
+ """Index Flet example projects found under *examples_dir*.
+
+ Returns the number of examples indexed.
+ """
+ _create_tables(conn)
+
+ count = 0
+ for pyproject_path in sorted(examples_dir.rglob("pyproject.toml")):
+ try:
+ with open(pyproject_path, "rb") as f:
+ data = tomllib.load(f)
+ except Exception as exc:
+ logger.warning("Failed to parse %s: %s", pyproject_path, exc)
+ continue
+
+ flet_meta = data.get("tool", {}).get("flet", {}).get("metadata")
+ if flet_meta is None:
+ continue
+
+ project = data.get("project", {})
+ project_dir = pyproject_path.parent
+ relative = project_dir.relative_to(examples_dir)
+
+ example_id = _derive_id(relative)
+ location = str(relative)
+ title = flet_meta.get("title") or project.get("name", example_id)
+ description = project.get("description", "")
+ tags: list[str] = project.get("keywords", [])
+ platforms: list[str] = (
+ data.get("tool", {})
+ .get("flet", {})
+ .get("platforms", ["web", "ios", "android", "macos", "windows", "linux"])
+ )
+ controls: list[str] = flet_meta.get("controls", [])
+ layout: str = flet_meta.get("layout_pattern", "")
+ complexity: str = flet_meta.get("complexity", "basic")
+ features: list[str] = flet_meta.get("features", [])
+
+ # ---- read files in the example directory ----
+ text_files = _read_text_files(project_dir)
+
+ # ---- build search_text ----
+ search_text = (
+ f"{title}\n\n"
+ f"{description}\n\n"
+ f"tags: {', '.join(tags)}\n"
+ f"platforms: {', '.join(platforms)}\n"
+ f"controls: {', '.join(controls)}\n"
+ f"complexity: {complexity}\n"
+ f"layout_pattern: {layout}\n"
+ f"features: {', '.join(features)}"
+ )
+
+ # ---- build code blob ----
+ code_parts: list[str] = []
+ for fname, content in text_files.items():
+ if Path(fname).suffix in _CODE_EXTENSIONS:
+ code_parts.append(content)
+ code = "\n\n".join(code_parts)
+
+ # ---- metadata JSON ----
+ metadata = json.dumps(
+ {
+ "title": title,
+ "description": description,
+ "tags": tags,
+ "platforms": platforms,
+ "controls": controls,
+ "layout_pattern": layout,
+ "complexity": complexity,
+ "features": features,
+ }
+ )
+
+ # ---- insert rows ----
+ conn.execute(
+ "INSERT OR REPLACE INTO examples (id, location, metadata) VALUES (?, ?, ?)",
+ (example_id, location, metadata),
+ )
+
+ for fname, content in text_files.items():
+ conn.execute(
+ "INSERT INTO example_files "
+ "(example_id, filename, content) VALUES (?, ?, ?)",
+ (example_id, fname, content),
+ )
+
+ conn.execute(
+ "INSERT INTO examples_fts "
+ "(title, description, tags, controls, "
+ "layout_pattern, features, search_text, code) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
+ (
+ title,
+ description,
+ ", ".join(tags),
+ ", ".join(controls),
+ layout,
+ ", ".join(features),
+ search_text,
+ code,
+ ),
+ )
+
+ count += 1
+ logger.debug("Indexed example %s (%s)", example_id, title)
+
+ conn.commit()
+ return count
diff --git a/sdk/python/packages/flet-mcp/src/flet_mcp/build/indexer.py b/sdk/python/packages/flet-mcp/src/flet_mcp/build/indexer.py
new file mode 100644
index 0000000000..54c4ac5155
--- /dev/null
+++ b/sdk/python/packages/flet-mcp/src/flet_mcp/build/indexer.py
@@ -0,0 +1,105 @@
+"""Build orchestrator for the Flet MCP data pipeline.
+
+Coordinates indexing of examples, docs, and API reference into the
+data directory consumed by the MCP server.
+"""
+
+from __future__ import annotations
+
+import logging
+import sqlite3
+import time
+from pathlib import Path
+
+from flet_mcp.build.api import build_api
+from flet_mcp.build.docs import index_docs
+from flet_mcp.build.examples import index_examples
+
+logger = logging.getLogger(__name__)
+
+_DATA_DIR = Path(__file__).resolve().parent.parent / "data"
+
+
+def _print_summary(metrics: list[tuple[str, str, float]]) -> None:
+ """Print a summary table of build steps.
+
+ Each entry is (step_name, result_text, elapsed_seconds).
+ """
+ try:
+ from rich.console import Console
+ from rich.table import Table
+
+ console = Console()
+ table = Table(title="MCP Build Summary")
+ table.add_column("Step", style="bold")
+ table.add_column("Result", justify="right")
+ table.add_column("Time", justify="right", style="dim")
+
+ for step, result, elapsed in metrics:
+ table.add_row(step, result, f"{elapsed:.2f}s")
+
+ console.print(table)
+ except ImportError:
+ # Fallback to plain output
+ print("\n--- MCP Build Summary ---")
+ for step, result, elapsed in metrics:
+ print(f" {step:<20s} {result:>30s} ({elapsed:.2f}s)")
+ print()
+
+
+def build_all(
+ examples_dir: Path | None = None,
+ docs_index: Path | None = None,
+ output_dir: Path | None = None,
+) -> None:
+ """Run the full MCP build pipeline.
+
+ Parameters:
+ examples_dir: Root directory containing Flet example projects.
+ docs_index: Path to the mkdocs search_index.json file.
+ output_dir: Where to write mcp.db and api.json.
+ Defaults to the ``flet_mcp/data/`` package directory.
+ """
+ if output_dir is None:
+ output_dir = _DATA_DIR
+ output_dir.mkdir(parents=True, exist_ok=True)
+
+ db_path = output_dir / "mcp.db"
+ api_path = output_dir / "api.json"
+
+ # Remove existing DB so we start fresh
+ if db_path.exists():
+ db_path.unlink()
+
+ metrics: list[tuple[str, str, float]] = []
+ conn = sqlite3.connect(str(db_path))
+
+ try:
+ # ---- Examples ----
+ if examples_dir is not None:
+ t0 = time.perf_counter()
+ n = index_examples(conn, examples_dir)
+ elapsed = time.perf_counter() - t0
+ metrics.append(("Examples", f"{n} indexed", elapsed))
+ logger.info("Indexed %d examples in %.2fs", n, elapsed)
+
+ # ---- Docs ----
+ if docs_index is not None:
+ t0 = time.perf_counter()
+ n = index_docs(conn, docs_index)
+ elapsed = time.perf_counter() - t0
+ metrics.append(("Docs", f"{n} indexed", elapsed))
+ logger.info("Indexed %d doc entries in %.2fs", n, elapsed)
+
+ # ---- API reference ----
+ t0 = time.perf_counter()
+ stats = build_api(api_path)
+ elapsed = time.perf_counter() - t0
+ parts = [f"{v} {k}" for k, v in stats.items()]
+ metrics.append(("API", ", ".join(parts), elapsed))
+ logger.info("Built API reference in %.2fs: %s", elapsed, stats)
+
+ finally:
+ conn.close()
+
+ _print_summary(metrics)
diff --git a/sdk/python/packages/flet-mcp/src/flet_mcp/data/.gitkeep b/sdk/python/packages/flet-mcp/src/flet_mcp/data/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/sdk/python/packages/flet-mcp/src/flet_mcp/data/icons.yml b/sdk/python/packages/flet-mcp/src/flet_mcp/data/icons.yml
new file mode 100644
index 0000000000..972c261a77
--- /dev/null
+++ b/sdk/python/packages/flet-mcp/src/flet_mcp/data/icons.yml
@@ -0,0 +1,2097 @@
+# https://github.com/Christilut/material-icons-aliases/blob/master/icons.yml
+action:
+ 3d rotation:
+ - arrows
+ - vr
+ accessibility:
+ - user
+ - person
+ accessible:
+ - wheelchair
+ - user
+ account balance:
+ - building
+ - temple
+ account balance wallet: []
+ account box:
+ - user
+ account circle:
+ - user
+ add shopping cart: []
+ alarm:
+ - time
+ - clock
+ alarm add:
+ - time
+ - clock
+ alarm off:
+ - time
+ - clock
+ alarm on:
+ - time
+ - clock
+ - checkmark
+ all out:
+ - arrows
+ - circle
+ - directional
+ - expand
+ android:
+ - operating system
+ announcement:
+ - comment
+ - chat
+ - message
+ - balloon
+ - exclamation
+ aspect ratio:
+ - screen
+ - resolution
+ - monitor
+ assessment:
+ - graph
+ - chart
+ assignment:
+ - clipboard
+ assignment ind:
+ - user
+ - clipboard
+ assignment late:
+ - exclamation
+ - clipboard
+ assignment return:
+ - clipboard
+ - arrow
+ assignment returned:
+ - clipboard
+ - arrow
+ assignment turned in:
+ - clipboard
+ - checkmark
+ autorenew:
+ - arrows
+ - loader
+ - loading
+ - refresh
+ backup:
+ - cloud
+ - arrow
+ - upload
+ book:
+ - bookmark
+ bookmark:
+ - favorite
+ bookmark border:
+ - favorite
+ bug report:
+ - ticket
+ - issue
+ build:
+ - wrench
+ - spanner
+ cached:
+ - arrows
+ - refresh
+ - loading
+ - loader
+ camera enhance:
+ - photo
+ card giftcard:
+ - creditcard
+ card membership:
+ - creditcard
+ card travel:
+ - creditcard
+ change history:
+ - triangle
+ check circle: []
+ chrome reader mode: []
+ class: []
+ code:
+ - parenthesis
+ - brackets
+ compare arrows:
+ - directional
+ copyright: []
+ credit card:
+ - creditcard
+ dashboard: []
+ date range:
+ - calendar
+ - agenda
+ delete:
+ - trashcan
+ - bin
+ - recycle
+ - junk
+ delete forever:
+ - trashcan
+ - bin
+ - recycle
+ - junk
+ description:
+ - file
+ - document
+ dns:
+ - server
+ - network
+ done:
+ - checkmark
+ done all:
+ - checkmark
+ donut large:
+ - circle
+ donut small:
+ - circle
+ eject: []
+ euro symbol:
+ - financial
+ - money
+ event:
+ - calendar
+ - agenda
+ event seat:
+ - chair
+ - bench
+ - sit
+ exit to app:
+ - quit
+ explore:
+ - compass
+ - map
+ extension:
+ - puzzle
+ face:
+ - person
+ - user
+ favorite:
+ - heart
+ - love
+ favorite border:
+ - heart
+ - love
+ feedback:
+ - comment
+ - message
+ - chat
+ find in page:
+ - search
+ find replace:
+ - search
+ fingerprint:
+ - biometrics
+ - touchid
+ flight land:
+ - airplane
+ - flying
+ flight takeoff:
+ - airplane
+ - flying
+ flip to back: []
+ flip to front: []
+ g translate:
+ - google
+ gavel:
+ - hammer
+ get app:
+ - download
+ gif:
+ - animated
+ grade:
+ - star
+ - rating
+ - rate
+ group work:
+ - circle
+ - film
+ - reel
+ help:
+ - question mark
+ help outline:
+ - question mark
+ highlight off:
+ - delete
+ - disable
+ - clear
+ - remove
+ - circle
+ - times
+ history:
+ - time
+ - revert
+ - undo
+ - clock
+ home:
+ - building
+ - house
+ hourglass empty:
+ - time
+ hourglass full:
+ - time
+ http:
+ - network
+ - internet
+ - web
+ https:
+ - network
+ - internet
+ - web
+ - security
+ - lock
+ - ssl
+ - encrypt
+ important devices:
+ - computer
+ - phone
+ - monitor
+ info:
+ - circle
+ - information
+ info outline:
+ - circle
+ - information
+ input:
+ - arrow
+ invert colors:
+ - drop
+ - water
+ - liquid
+ label:
+ - tag
+ - badge
+ label outline:
+ - tag
+ - badge
+ language:
+ - earth
+ - world
+ - web
+ launch:
+ - arrow
+ lightbulb outline: []
+ line style:
+ - editor
+ line weight:
+ - editor
+ list:
+ - editor
+ lock:
+ - security
+ lock open:
+ - security
+ lock outline:
+ - security
+ loyalty:
+ - tag
+ - badge
+ - heart
+ - love
+ markunread mailbox: []
+ motorcycle:
+ - motorbike
+ note add:
+ - document
+ - create
+ offline pin:
+ - checkmark
+ - circle
+ opacity:
+ - drop
+ - water
+ - liquid
+ open in browser: []
+ open in new: []
+ open with:
+ - arrows
+ - directional
+ - expand
+ pageview:
+ - search
+ - find
+ - looking glass
+ pan tool:
+ - hand
+ - touch
+ - drag
+ payment:
+ - creditcard
+ - financial
+ - money
+ perm camera mic: []
+ perm contact calendar:
+ - user
+ - agenda
+ perm data setting:
+ - wifi
+ - wireless
+ - network
+ - cellular
+ - configure
+ perm device information:
+ - phone
+ - mobile
+ perm identity:
+ - user
+ perm media:
+ - folders
+ - directories
+ perm phone msg:
+ - mobile
+ - message
+ perm scan wifi:
+ - network
+ - wireless
+ pets:
+ - animal
+ - claw
+ - paw
+ - dog
+ - cat
+ picture in picture: []
+ picture in picture alt: []
+ play for work:
+ - arrow
+ polymer: []
+ power settings new:
+ - 'off'
+ pregnant woman:
+ - user
+ - person
+ print:
+ - printer
+ - paper
+ query builder:
+ - clock
+ - time
+ question answer:
+ - comment
+ - message
+ - chat
+ receipt:
+ - transaction
+ record voice over:
+ - user
+ - sound
+ redeem:
+ - present
+ - giftcard
+ remove shopping cart: []
+ reorder:
+ - lines
+ report problem:
+ - triangle
+ - warning
+ - danger
+ - exclamation
+ restore:
+ - clock
+ - undo
+ - time
+ - history
+ restore page:
+ - history
+ - web
+ - undo
+ room:
+ - location
+ - gps
+ rounded corner: []
+ rowing:
+ - sports
+ schedule:
+ - time
+ - clock
+ search:
+ - find
+ - looking glass
+ settings:
+ - gear
+ settings applications:
+ - gear
+ settings backup restore:
+ - time
+ - history
+ - undo
+ settings bluetooth:
+ - network
+ settings brightness:
+ - sun
+ settings cell:
+ - mobile
+ - phone
+ settings ethernet:
+ - brackets
+ - parenthesis
+ - network
+ settings input antenna:
+ - wifi
+ - network
+ settings input component:
+ - plugs
+ - cable
+ settings input composite:
+ - plugs
+ - cable
+ settings input hdmi:
+ - cable
+ settings input svideo:
+ - plugs
+ settings overscan:
+ - expand
+ settings phone: []
+ settings power: []
+ settings remote:
+ - control
+ settings voice:
+ - microphone
+ shop:
+ - briefcase
+ shop two:
+ - briefcase
+ shopping basket: []
+ shopping cart: []
+ speaker notes:
+ - message
+ - chat
+ - comment
+ speaker notes off:
+ - message
+ - chat
+ - comment
+ spellcheck:
+ - letter
+ - alphabet
+ - checkmark
+ stars:
+ - rating
+ store:
+ - building
+ subject:
+ - lines
+ - text
+ supervisor account:
+ - user
+ - administrator
+ - person
+ swap horiz:
+ - arrows
+ swap vert:
+ - arrows
+ swap vertical circle:
+ - arrows
+ system update alt:
+ - arrow
+ tab: []
+ tab unselected: []
+ theaters:
+ - video
+ - media
+ - photography
+ thumb down:
+ - hand
+ thumb up:
+ - hand
+ thumbs up down:
+ - hand
+ timeline:
+ - chart
+ - graph
+ - line
+ toc:
+ - lines
+ - text
+ today:
+ - agenda
+ - calendar
+ toll:
+ - circles
+ touch app:
+ - hand
+ track changes:
+ - radar
+ - circle
+ translate:
+ - alphabet
+ - text
+ - letter
+ trending down:
+ - chart
+ - graph
+ - arrow
+ trending flat:
+ - arrow
+ - chart
+ - graph
+ trending up:
+ - arrow
+ - chart
+ - graph
+ turned in: []
+ turned in not: []
+ update:
+ - clock
+ - time
+ - arrow
+ - future
+ verified user:
+ - shield
+ - checkmark
+ view agenda:
+ - blocks
+ view array:
+ - blocks
+ view carousel:
+ - blocks
+ view column:
+ - blocks
+ view day:
+ - blocks
+ view headline:
+ - blocks
+ view list:
+ - blocks
+ view module:
+ - blocks
+ view quilt:
+ - blocks
+ view stream:
+ - blocks
+ view week:
+ - blocks
+ visibility:
+ - eye
+ visibility off:
+ - eye
+ - invisible
+ watch later:
+ - clock
+ - time
+ work:
+ - briefcase
+ - suitcase
+ youtube searched for:
+ - arrow
+ - looking glass
+ zoom in:
+ - looking glass
+ zoom out:
+ - looking glass
+alert:
+ add alert:
+ - bell
+ - notification
+ - reminder
+ error:
+ - warning
+ - danger
+ - exclamation
+ - circle
+ error outline:
+ - warning
+ - danger
+ - exclamation
+ - circle
+ warning:
+ - danger
+ - exclamation
+ - triangle
+av:
+ add to queue:
+ - television
+ - monitor
+ - plus
+ airplay:
+ - apple
+ - television
+ - monitor
+ album:
+ - vinyl
+ - cd
+ - record
+ art track: []
+ av timer:
+ - stopwatch
+ branding watermark: []
+ call to action: []
+ closed caption:
+ - subtitles
+ equalizer:
+ - graph
+ - chart
+ explicit: []
+ fast forward: []
+ fast rewind: []
+ featured play list: []
+ featured video: []
+ fiber dvr: []
+ fiber manual record:
+ - dot
+ - circle
+ fiber new: []
+ fiber pin: []
+ fiber smart record: []
+ forward 10:
+ - arrow
+ - circle
+ forward 30:
+ - arrow
+ - circle
+ forward 5:
+ - arrow
+ - circle
+ games:
+ - playstation
+ - nintendo
+ - xbox
+ - dpad
+ - controller
+ hd: []
+ hearing: []
+ high quality:
+ - hq
+ library add:
+ - collection
+ library books:
+ - collection
+ library music:
+ - collection
+ loop:
+ - refresh
+ - loader
+ - loading
+ mic:
+ - microphone
+ mic none:
+ - microphone
+ mic off:
+ - microphone
+ movie:
+ - video
+ - film
+ music video: []
+ new releases:
+ - exclamation
+ not interested:
+ - disabled
+ - not allowed
+ - banned
+ - prohibited
+ note: []
+ pause: []
+ pause circle filled: []
+ pause circle outline: []
+ play arrow: []
+ play circle filled: []
+ play circle outline: []
+ playlist add: []
+ playlist add check:
+ - checkmark
+ playlist play: []
+ queue: []
+ queue music: []
+ queue play next: []
+ radio: []
+ recent actors:
+ - contacts
+ remove from queue:
+ - television
+ - monitor
+ repeat:
+ - arrows
+ repeat one:
+ - arrows
+ replay:
+ - undo
+ - rewind
+ - arrow
+ replay 10:
+ - rewind
+ - arrow
+ replay 30:
+ - rewind
+ - arrow
+ replay 5:
+ - rewind
+ - arrow
+ shuffle:
+ - arrows
+ skip next: []
+ skip previous: []
+ slow motion video:
+ - circle
+ - time
+ snooze:
+ - alarm
+ sort by alpha:
+ - alphabet
+ - letters
+ stop:
+ - square
+ subscriptions:
+ - youtube
+ - playlist
+ - queue
+ subtitles:
+ - captions
+ surround sound:
+ - audio
+ video call:
+ - camera
+ video label: []
+ video library:
+ - collection
+ videocam:
+ - camera
+ videocam off:
+ - camera
+ volume down:
+ - sound
+ volume mute:
+ - sound
+ volume off:
+ - sound
+ volume up:
+ - sound
+ web:
+ - blocks
+ web asset: []
+communication:
+ business:
+ - building
+ - flat
+ - apartment
+ call:
+ - phone
+ call end:
+ - phone
+ call made:
+ - arrow
+ call merge:
+ - arrow
+ call missed:
+ - arrow
+ call missed outgoing:
+ - arrow
+ call received:
+ - arrow
+ call split:
+ - arrow
+ chat:
+ - message
+ - comment
+ chat bubble:
+ - message
+ - comment
+ chat bubble outline:
+ - message
+ - comment
+ clear all:
+ - lines
+ - notifications
+ comment:
+ - chat
+ - message
+ contact mail:
+ - user
+ - person
+ contact phone:
+ - user
+ - person
+ contacts:
+ - user
+ - person
+ dialer sip:
+ - phone
+ dialpad:
+ - dots
+ - phone
+ email:
+ - letter
+ - envelope
+ forum:
+ - chat
+ - messages
+ - conversation
+ import contacts:
+ - book
+ - magazine
+ import export:
+ - arrows
+ invert colors off:
+ - drop
+ - liquid
+ - water
+ live help:
+ - comment
+ - question
+ - faq
+ location off:
+ - gps
+ location on:
+ - gps
+ mail outline:
+ - letter
+ - envelope
+ message:
+ - chat
+ - comment
+ no sim: []
+ phone:
+ - mobile
+ phonelink erase:
+ - mobile
+ phonelink lock:
+ - mobile
+ - security
+ phonelink ring:
+ - mobile
+ phonelink setup:
+ - mobile
+ - settings
+ portable wifi off:
+ - network
+ present to all:
+ - arrow
+ ring volume:
+ - phone
+ - mobile
+ rss feed:
+ - wifi
+ - network
+ screen share:
+ - monitor
+ - laptop
+ speaker phone:
+ - mobile
+ stay current landscape:
+ - mobile
+ - phone
+ stay current portrait:
+ - mobile
+ - phone
+ stay primary landscape:
+ - mobile
+ - phone
+ stay primary portrait:
+ - mobile
+ - phone
+ stop screen share:
+ - monitor
+ - laptop
+ swap calls:
+ - arrow
+ textsms:
+ - message
+ - chat
+ - comment
+ voicemail: []
+ vpn key:
+ - security
+content:
+ add:
+ - plus
+ - new
+ - create
+ add box:
+ - plus
+ - new
+ - create
+ - square
+ add circle:
+ - plus
+ - new
+ - create
+ add circle outline:
+ - plus
+ - new
+ - create
+ archive: []
+ backspace:
+ - delete
+ - clear
+ - remove
+ block:
+ - disable
+ - not allowed
+ - cancel
+ - banned
+ clear:
+ - disable
+ - cancel
+ - cross
+ - times
+ - x
+ content copy:
+ - files
+ content cut:
+ - scissor
+ content paste:
+ - clipboard
+ create:
+ - pencil
+ - new
+ delete sweep:
+ - trashcan
+ - bin
+ drafts:
+ - email
+ - letter
+ - envelope
+ filter list:
+ - lines
+ flag: []
+ font download:
+ - letter
+ - square
+ forward:
+ - arrow
+ gesture:
+ - line
+ - drawing
+ inbox: []
+ link: []
+ low priority:
+ - arrow
+ - list
+ mail:
+ - letter
+ - envelope
+ markunread:
+ - letter
+ - envelope
+ move to inbox: []
+ next week:
+ - suitcase
+ - briefcase
+ redo:
+ - arrow
+ remove:
+ - minus
+ - line
+ remove circle:
+ - not allowed
+ - disable
+ - banned
+ remove circle outline:
+ - not allowed
+ - disable
+ - banned
+ reply:
+ - arrow
+ reply all:
+ - arrows
+ report:
+ - exclamation
+ save:
+ - floppy
+ - diskette
+ select all: []
+ send:
+ - chat
+ - message
+ - telegram
+ sort:
+ - lines
+ text format:
+ - letter
+ unarchive:
+ - arrow
+ undo:
+ - arrow
+ weekend:
+ - couch
+ - seat
+ - chair
+device:
+ access alarm:
+ - time
+ - clock
+ access alarms:
+ - time
+ - clock
+ access time:
+ - time
+ - clock
+ add alarm:
+ - time
+ - clock
+ - plus
+ airplanemode active:
+ - flying
+ airplanemode inactive:
+ - flying
+ battery 20: []
+ battery 30: []
+ battery 50: []
+ battery 60: []
+ battery 80: []
+ battery 90: []
+ battery alert:
+ - exclamation
+ battery charging 20: []
+ battery charging 30: []
+ battery charging 50: []
+ battery charging 60: []
+ battery charging 80: []
+ battery charging 90: []
+ battery charging full:
+ - lightning
+ battery full: []
+ battery std: []
+ battery unknown:
+ - question
+ bluetooth:
+ - wireless
+ - network
+ bluetooth connected:
+ - wireless
+ - network
+ bluetooth disabled:
+ - wireless
+ - network
+ bluetooth searching:
+ - wireless
+ - network
+ brightness auto: []
+ brightness high: []
+ brightness low: []
+ brightness medium: []
+ data usage:
+ - circle
+ developer mode:
+ - code
+ devices:
+ - laptop
+ - phone
+ - mobile
+ dvr:
+ - monitor
+ - laptop
+ gps fixed:
+ - location
+ gps not fixed: []
+ gps off: []
+ graphic eq:
+ - equalizer
+ - audio
+ location disabled: []
+ location searching: []
+ network cell: []
+ network wifi: []
+ nfc: []
+ screen lock landscape:
+ - mobile
+ - phone
+ - security
+ screen lock portrait:
+ - mobile
+ - phone
+ - security
+ screen lock rotation:
+ - mobile
+ - phone
+ screen rotation:
+ - mobile
+ - phone
+ sd storage:
+ - microsd
+ settings system daydream:
+ - cloud
+ signal cellular 0 bar: []
+ signal cellular 1 bar: []
+ signal cellular 2 bar: []
+ signal cellular 3 bar: []
+ signal cellular 4 bar:
+ - network
+ signal cellular connected no internet 0 bar: []
+ signal cellular connected no internet 1 bar: []
+ signal cellular connected no internet 2 bar: []
+ signal cellular connected no internet 3 bar: []
+ signal cellular connected no internet 4 bar:
+ - network
+ signal cellular no sim: []
+ signal cellular null:
+ - wifi
+ signal cellular off:
+ - network
+ signal wifi 0 bar: []
+ signal wifi 1 bar: []
+ signal wifi 1 bar lock: []
+ signal wifi 2 bar: []
+ signal wifi 2 bar lock: []
+ signal wifi 3 bar: []
+ signal wifi 3 bar lock: []
+ signal wifi 4 bar: []
+ signal wifi 4 bar lock:
+ - network
+ signal wifi off:
+ - network
+ signal wifi statusbar 1 bar 26x: []
+ signal wifi statusbar 2 bar 26x: []
+ signal wifi statusbar 3 bar 26x: []
+ signal wifi statusbar 4 bar 26x: []
+ signal wifi statusbar connected no internet 1 26x: []
+ signal wifi statusbar connected no internet 2 26x: []
+ signal wifi statusbar connected no internet 26x: []
+ signal wifi statusbar connected no internet 3 26x: []
+ signal wifi statusbar connected no internet 4 26x: []
+ signal wifi statusbar not connected 26x: []
+ signal wifi statusbar null 26x: []
+ storage:
+ - network
+ - server
+ - database
+ usb: []
+ wallpaper:
+ - image
+ - picture
+ widgets:
+ - blocks
+ wifi lock:
+ - network
+ wifi tethering:
+ - network
+editor:
+ attach file:
+ - paperclip
+ attach money:
+ - financial
+ - dollar
+ border all: []
+ border bottom: []
+ border clear: []
+ border color:
+ - pencil
+ - marker
+ - create
+ border horizontal: []
+ border inner: []
+ border left: []
+ border outer: []
+ border right: []
+ border style: []
+ border top: []
+ border vertical: []
+ bubble chart:
+ - graph
+ drag handle:
+ - lines
+ format align center:
+ - lines
+ format align justify:
+ - lines
+ format align left:
+ - lines
+ format align right:
+ - lines
+ format bold: []
+ format clear: []
+ format color fill:
+ - bucket
+ format color reset:
+ - drop
+ - water
+ - liquid
+ format color text: []
+ format indent decrease: []
+ format indent increase: []
+ format italic: []
+ format line spacing: []
+ format list bulleted: []
+ format list numbered: []
+ format paint:
+ - paintroller
+ format quote:
+ - quotation
+ format shapes: []
+ format size: []
+ format strikethrough: []
+ format textdirection l to r:
+ - paragraph
+ format textdirection r to l:
+ - paragraph
+ format underlined: []
+ functions:
+ - sigma
+ highlight:
+ - marker
+ - flashlight
+ insert chart:
+ - graph
+ insert comment:
+ - chat
+ - message
+ insert drive file: []
+ insert emoticon: []
+ insert invitation:
+ - agenda
+ - calendar
+ insert link: []
+ insert photo:
+ - image
+ - wallpaper
+ linear scale: []
+ merge type:
+ - arrow
+ mode comment:
+ - message
+ - chat
+ mode edit:
+ - pencil
+ - create
+ - new
+ monetization on:
+ - money
+ - financial
+ - dollar
+ money off:
+ - money
+ - financial
+ - dollar
+ multiline chart:
+ - graph
+ pie chart:
+ - graph
+ pie chart outlined:
+ - graph
+ publish:
+ - upload
+ - arrow
+ short text:
+ - lines
+ show chart:
+ - graph
+ space bar: []
+ strikethrough s: []
+ text fields: []
+ title: []
+ vertical align bottom:
+ - arrow
+ vertical align center:
+ - arrow
+ vertical align top:
+ - arrow
+ wrap text:
+ - arrow
+file:
+ attachment:
+ - paperclip
+ cloud: []
+ cloud circle: []
+ cloud done:
+ - checkmark
+ cloud download: []
+ cloud off: []
+ cloud queue: []
+ cloud upload: []
+ create new folder:
+ - directory
+ file download: []
+ file upload: []
+ folder:
+ - directory
+ folder open:
+ - directory
+ folder shared:
+ - directory
+hardware:
+ cast:
+ - chromecast
+ cast connected:
+ - chromecast
+ computer:
+ - laptop
+ - pc
+ - monitor
+ desktop mac:
+ - pc
+ - apple
+ - monitor
+ desktop windows:
+ - pc
+ - monitor
+ developer board:
+ - devkit
+ device hub: []
+ devices other:
+ - smartwatch
+ dock:
+ - charger
+ gamepad: []
+ headset:
+ - earmuffs
+ - headphones
+ headset mic:
+ - headphones
+ keyboard: []
+ keyboard arrow down:
+ - chevron
+ keyboard arrow left:
+ - chevron
+ keyboard arrow right:
+ - chevron
+ keyboard arrow up:
+ - chevron
+ keyboard backspace: []
+ keyboard capslock:
+ - arrow
+ keyboard hide: []
+ keyboard return:
+ - arrow
+ keyboard tab:
+ - arrow
+ keyboard voice: []
+ laptop: []
+ laptop chromebook: []
+ laptop mac:
+ - apple
+ laptop windows: []
+ memory:
+ - chip
+ mouse: []
+ phone android:
+ - mobile
+ phone iphone:
+ - mobile
+ - apple
+ phonelink: []
+ phonelink off: []
+ power input:
+ - dc
+ router:
+ - device
+ - network
+ scanner:
+ - device
+ security:
+ - shield
+ sim card: []
+ smartphone:
+ - mobile
+ speaker:
+ - audio
+ speaker group:
+ - audio
+ tablet: []
+ tablet android: []
+ tablet mac:
+ - apple
+ toys:
+ - fan
+ tv:
+ - television
+ videogame asset:
+ - gamepad
+ - controller
+ - playstation
+ - xbox
+ - nintendo
+ watch:
+ - smartwatch
+image:
+ add a photo:
+ - camera
+ add to photos:
+ - plus
+ adjust:
+ - circle
+ assistant:
+ - star
+ - comment
+ - chat
+ assistant photo: []
+ audiotrack:
+ - note
+ blur circular:
+ - circle
+ blur linear: []
+ blur off: []
+ blur on: []
+ brightness 1: []
+ brightness 2:
+ - moon
+ brightness 3:
+ - moon
+ brightness 4:
+ - moon
+ - sun
+ brightness 5:
+ - sun
+ brightness 6:
+ - sun
+ brightness 7:
+ - sun
+ broken image:
+ - picture
+ brush:
+ - paint
+ burst mode:
+ - image
+ - picture
+ camera: []
+ camera alt: []
+ camera front: []
+ camera rear: []
+ camera roll: []
+ center focus strong: []
+ center focus weak: []
+ collections: []
+ collections bookmark: []
+ color lens: []
+ colorize:
+ - color picker
+ - pipette
+ compare: []
+ control point:
+ - circle
+ - plus
+ control point duplicate:
+ - circle
+ - plus
+ crop: []
+ crop 16 9:
+ - square
+ crop 3 2:
+ - square
+ crop 5 4:
+ - square
+ crop 7 5:
+ - square
+ crop din:
+ - square
+ crop free:
+ - square
+ crop landscape:
+ - square
+ crop original:
+ - square
+ crop portrait:
+ - square
+ crop rotate: []
+ crop square: []
+ dehaze: []
+ details:
+ - triangle
+ edit: []
+ exposure:
+ - plus
+ - minus
+ exposure neg 1:
+ - minus
+ exposure neg 2:
+ - minus
+ exposure plus 1:
+ - plus
+ exposure plus 2:
+ - plus
+ exposure zero: []
+ filter: []
+ filter 1: []
+ filter 2: []
+ filter 3: []
+ filter 4: []
+ filter 5: []
+ filter 6: []
+ filter 7: []
+ filter 8: []
+ filter 9: []
+ filter 9 plus: []
+ filter b and w: []
+ filter center focus: []
+ filter drama:
+ - cloud
+ filter frames: []
+ filter hdr:
+ - mountains
+ filter none: []
+ filter tilt shift: []
+ filter vintage:
+ - flower
+ flare:
+ - lensflare
+ - sun
+ - star
+ - light
+ - bright
+ - shine
+ - sparkle
+ flash auto:
+ - lightning
+ flash off:
+ - lightning
+ flash on:
+ - lightning
+ flip: []
+ gradient: []
+ grain: []
+ grid off: []
+ grid on: []
+ hdr off: []
+ hdr on: []
+ hdr strong: []
+ hdr weak: []
+ healing:
+ - bandaid
+ image:
+ - picture
+ image aspect ratio: []
+ iso: []
+ landscape: []
+ leak add: []
+ leak remove: []
+ lens: []
+ linked camera: []
+ looks:
+ - rainbow
+ looks 3: []
+ looks 4: []
+ looks 5: []
+ looks 6: []
+ looks one: []
+ looks two: []
+ loupe: []
+ monochrome photos: []
+ movie creation:
+ - video
+ - film
+ movie filter:
+ - video
+ - film
+ music note: []
+ nature:
+ - tree
+ - forest
+ nature people:
+ - tree
+ - forest
+ - person
+ navigate before:
+ - arrow
+ navigate next:
+ - arrow
+ palette: []
+ panorama:
+ - picture
+ - image
+ panorama fish eye: []
+ panorama horizontal: []
+ panorama vertical: []
+ panorama wide angle: []
+ photo: []
+ photo album:
+ - picture
+ - image
+ photo camera:
+ - picture
+ - image
+ photo filter: []
+ photo library:
+ - picture
+ - image
+ photo size select actual: []
+ photo size select large: []
+ photo size select small: []
+ picture as pdf: []
+ portrait: []
+ remove red eye: []
+ rotate 90 degrees ccw:
+ - arrow
+ rotate left:
+ - arrow
+ - circle
+ rotate right:
+ - arrow
+ - circle
+ slideshow: []
+ straighten:
+ - piano
+ style: []
+ switch camera: []
+ switch video: []
+ tag faces: []
+ texture: []
+ timelapse: []
+ timer:
+ - stopwatch
+ timer 10: []
+ timer 3: []
+ timer off:
+ - stopwatch
+ tonality: []
+ transform: []
+ tune:
+ - settings
+ - sliders
+ view comfy:
+ - grid
+ view compact:
+ - grid
+ vignette: []
+ wb auto: []
+ wb cloudy: []
+ wb incandescent:
+ - lightbulb
+ wb iridescent: []
+ wb sunny: []
+maps:
+ add location:
+ - gps
+ beenhere:
+ - checkmark
+ directions:
+ - naviate
+ - arrow
+ directions bike:
+ - bicycle
+ directions boat: []
+ directions bus: []
+ directions car: []
+ directions railway: []
+ directions run:
+ - person
+ directions subway: []
+ directions transit:
+ - subway
+ - metro
+ directions walk:
+ - person
+ edit location:
+ - gps
+ ev station:
+ - gas station
+ - gasoline
+ - fuel
+ - filling station
+ flight:
+ - flying
+ - airplane
+ hotel:
+ - bed
+ - sleep
+ layers: []
+ layers clear: []
+ local activity:
+ - star
+ local airport:
+ - flying
+ - airplane
+ local atm:
+ - financial
+ - dollar
+ - money
+ local bar:
+ - alcohol
+ - drink
+ - martini
+ local cafe:
+ - drink
+ - coffee
+ local car wash: []
+ local convenience store:
+ - building
+ local dining:
+ - cutlery
+ - knife
+ - spoon
+ local drink:
+ - glass
+ - water
+ local florist:
+ - flower
+ local gas station:
+ - gasoline
+ - fuel
+ - filling station
+ local grocery store: []
+ local hospital:
+ - plus
+ local hotel: []
+ local laundry service: []
+ local library:
+ - book
+ - person
+ local mall:
+ - shopping bag
+ local movies: []
+ local offer: []
+ local parking: []
+ local pharmacy:
+ - food
+ local phone: []
+ local pizza: []
+ local play: []
+ local post office: []
+ local printshop: []
+ local see: []
+ local shipping:
+ - car
+ - truck
+ - semi
+ local taxi:
+ - car
+ map: []
+ my location: []
+ navigation:
+ - arrow
+ near me:
+ - arrow
+ person pin:
+ - location
+ - gps
+ person pin circle:
+ - location
+ - gps
+ pin drop:
+ - location
+ - gps
+ place: []
+ rate review:
+ - comment
+ - message
+ - chat
+ restaurant:
+ - cutlery
+ - knife
+ - fork
+ restaurant menu: []
+ satellite: []
+ store mall directory:
+ - building
+ streetview:
+ - map
+ - gps
+ - location
+ subway:
+ - metro
+ terrain: []
+ traffic:
+ - light
+ train: []
+ tram: []
+ transfer within a station:
+ - person
+ zoom out map:
+ - arrows
+navigation:
+ apps:
+ - grid
+ arrow back: []
+ arrow downward: []
+ arrow drop down: []
+ arrow drop down circle: []
+ arrow drop up: []
+ arrow forward: []
+ arrow upward: []
+ cancel:
+ - disable
+ - cross
+ check:
+ - checkmark
+ chevron left:
+ - arrow
+ chevron right:
+ - arrow
+ close:
+ - cancel
+ - disable
+ - not allowed
+ expand less:
+ - arrow
+ expand more:
+ - arrow
+ first page:
+ - arrow
+ fullscreen: []
+ fullscreen exit: []
+ last page:
+ - arrow
+ menu: []
+ more horiz:
+ - dots
+ more vert:
+ - dots
+ refresh: []
+ subdirectory arrow left:
+ - arrow
+ subdirectory arrow right:
+ - arrow
+ unfold less:
+ - arrow
+ unfold more:
+ - arrow
+notification:
+ adb:
+ - android
+ airline seat flat:
+ - bed
+ - sleep
+ airline seat flat angled:
+ - bed
+ - sleep
+ airline seat individual suite:
+ - bed
+ - sleep
+ airline seat legroom extra: []
+ airline seat legroom normal: []
+ airline seat legroom reduced: []
+ airline seat recline extra: []
+ airline seat recline normal: []
+ bluetooth audio: []
+ confirmation number:
+ - ticket
+ disc full:
+ - cd
+ - vinyl
+ - exclamation
+ do not disturb:
+ - banned
+ do not disturb alt:
+ - banned
+ do not disturb off:
+ - banned
+ do not disturb on:
+ - banned
+ drive eta: []
+ enhanced encryption:
+ - security
+ - lock
+ event available:
+ - agenda
+ - calendar
+ event busy:
+ - agenda
+ - calendar
+ event note:
+ - agenda
+ - calendar
+ folder special:
+ - directory
+ live tv:
+ - monitor
+ - television
+ mms:
+ - chat
+ - message
+ - comment
+ more:
+ - tag
+ - badge
+ network check:
+ - wifi
+ network locked:
+ - wifi
+ no encryption:
+ - lock
+ - security
+ ondemand video:
+ - monitor
+ - television
+ personal video:
+ - monitor
+ - television
+ phone bluetooth speaker: []
+ phone forwarded: []
+ phone in talk: []
+ phone locked:
+ - security
+ phone missed: []
+ phone paused: []
+ power:
+ - socket
+ - plug
+ priority high:
+ - exclamation
+ rv hookup: []
+ sd card: []
+ sim card alert:
+ - exclamation
+ sms:
+ - chat
+ - comment
+ - message
+ sms failed:
+ - chat
+ - comment
+ - message
+ sync: []
+ sync disabled:
+ - refresh
+ - arrows
+ sync problem:
+ - refresh
+ - arrows
+ system update:
+ - download
+ tap and play:
+ - nfc
+ - wifi
+ time to leave: []
+ vibration:
+ - mobile
+ - phone
+ voice chat:
+ - camera
+ vpn lock:
+ - world
+ - security
+ wc:
+ - person
+ - toilet
+ - unisex
+ wifi:
+ - network
+places:
+ ac unit:
+ - snowflake
+ - freeze
+ airport shuttle:
+ - bus
+ - car
+ all inclusive:
+ - infinite
+ beach access:
+ - umbrella
+ - parasol
+ business center:
+ - suitcase
+ - briefcase
+ casino:
+ - dice
+ child care:
+ - baby
+ child friendly:
+ - baby
+ fitness center:
+ - weights
+ - workout
+ free breakfast:
+ - tea
+ - coffee
+ - drink
+ golf course:
+ - flag
+ hot tub:
+ - jacuzzi
+ kitchen:
+ - refrigerator
+ - cabinet
+ pool:
+ - water
+ - swimming
+ room service:
+ - bell
+ - concierge
+ rv hookup: []
+ smoke free:
+ - cigarette
+ smoking rooms:
+ - cigarette
+ spa:
+ - flower
+social:
+ cake:
+ - pie
+ domain: []
+ group:
+ - person
+ - user
+ group add:
+ - person
+ - user
+ - people
+ location city:
+ - building
+ mood:
+ - smiley
+ - emoji
+ - emoticon
+ mood bad:
+ - smiley
+ - emoji
+ - emoticon
+ notifications:
+ - bell
+ - ring
+ notifications active:
+ - bell
+ - ring
+ notifications none:
+ - bell
+ - ring
+ notifications off:
+ - bell
+ - ring
+ notifications paused:
+ - bell
+ - ring
+ pages: []
+ party mode: []
+ people:
+ - person
+ - user
+ people outline:
+ - person
+ - user
+ person:
+ - user
+ person add:
+ - user
+ person outline:
+ - user
+ plus one:
+ - add
+ poll:
+ - graph
+ - chart
+ public:
+ - world
+ - earth
+ school:
+ - university
+ - college
+ sentiment very dissatisfied:
+ - smiley
+ - emoji
+ - emoticon
+ - sad
+ sentiment dissatisfied:
+ - smiley
+ - emoji
+ - emoticon
+ - sad
+ sentiment neutral:
+ - smiley
+ - emoji
+ - emoticon
+ sentiment satisfied:
+ - smiley
+ - emoji
+ - emoticon
+ - happy
+ sentiment very satisfied:
+ - smiley
+ - emoji
+ - emoticon
+ - happy
+ share: []
+ whatshot:
+ - fire
+toggle:
+ check box:
+ - checkmark
+ - square
+ check box outline blank:
+ - square
+ indeterminate check box:
+ - square
+ - minus
+ radio button checked:
+ - circle
+ radio button unchecked:
+ - circle
+ star:
+ - rating
+ star border:
+ - rating
+ star half:
+ - rating
diff --git a/sdk/python/packages/flet-mcp/src/flet_mcp/db.py b/sdk/python/packages/flet-mcp/src/flet_mcp/db.py
new file mode 100644
index 0000000000..c02b8fcbc5
--- /dev/null
+++ b/sdk/python/packages/flet-mcp/src/flet_mcp/db.py
@@ -0,0 +1,42 @@
+"""SQLite helpers for the MCP server."""
+
+import importlib.resources
+from pathlib import Path
+
+
+def get_db_path() -> Path:
+ """Return filesystem path to the bundled mcp.db."""
+ ref = importlib.resources.files("flet_mcp").joinpath("data/mcp.db")
+ # For installed packages, this returns a real path
+ return Path(str(ref))
+
+
+def snippet(text: str, query: str, length: int = 200) -> str:
+ """Extract a snippet around the first match of query tokens in text."""
+ if not text or not query:
+ return text[:length] if text else ""
+
+ query_tokens = query.lower().split()
+ text_lower = text.lower()
+
+ # Find the earliest match position
+ best_pos = len(text)
+ for token in query_tokens:
+ pos = text_lower.find(token)
+ if pos != -1 and pos < best_pos:
+ best_pos = pos
+
+ if best_pos == len(text):
+ return text[:length]
+
+ # Center snippet around match
+ start = max(0, best_pos - length // 4)
+ end = min(len(text), start + length)
+
+ result = text[start:end]
+ if start > 0:
+ result = "..." + result
+ if end < len(text):
+ result = result + "..."
+
+ return result
diff --git a/sdk/python/packages/flet-mcp/src/flet_mcp/icons_store.py b/sdk/python/packages/flet-mcp/src/flet_mcp/icons_store.py
new file mode 100644
index 0000000000..b87094ba7c
--- /dev/null
+++ b/sdk/python/packages/flet-mcp/src/flet_mcp/icons_store.py
@@ -0,0 +1,138 @@
+"""In-memory icon search across Material and Cupertino icon sets."""
+
+from __future__ import annotations
+
+import json
+from collections import defaultdict
+from importlib import resources
+
+import yaml
+
+
+class IconStore:
+ def __init__(self):
+ # Load icon JSON files from the flet package
+ material_path = resources.files("flet") / "controls" / "material" / "icons.json"
+ cupertino_path = (
+ resources.files("flet") / "controls" / "cupertino" / "cupertino_icons.json"
+ )
+
+ with resources.as_file(material_path) as p:
+ self._material: dict[str, int] = json.loads(p.read_text("utf-8"))
+
+ with resources.as_file(cupertino_path) as p:
+ self._cupertino: dict[str, int] = json.loads(p.read_text("utf-8"))
+
+ # Load synonym data from flet_mcp/data/icons.yml
+ synonyms_path = resources.files("flet_mcp") / "data" / "icons.yml"
+ with resources.as_file(synonyms_path) as p:
+ self._synonyms_yaml: dict = yaml.safe_load(p.read_text("utf-8")) or {}
+
+ # Build a flat lookup: lowercase base name -> list of synonym keywords
+ self._synonym_map: dict[str, list[str]] = {}
+ for _category, icons in self._synonyms_yaml.items():
+ if not isinstance(icons, dict):
+ continue
+ for base_name, keywords in icons.items():
+ key = str(base_name).lower().replace(" ", "_")
+ if isinstance(keywords, list):
+ self._synonym_map[key] = [str(k).lower() for k in keywords]
+
+ # Inverted index: keyword -> set of (family, icon_name)
+ self._index: dict[str, set[tuple[str, str]]] = defaultdict(set)
+
+ # Track which (family, icon_name) pairs have synonym-contributed keywords
+ self._synonym_entries: set[tuple[str, str, str]] = set()
+
+ self._build_index()
+
+ def _build_index(self):
+ for icon_name in self._material:
+ self._index_icon("material", icon_name)
+ for icon_name in self._cupertino:
+ self._index_icon("cupertino", icon_name)
+
+ def _index_icon(self, family: str, icon_name: str):
+ entry = (family, icon_name)
+ tokens = icon_name.lower().split("_")
+ for token in tokens:
+ if token:
+ self._index[token].add(entry)
+
+ # Look up synonyms by the lowercase underscored name
+ lookup_key = icon_name.lower()
+ synonyms = self._synonym_map.get(lookup_key, [])
+ for keyword in synonyms:
+ self._index[keyword].add(entry)
+ self._synonym_entries.add((family, icon_name, keyword))
+
+ def find(
+ self,
+ query: str,
+ family: str | None = None,
+ limit: int = 10,
+ ) -> list[str]:
+ """Search icons by query string.
+
+ Args:
+ query: Space-separated search terms.
+ family: Optional filter — "material" or "cupertino".
+ limit: Maximum number of results to return.
+
+ Returns:
+ List of qualified icon names like "Icons.ARROW_BACK"
+ or "CupertinoIcons.BACK".
+ """
+ query_tokens = [t for t in query.lower().split() if t]
+ if not query_tokens:
+ return []
+
+ # Collect all candidate icons that match at least one token
+ candidates: dict[tuple[str, str], float] = defaultdict(float)
+
+ for token in query_tokens:
+ for entry in self._index.get(token, set()):
+ fam, icon_name = entry
+ if family and fam != family:
+ continue
+
+ icon_tokens = set(icon_name.lower().split("_"))
+
+ # Check if this token matched via synonym
+ is_synonym = (fam, icon_name, token) in self._synonym_entries
+ if is_synonym:
+ candidates[entry] += 5
+ else:
+ candidates[entry] += 10
+
+ # Bonus for exact full-name match
+ query_as_name = "_".join(query_tokens).upper()
+ for entry in list(candidates):
+ if entry[1] == query_as_name:
+ candidates[entry] += 100
+
+ # Bonus: all query tokens present in icon name tokens
+ for entry in list(candidates):
+ icon_tokens = set(entry[1].lower().split("_"))
+ synonym_keywords = {
+ kw
+ for f, n, kw in self._synonym_entries
+ if f == entry[0] and n == entry[1]
+ }
+ all_tokens = icon_tokens | synonym_keywords
+ if all(qt in all_tokens for qt in query_tokens):
+ # Already scored per-token above; the per-token +10/+5 covers this
+ pass
+
+ sorted_results = sorted(candidates.items(), key=lambda x: -x[1])
+
+ results: list[str] = []
+ for (fam, icon_name), _score in sorted_results:
+ if len(results) >= limit:
+ break
+ if fam == "material":
+ results.append(f"Icons.{icon_name}")
+ else:
+ results.append(f"CupertinoIcons.{icon_name}")
+
+ return results
diff --git a/sdk/python/packages/flet-mcp/src/flet_mcp/server.py b/sdk/python/packages/flet-mcp/src/flet_mcp/server.py
new file mode 100644
index 0000000000..6075b206dc
--- /dev/null
+++ b/sdk/python/packages/flet-mcp/src/flet_mcp/server.py
@@ -0,0 +1,364 @@
+import json
+import sqlite3
+from typing import Optional
+
+from fastmcp import FastMCP
+
+from flet_mcp.api_store import ApiStore
+from flet_mcp.db import get_db_path
+from flet_mcp.icons_store import IconStore
+
+mcp = FastMCP(
+ "flet-mcp",
+ instructions=(
+ "Flet MCP server provides tools to search examples, documentation, "
+ "and API reference for building Flet applications. "
+ "Use search tools first to discover relevant content, "
+ "then use get tools to retrieve full details."
+ ),
+)
+
+_api_store: Optional[ApiStore] = None
+_icon_store: Optional[IconStore] = None
+
+
+def _get_api_store() -> ApiStore:
+ global _api_store
+ if _api_store is None:
+ _api_store = ApiStore()
+ return _api_store
+
+
+def _get_icon_store() -> IconStore:
+ global _icon_store
+ if _icon_store is None:
+ _icon_store = IconStore()
+ return _icon_store
+
+
+def _get_db() -> sqlite3.Connection:
+ db_path = get_db_path()
+ conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
+ conn.row_factory = sqlite3.Row
+ return conn
+
+
+# ── Examples ──────────────────────────────────────────────────────────────
+
+
+@mcp.tool()
+def search_examples(
+ query: str, platform: Optional[str] = None, limit: int = 5
+) -> list[dict]:
+ """Search Flet example projects by keyword.
+
+ Returns a ranked list of examples with id, title, description, and snippet.
+ Use get_example() to retrieve full source code.
+
+ Args:
+ query: Search keywords (e.g. "counter", "form validation", "navigation").
+ platform: Optional platform filter (web, ios, android, macos, windows, linux).
+ limit: Maximum number of results to return.
+ """
+ conn = _get_db()
+ try:
+ # Check if examples table exists
+ table_check = conn.execute(
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='examples_fts'"
+ ).fetchone()
+ if not table_check:
+ return []
+
+ # BM25 weights: title=8, description=5, tags=4, controls=5,
+ # layout_pattern=6, features=4, search_text=2, code=1
+ rows = conn.execute(
+ """
+ SELECT e.id, e.location, e.metadata,
+ snippet(examples_fts, 6, '**', '**', '...', 40) AS snip,
+ bm25(examples_fts, 8.0, 5.0, 4.0, 5.0, 6.0, 4.0, 2.0, 1.0) AS rank
+ FROM examples_fts
+ JOIN examples e ON examples_fts.rowid = e.rowid
+ WHERE examples_fts MATCH ?
+ ORDER BY rank
+ LIMIT ?
+ """,
+ (query, limit),
+ ).fetchall()
+
+ results = []
+ for row in rows:
+ meta = json.loads(row["metadata"])
+ if platform and platform.lower() not in [
+ p.lower() for p in meta.get("platforms", [])
+ ]:
+ continue
+ results.append(
+ {
+ "id": row["id"],
+ "title": meta.get("title", ""),
+ "description": meta.get("description", ""),
+ "controls": meta.get("controls", []),
+ "complexity": meta.get("complexity", ""),
+ "snippet": row["snip"] or "",
+ }
+ )
+ return results
+ finally:
+ conn.close()
+
+
+@mcp.tool()
+def get_example(example_id: str) -> dict:
+ """Get full source code and metadata for a specific example.
+
+ Args:
+ example_id: The example ID returned by search_examples().
+ """
+ conn = _get_db()
+ try:
+ table_check = conn.execute(
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='examples'"
+ ).fetchone()
+ if not table_check:
+ return {"error": "No examples indexed"}
+
+ row = conn.execute(
+ "SELECT id, location, metadata FROM examples WHERE id = ?",
+ (example_id,),
+ ).fetchone()
+ if not row:
+ return {"error": f"Example '{example_id}' not found"}
+
+ files = conn.execute(
+ "SELECT filename, content FROM example_files WHERE example_id = ?",
+ (example_id,),
+ ).fetchall()
+
+ meta = json.loads(row["metadata"])
+ return {
+ "id": row["id"],
+ "location": row["location"],
+ **meta,
+ "files": {f["filename"]: f["content"] for f in files},
+ }
+ finally:
+ conn.close()
+
+
+# ── Documentation ─────────────────────────────────────────────────────────
+
+
+@mcp.tool()
+def search_docs(query: str, limit: int = 5) -> list[dict]:
+ """Search Flet documentation by keyword.
+
+ Returns ranked results with title, location, and snippet.
+ Use get_doc() to retrieve full content.
+
+ Args:
+ query: Search keywords (e.g. "TextField validation", "routing", "theme").
+ limit: Maximum number of results to return.
+ """
+ conn = _get_db()
+ try:
+ # BM25 weights: title=6, location_text=4, content=1
+ rows = conn.execute(
+ """
+ SELECT d.location, d.title,
+ snippet(docs_fts, 2, '**', '**', '...', 40) AS snip,
+ bm25(docs_fts, 6.0, 4.0, 1.0) AS rank
+ FROM docs_fts
+ JOIN docs d ON docs_fts.rowid = d.rowid
+ WHERE docs_fts MATCH ?
+ ORDER BY rank
+ LIMIT ?
+ """,
+ (query, limit),
+ ).fetchall()
+
+ return [
+ {
+ "title": row["title"],
+ "location": row["location"],
+ "snippet": row["snip"] or "",
+ }
+ for row in rows
+ ]
+ finally:
+ conn.close()
+
+
+@mcp.tool()
+def get_doc(location: str) -> dict:
+ """Get full content of a documentation section by its location path.
+
+ Args:
+ location: The location path returned by search_docs()
+ (e.g. "controls/textfield/", "controls/textfield/#validation").
+ """
+ conn = _get_db()
+ try:
+ row = conn.execute(
+ "SELECT location, title, content FROM docs WHERE location = ?",
+ (location,),
+ ).fetchone()
+ if not row:
+ return {"error": f"Document '{location}' not found"}
+ return {
+ "location": row["location"],
+ "title": row["title"],
+ "content": row["content"],
+ }
+ finally:
+ conn.close()
+
+
+# ── API Reference ─────────────────────────────────────────────────────────
+
+
+@mcp.tool()
+def list_controls(
+ category: Optional[str] = None,
+ kind: Optional[str] = None,
+ limit: int = 50,
+) -> list[dict]:
+ """List available Flet controls and services.
+
+ Args:
+ category: Optional category filter (e.g. "input", "layout", "navigation").
+ kind: Optional kind filter - "control" for visual controls,
+ "service" for non-visual services (Audio, FilePicker, sensors, etc.).
+ limit: Maximum number of results to return.
+ """
+ store = _get_api_store()
+ return store.list_controls(category=category, kind=kind, limit=limit)
+
+
+@mcp.tool()
+def get_control_api(name: str) -> dict:
+ """Get detailed API reference for a specific control or service.
+
+ Returns properties (with types and docstrings), events, and methods
+ (with argument signatures). The response includes a 'kind' field
+ indicating whether it's a visual "control" or a non-visual "service".
+
+ Args:
+ name: Control or service class name (e.g. "TextField", "FilePicker", "Audio").
+ """
+ store = _get_api_store()
+ result = store.get_control(name)
+ if result is None:
+ return {"error": f"Control or service '{name}' not found"}
+ return result
+
+
+@mcp.tool()
+def get_type_api(name: str) -> dict:
+ """Get API reference for a non-control type (dataclass).
+
+ Returns fields, class methods, and factory methods for types like
+ ButtonStyle, TextStyle, Padding, Border, Theme, ColorScheme, etc.
+
+ Args:
+ name: Type class name (e.g. "ButtonStyle", "Padding", "TextStyle").
+ """
+ store = _get_api_store()
+ result = store.get_type(name)
+ if result is None:
+ return {"error": f"Type '{name}' not found"}
+ return result
+
+
+@mcp.tool()
+def get_enum(name: str) -> dict:
+ """Get enum definition.
+
+ For small enums (< 50 members), returns all members.
+ For large enums (Icons, CupertinoIcons), returns metadata and sample members.
+ Use search_enum_members() for large enum lookup.
+
+ Args:
+ name: Enum class name (e.g. "TextAlign", "Icons", "CupertinoIcons").
+ """
+ store = _get_api_store()
+ result = store.get_enum(name)
+ if result is None:
+ return {"error": f"Enum '{name}' not found"}
+ return result
+
+
+# ── Enum Search ───────────────────────────────────────────────────────────
+
+
+@mcp.tool()
+def search_enum_members(name: str, query: str, limit: int = 10) -> dict:
+ """Search enum members by name pattern.
+
+ Useful for large enums like Icons and CupertinoIcons.
+
+ Args:
+ name: Enum class name (e.g. "Icons", "CupertinoIcons").
+ query: Search pattern (e.g. "arrow", "settings", "home").
+ limit: Maximum number of results to return.
+ """
+ store = _get_api_store()
+ members = store.search_enum_members(name, query, limit=limit)
+ return {"enum": name, "matches": members}
+
+
+@mcp.tool()
+def enum_has_member(name: str, member: str) -> dict:
+ """Check if an enum has a specific member.
+
+ Use this to verify that an enum value exists before using it in code.
+
+ Args:
+ name: Enum class name (e.g. "Icons", "TextAlign").
+ member: Member name to check (e.g. "ARROW_BACK", "CENTER").
+ """
+ store = _get_api_store()
+ return {
+ "enum": name,
+ "member": member,
+ "exists": store.enum_has_member(name, member),
+ }
+
+
+# ── Icons ─────────────────────────────────────────────────────────────────
+
+
+@mcp.tool()
+def find_icon(query: str, family: Optional[str] = None, limit: int = 10) -> dict:
+ """Search Material and Cupertino icons by keyword.
+
+ Supports synonym matching (e.g. "user" finds "account_circle").
+
+ Args:
+ query: Descriptive keywords (e.g. "back arrow", "settings", "user profile").
+ family: Optional icon family filter - "material" or "cupertino".
+ limit: Maximum number of results to return.
+ """
+ store = _get_icon_store()
+ return {"icons": store.find(query, family=family, limit=limit)}
+
+
+# ── CLI Help ──────────────────────────────────────────────────────────────
+
+
+@mcp.tool()
+def get_cli_help(command: Optional[str] = None) -> dict:
+ """Get Flet CLI usage and options.
+
+ Without arguments, returns an overview of all available commands.
+ With a command name, returns detailed flags and options.
+
+ Args:
+ command: Optional command name (e.g. "run", "build", "publish", "create").
+ """
+ store = _get_api_store()
+ result = store.get_cli_help(command)
+ if command is None:
+ return {"commands": result}
+ if result is None:
+ return {"error": f"Unknown command: {command}"}
+ return result
diff --git a/sdk/python/packages/flet/src/flet/controls/base_control.py b/sdk/python/packages/flet/src/flet/controls/base_control.py
index 2a0a41c74d..6cb77a05d7 100644
--- a/sdk/python/packages/flet/src/flet/controls/base_control.py
+++ b/sdk/python/packages/flet/src/flet/controls/base_control.py
@@ -62,6 +62,8 @@ def control(
*,
isolated: Optional[bool] = None,
post_init_args: int = 1,
+ categories: Optional[tuple[str, ...]] = None,
+ tags: Optional[tuple[str, ...]] = None,
**dataclass_kwargs: Any,
) -> Callable[[type[T]], type[T]]:
"""
@@ -79,6 +81,8 @@ def control(
*,
isolated: Optional[bool] = None,
post_init_args: int = 1,
+ categories: Optional[tuple[str, ...]] = None,
+ tags: Optional[tuple[str, ...]] = None,
**dataclass_kwargs: Any,
) -> Union[type[T], Callable[[type[T]], type[T]]]:
"""
@@ -90,6 +94,8 @@ def control(
isolated: If `True`, marks the control as isolated. An isolated control
is excluded from page updates when its parent control is updated.
post_init_args: Number of InitVar arguments to pass to __post_init__.
+ categories: MCP metadata — control categories (e.g. ``("input", "form")``).
+ tags: MCP metadata — descriptive tags (e.g. ``("text", "editable")``).
dataclass_kwargs: Additional keyword arguments passed to `@dataclass`.
Usage:
@@ -97,18 +103,32 @@ def control(
- Supports `@control("WidgetName")` (with optional arguments)
- Supports `@control("WidgetName", post_init_args=1, isolated=True)` to
specify the number of `InitVar` arguments and isolation
+ - Supports `@control("WidgetName", categories=("input",), tags=("text",))`
+ to add MCP metadata for discovery
"""
# Case 1: If used as `@control` (without parentheses)
if isinstance(dart_widget_name, type):
return _apply_control(
- dart_widget_name, None, isolated, post_init_args, **dataclass_kwargs
+ dart_widget_name,
+ None,
+ isolated,
+ post_init_args,
+ categories=categories,
+ tags=tags,
+ **dataclass_kwargs,
)
# Case 2: If used as `@control("custom_type", post_init_args=N, isolated=True)`
def wrapper(cls: type[T]) -> type[T]:
return _apply_control(
- cls, dart_widget_name, isolated, post_init_args, **dataclass_kwargs
+ cls,
+ dart_widget_name,
+ isolated,
+ post_init_args,
+ categories=categories,
+ tags=tags,
+ **dataclass_kwargs,
)
return wrapper
@@ -119,11 +139,19 @@ def _apply_control(
type_name: Optional[str],
isolated: Optional[bool],
post_init_args: int,
+ categories: Optional[tuple[str, ...]] = None,
+ tags: Optional[tuple[str, ...]] = None,
**dataclass_kwargs,
) -> type[T]:
"""Applies @control logic, ensuring compatibility with @dataclass."""
cls = dataclass(**dataclass_kwargs)(cls) # Apply @dataclass first
+ # Store MCP metadata for discovery by Flet MCP server
+ cls.__control_meta__ = {
+ "categories": tuple(categories) if categories else (),
+ "tags": tuple(tags) if tags else (),
+ }
+
orig_post_init = getattr(cls, "__post_init__", lambda self, *args: None)
def new_post_init(self: T, *args):
diff --git a/sdk/python/packages/flet/src/flet/controls/core/column.py b/sdk/python/packages/flet/src/flet/controls/core/column.py
index 64c537afc8..ae04c572db 100644
--- a/sdk/python/packages/flet/src/flet/controls/core/column.py
+++ b/sdk/python/packages/flet/src/flet/controls/core/column.py
@@ -10,7 +10,7 @@
__all__ = ["Column"]
-@control("Column")
+@control("Column", categories=("layout",), tags=("vertical", "flex", "scrollable"))
class Column(LayoutControl, ScrollableControl, AdaptiveControl):
"""
Arranges child controls vertically, optionally aligning and spacing them within \
diff --git a/sdk/python/packages/flet/src/flet/controls/core/row.py b/sdk/python/packages/flet/src/flet/controls/core/row.py
index ab2c2724ec..de85293aa7 100644
--- a/sdk/python/packages/flet/src/flet/controls/core/row.py
+++ b/sdk/python/packages/flet/src/flet/controls/core/row.py
@@ -10,7 +10,7 @@
__all__ = ["Row"]
-@control("Row")
+@control("Row", categories=("layout",), tags=("horizontal", "flex", "scrollable"))
class Row(LayoutControl, ScrollableControl, AdaptiveControl):
"""
Displays its children in a horizontal array.
diff --git a/sdk/python/packages/flet/src/flet/controls/material/checkbox.py b/sdk/python/packages/flet/src/flet/controls/material/checkbox.py
index fbcbd8ca02..4c60116a5a 100644
--- a/sdk/python/packages/flet/src/flet/controls/material/checkbox.py
+++ b/sdk/python/packages/flet/src/flet/controls/material/checkbox.py
@@ -20,7 +20,7 @@
__all__ = ["Checkbox"]
-@control("Checkbox")
+@control("Checkbox", categories=("input", "selection"), tags=("form", "toggle"))
class Checkbox(LayoutControl, AdaptiveControl):
"""
Checkbox allows to select one or more items from a group, or switch between two \
diff --git a/sdk/python/packages/flet/src/flet/controls/material/tabs.py b/sdk/python/packages/flet/src/flet/controls/material/tabs.py
index fd5864618e..cc33bbc9b1 100644
--- a/sdk/python/packages/flet/src/flet/controls/material/tabs.py
+++ b/sdk/python/packages/flet/src/flet/controls/material/tabs.py
@@ -154,7 +154,7 @@ class UnderlineTabIndicator:
"""
-@control("Tabs")
+@control("Tabs", categories=("navigation", "layout"), tags=("tabbed", "switcher"))
class Tabs(LayoutControl, AdaptiveControl):
"""
Used for navigating frequently accessed, distinct content categories. Tabs allow \
diff --git a/sdk/python/packages/flet/src/flet/controls/material/textfield.py b/sdk/python/packages/flet/src/flet/controls/material/textfield.py
index 751afeaac1..811f8ee72c 100644
--- a/sdk/python/packages/flet/src/flet/controls/material/textfield.py
+++ b/sdk/python/packages/flet/src/flet/controls/material/textfield.py
@@ -268,7 +268,9 @@ def __init__(self):
super().__init__(regex_string=r"^[a-zA-Z]*$", allow=True, replacement_string="")
-@control("TextField")
+@control(
+ "TextField", categories=("input", "form"), tags=("text", "editable", "validation")
+)
class TextField(FormFieldControl, AdaptiveControl):
"""
A text field lets the user enter text, either with hardware keyboard or with an \
diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml
index 94f03f5f98..61c0dbf627 100644
--- a/sdk/python/pyproject.toml
+++ b/sdk/python/pyproject.toml
@@ -23,6 +23,7 @@ dependencies = [
"flet-geolocator",
"flet-lottie",
"flet-map",
+ "flet-mcp",
"flet-permission-handler",
"flet-rive",
"flet-secure-storage",
@@ -41,19 +42,21 @@ flet-audio = { workspace = true }
flet-audio-recorder = { workspace = true }
flet-camera = { workspace = true }
flet-charts = { workspace = true }
+flet-code-editor = { workspace = true }
+flet-color-pickers = { workspace = true }
flet-datatable2 = { workspace = true }
flet-flashlight = { workspace = true }
flet-geolocator = { workspace = true }
flet-lottie = { workspace = true }
flet-map = { workspace = true }
+flet-mcp = { workspace = true }
flet-permission-handler = { workspace = true }
flet-rive = { workspace = true }
flet-secure-storage = { workspace = true }
flet-video = { workspace = true }
flet-webview = { workspace = true }
-flet-code-editor = { workspace = true }
mkdocs-external-images = { git = "https://github.com/flet-dev/mkdocs-external-images", tag = "v0.2.0" }
-flet-color-pickers = { workspace = true }
+
[tool.uv.workspace]
members = ["packages/*"]
@@ -103,6 +106,10 @@ docs = [
"packaging >=23.0",
{ include-group = 'docs-coverage' },
]
+mcp-build = [
+ "markdownify >=0.14.1",
+ "griffe >=1.6.2",
+]
all = [
{ include-group = 'dev' },
{ include-group = 'docs' },
@@ -136,11 +143,12 @@ isort = { known-first-party = [
"flet_geolocator",
"flet_lottie",
"flet_map",
+ "flet_mcp",
"flet_permission_handler",
"flet_rive",
"flet_secure_storage",
"flet_video",
- "flet_webview"
+ "flet_webview",
] }
preview = true
select = [