mcp (0.2.0)
Installation
dart pub add mcp:0.2.0 --hosted-url=About this package
mcp
Lets an AI agent see and drive a running Flutter app, like Playwright does for a browser. Debug builds only: in profile and release builds, and on the web, start() does nothing.
Every running app registers itself in one shared file. One gateway, registered once in your MCP client, serves all of them, so there is nothing to configure per app or per port.
Add it to an app
dependencies:
mcp: ^0.2.0
import 'package:mcp/mcp.dart';
final mcp = Mcp(
name: 'My App',
version: '1.0.0',
state: () => {'user': session.userName},
tools: [
McpTool(
name: 'set_theme',
description: 'Switch between the light and dark theme.',
properties: {'dark': {'type': 'boolean'}},
requiredArguments: ['dark'],
handler: (arguments) async {
themeNotifier.value = arguments['dark'] == true ? ThemeMode.dark : ThemeMode.light;
return McpResult.text('ok');
},
),
],
);
Future<void> main() async {
await mcp.start();
runApp(const MyApp());
}
Optional: add McpNavigatorObserver() to navigatorObservers so state lists the open pages, and call McpLog.add(line) from your own logger to feed the log tool (debugPrint is captured already).
Optional: wrap the app in McpViewport so window can imitate monitors this screen can't show: other display scales, text sizes and resolutions larger than the screen. It passes the child straight through outside debug builds.
MaterialApp(
builder: (context, child) => McpViewport(child: child!),
)
Register the gateway once
build.bat
claude mcp add --scope user flutter -- <path to this repo>\MCP.exe
build.bat also writes MCP, a Linux x64 build (Dart cross-compiles it from Windows).
Or run it from source: claude mcp add --scope user flutter -- dart run mcp:gateway.
Flags: --name (the server name clients see, default flutter) and --registry (path of the registry file).
Built-in tools
| Tool | What it does |
|---|---|
state |
App name, version, open pages and whatever your state callback returns |
snapshot |
Every on-screen element as text: role, name, value, states, actions, position, and a ref |
click |
Tap by ref, by text (exact or partial, case blind, nth for repeats) or by widget key |
type |
Set the text of a field |
scroll |
Scroll the first or a chosen scrollable up, down, left or right |
adjust |
Step a slider up or down |
long_press |
Long-press an element or a position |
hover |
Move the mouse over an element or a position and leave it there (tooltips, hover states) |
drag |
Drag from an element or position to another one, or by dx/dy |
press_key |
Press a key or shortcut on the focused element, for example enter, shift+tab, ctrl+s |
window |
Show the app as on another monitor: monitor ("2560x1440") and scale, or width/height, plus textScale; reset undoes it |
wait_for |
Wait until some text or key is on screen |
screenshot |
PNG of the window, including embedded web views |
log |
Recent log lines |
long_press, hover and drag also take x and y, the logical position snapshot prints after @.
window resizes the real window when the size fits this screen at its own scale and text size (Windows). Anything else needs McpViewport: the app then lays out at the imitated size, scale and text size, shrunk to fit the window, and screenshot returns the imitated monitor's pixels.
With more than one app running, pass instance (an id, a pid, a platform or part of the app name). The instances tool lists them.
How it works
The snapshot and the actions use the app's semantics tree, so anything a screen reader can reach is reachable, and a widget you add later is covered with no extra code. Give icon-only buttons a tooltip or Semantics label so they get a name; a widget with only a ValueKey can still be clicked with key.
Each app serves loopback HTTP on a random port with a random bearer token, refuses browser origins and foreign hosts, and keeps itself in %USERPROFILE%\.mcp\instances.json (~/.local/state/Mcp/instances.json elsewhere; MCP_REGISTRY overrides it) with a 5 second heartbeat. The Windows registry stays out of %LOCALAPPDATA% because MSIX hosts such as the Claude desktop app give the processes they start a private copy of AppData, so a gateway started there would never see the apps. Entries not refreshed for 20 seconds are dropped. Apps started by flutter test do not register.