inkpal_bridge 5.0.0
inkpal_bridge: ^5.0.0 copied to clipboard
Turn your running Flutter app into an MCP server. Any AI coding assistant (Claude Code, Cursor, Windsurf, Codex, Copilot) can inspect, drive, and debug it — 50+ tools over local HTTP. Zero deps. Debug [...]
inkpal_bridge #
Turn your running Flutter app into an MCP server. Any AI coding assistant — Claude Code, Cursor, Windsurf, Codex CLI, Copilot in VS Code — can inspect, drive, and debug your live app. One dependency. One line of code. One JSON block. Done.
Install once, use everywhere #
# pubspec.yaml
dependencies:
inkpal_bridge: ^5.0.0
// lib/main.dart
import 'package:inkpal_bridge/inkpal_bridge.dart';
void main() => inkpalRunApp(const MyApp());
flutter run
That's it. The bridge starts an MCP HTTP server on
http://127.0.0.1:8767/mcp and prints:
┌──────────────────────────────────────────────────────────────┐
│ InkPal Bridge active │
│ MCP: http://127.0.0.1:8767/mcp │
│ │
│ Paste into your MCP client's config: │
│ {"mcpServers":{"inkpal":{"transport":"http", │
│ "url":"http://127.0.0.1:8767/mcp"}}} │
│ │
│ Free for everyone · no API key, no signup │
└──────────────────────────────────────────────────────────────┘
Paste the JSON into your AI assistant's MCP config (below) and restart it. Every one of the 53 bridge tools is now reachable from your editor.
Wire your AI assistant #
Each assistant has its own MCP config location — the shape is the same.
Claude Code — ~/.claude/mcp.json or .mcp.json in your project root:
{
"mcpServers": {
"inkpal": {
"transport": "http",
"url": "http://127.0.0.1:8767/mcp"
}
}
}
Cursor — ~/.cursor/mcp.json: same block.
Codex CLI — ~/.codex/config.toml:
[[mcp_servers]]
name = "inkpal"
transport = "http"
url = "http://127.0.0.1:8767/mcp"
Windsurf, VS Code (Copilot), and any other MCP-aware editor use the same JSON shape as Claude Code / Cursor.
Restart the editor once and ask:
"Take a screenshot, then tap the settings icon and tell me what changed."
What you get — 53 tools, all in-app #
The bridge exposes every tool your assistant needs to look at, drive, assert, and reason about your live Flutter app. No process to spawn, no external service, no ADB round-trips.
- Inspection —
observe(fused route + elements + logs + state in one call),get_interactive_elements,get_elements(typed query),get_widget_tree,find_widget,get_current_route,get_routes,get_app_state,get_app_map,get_screen_manifest,device_metrics,screenshot,screen_snapshot,screen_diff. - Interaction —
tap,tap_with_context,long_press,scroll,enter_text,navigate_to_route,navigate_back,increase_value,decrease_value,set_touch_feedback. - Wait + assert —
wait_for,wait_for_idle,assert_element,assert_no_errors,stability_check. - Accessibility + i18n —
accessibility_audit(WCAG-style code rules),verify_translations. - Runtime + logs —
get_runtime_errors,get_app_logs,get_recent_logs,query_logs(level / category / regex),get_performance. - Evaluate —
evaluate(Dart expression via opt-inevalHook). - App extensions / state seed —
list_app_extensions,call_app_extension. - State time-travel —
state_capture,state_list,state_get,state_diff,stream_state. - Recording —
recording_start,recording_stop,recording_status,recording_export. - Self-healing —
heal_watch_start,heal_watch_stop,heal_get_errors,heal_get_error_context,heal_verify_no_error.
Every tool is dispatched through an alias table verified in CI, so
public MCP names like inkpal_tap always route to the right internal
command.
Advanced: routers, state, custom widgets #
Custom design-system widgets — teach the semantics walker to recognise them:
inkpalRunApp(
const MyApp(),
walkerHooks: InkPalWalkerHooks(
isInteractiveWidget: (w) => w is BrandButton,
extractTextFrom: (w) => w is BrandButton ? w.label : null,
),
);
go_router — pass your router and route tracking is automatic
(including imperative context.push):
final router = GoRouter(routes: [...]);
inkpalRunApp(MyApp(router: router), router: router);
Other routers — pass an onNavigateToRoute callback:
inkpalRunApp(
const MyApp(),
onNavigateToRoute: (route) async => Get.toNamed(route),
);
Live app state — expose it so observe() includes it:
inkpalRunApp(
const MyApp(),
globalStateProvider: () async => {
'user': {'plan': currentUser.plan},
'cart': {'items': cart.length, 'total': cart.total},
},
);
State-seed extensions — register app-specific ops the agent can invoke directly (reseed data, flip a flag, jump onboarding):
InkPalAppExtensions.register(
name: 'seed',
description: 'Wipe and reseed the sample dataset.',
handler: (params) async {
await db.reseed();
return {'reseeded': true};
},
);
The agent discovers registered extensions via inkpal_list_app_extensions
and invokes them via inkpal_call_app_extension.
Dart expression evaluator (opt-in):
inkpalRunApp(
const MyApp(),
evalHook: (expression) async {
// Only expose what you want reachable — the bridge holds no default.
return await MyEvalScope.run(expression);
},
);
Privacy + security #
- Debug-only. Release builds bypass everything —
inkpalRunAppcollapses to a plainrunAppwith zero overhead. - Loopback-only. The MCP server binds to
127.0.0.1; nothing leaves your machine. - Sensitive headers redacted.
Authorization,Cookie,X-Api-Key, and similar patterns are stripped from any HTTP traffic surfaced to the agent. - Port-in-use is non-fatal. If
8767is taken the bridge logs a warning and stays healthy over its WebSocket fallback. - MIT-licensed. Open source.
Migrating from 4.x #
No source changes required. The licenseKey: and apiUrl: deprecated
shims from 4.0 continue to be accepted and ignored so 3.x code keeps
compiling. Delete them from your inkpalRunApp(...) call whenever you
want the analyzer to go quiet.
Support #
Issues + feature requests: GitHub.
Requirements #
- Flutter ≥ 3.10
- Dart ≥ 3.0