From bf3b7d7127eacbe822c302a6e34059354d0ff6fb Mon Sep 17 00:00:00 2001 From: drown0315 Date: Sun, 28 Jun 2026 14:04:51 +0800 Subject: [PATCH 1/5] test --- .../SKILL.md | 97 +++++ .../flutter-mcp-toolkit-control/SKILL.md | 239 +++++++++++ .../flutter-mcp-toolkit-custom-tools/SKILL.md | 139 ++++++ .../flutter-mcp-toolkit-debug/SKILL.md | 386 +++++++++++++++++ .../flutter-mcp-toolkit-guide/SKILL.md | 79 ++++ .../flutter-mcp-toolkit-inspect/SKILL.md | 228 ++++++++++ .../SKILL.md | 127 ++++++ .../flutter-mcp-toolkit-setup/SKILL.md | 222 ++++++++++ .../flutter-mcp-toolkit/flutter-mcp/SKILL.md | 91 ++++ example/.claude/skills/grill-me/SKILL.md | 10 + .../macos_visual_capture_-585b0c45b152b847 | Bin 0 -> 149896 bytes ...cos_visual_capture_-585b0c45b152b847.swift | 403 ++++++++++++++++++ example/.gitignore | 2 + example/CLAUDE.md | 55 +++ example/android/app/build.gradle | 2 + example/ios/Flutter/Debug.xcconfig | 1 + example/ios/Flutter/Release.xcconfig | 1 + example/ios/Podfile | 43 ++ example/lib/main.dart | 15 +- .../src/curve_app/widgets/ball_animation.dart | 26 +- .../flutter/generated_plugin_registrant.cc | 4 + example/linux/flutter/generated_plugins.cmake | 1 + example/macos/Flutter/Flutter-Debug.xcconfig | 1 + .../macos/Flutter/Flutter-Release.xcconfig | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 2 + example/macos/Podfile | 42 ++ example/macos/Podfile.lock | 22 + .../macos/Runner.xcodeproj/project.pbxproj | 106 ++++- .../xcshareddata/xcschemes/Runner.xcscheme | 3 +- .../contents.xcworkspacedata | 3 + example/macos/Runner/AppDelegate.swift | 6 +- example/pubspec.lock | 162 +++++-- example/pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 1 + 35 files changed, 2459 insertions(+), 65 deletions(-) create mode 100644 example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-cli-runtime-validation/SKILL.md create mode 100644 example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-control/SKILL.md create mode 100644 example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-custom-tools/SKILL.md create mode 100644 example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-debug/SKILL.md create mode 100644 example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-guide/SKILL.md create mode 100644 example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-inspect/SKILL.md create mode 100644 example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-repo-maintainer/SKILL.md create mode 100644 example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-setup/SKILL.md create mode 100644 example/.claude/skills/flutter-mcp-toolkit/flutter-mcp/SKILL.md create mode 100644 example/.claude/skills/grill-me/SKILL.md create mode 100755 example/.flutter_mcp/cache/macos_helper/macos_visual_capture_-585b0c45b152b847 create mode 100644 example/.flutter_mcp/cache/macos_helper/macos_visual_capture_-585b0c45b152b847.swift create mode 100644 example/CLAUDE.md create mode 100644 example/ios/Podfile create mode 100644 example/macos/Podfile create mode 100644 example/macos/Podfile.lock diff --git a/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-cli-runtime-validation/SKILL.md b/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-cli-runtime-validation/SKILL.md new file mode 100644 index 0000000..7f5c7f3 --- /dev/null +++ b/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-cli-runtime-validation/SKILL.md @@ -0,0 +1,97 @@ +--- +name: flutter-mcp-cli-runtime-validation +description: Run Flutter MCP runtime validation from CLI in two steps (launch app, then run validate-runtime), including toolkit-extension gating, screenshot/layout capture, app error collection, optional reload verification, and retry handling for transient first-connect failures. +--- +> Calls in this skill run via the `flutter-mcp-toolkit` CLI binary: +> flutter-mcp-toolkit exec --name --args '' +> Output is JSON on stdout. Errors come as `{"error":{"code":..., "message":..., "recovery":...}}`. +> Throughout this skill, calls are written as `tap_widget(selector: "...")` — translate to the CLI form. +> If the binary isn't on PATH, see `flutter-mcp-toolkit-setup`. + +# Flutter MCP CLI Runtime Validation + +Use this skill when you need agent-style runtime validation through `flutter-mcp-toolkit` with minimal operator steps. + +## Two-Step Flow + +1. Launch the Flutter app in debug mode. +2. Run one CLI command: + +```bash +dart run mcp_server_dart/bin/flutter_mcp_toolkit.dart --save-images --output-dir .flutter_mcp/runtime_validation validate-runtime \ + --target ws://127.0.0.1:8181//ws \ + --timeout-ms 10000 \ + --post-reload-delay-ms 500 \ + --after-reload +``` + +Optional skill install in the same command: + +```bash +dart run mcp_server_dart/bin/flutter_mcp_toolkit.dart validate-runtime \ + --target ws://127.0.0.1:8181//ws \ + --install-skill +``` + +Permission behavior for this flow: + +- `validate-runtime` stays read/write only for visual capture and defaults to `auto_request_once`. +- `doctor` remains read-only. +- On macOS, Screen Recording permission belongs to the host process running `flutter-mcp-toolkit`. +- On web: `desktop_window` uses macOS ScreenCaptureKit then Chrome CDP (`Page.captureScreenshot`); Linux/Windows use CDP when remote debugging is reachable. Pass `--web-browser-debugging-port` if discovery fails. With platform views detected, `validate-runtime` does not fall back to `flutter_layer` after a successful `desktop_window` capture. Check `captureHints.weakSignalsDetected` for `Texture`-only apps. +- Executor recovery retries host capture once (`desktopCaptureRetried` in screenshot payloads). When `captureHints.platformViewsDetected` is true, validate-runtime does not fall back to `flutter_layer`. Otherwise it may retry once with `flutter_layer` after a failed host capture. +- You may pass the VM URI as global `--vm-service-uri` instead of `validate-runtime --target` when only one URI is needed. + +## What `validate-runtime` Must Prove + +- Doctor preflight passes critical checks. +- Required toolkit extensions exist: + - `ext.mcp.toolkit.app_errors` + - `ext.mcp.toolkit.view_details` + - `ext.mcp.toolkit.view_screenshots` + - `ext.mcp.toolkit.inspect_widget_at_point` +- Screenshot capture works. +- View details (layout metadata) are available. +- App errors are retrievable. +- If `--after-reload` is enabled, post-reload screenshot also works. + +## Output Handling + +- Use `data.summary` as pass/fail status for automation. +- Use `data.summary.capturePlatformViewsDetected` and `captureFocusAttempted` for platform-view routing. +- Use `data.summary.captureFallbackUsed` to see whether a `flutter_layer` retry ran (skipped when platform views were detected). +- Use `data.steps` for per-step evidence and retries. +- Use `data.doctor.checks` to explain setup blockers. +- Use `data.summary.screenshotFiles` for saved screenshot paths when `--save-images` is enabled. +- When `--save-images` is enabled, read screenshot file URLs from step data. +- For visual debugging reports, also run: + - `exec --name capture_ui_snapshot --args '{"errorsCount":4,"compress":true,"includeViewDetails":true,"includeErrors":true}'` + - `exec --name inspect_widget_at_point --args '{"x":,"y":}'` + +## Failure Rules + +- If toolkit extensions are missing, stop and report instrumentation gap with exact fix: + - add `mcp_toolkit` to app dependencies + - ensure `MCPToolkitBinding.instance.bootstrapFlutter(...)` or equivalent manual initialization runs before `runApp` + - hot restart or rerun the app +- If first explicit URI connect fails, retry is automatic for retryable connection errors. +- If screenshots are blank, verify app window is visible and retry. +- If macOS visual capture is denied, use: + - `dart run mcp_server_dart/bin/flutter_mcp_toolkit.dart permissions status` + - `dart run mcp_server_dart/bin/flutter_mcp_toolkit.dart permissions request` + - `dart run mcp_server_dart/bin/flutter_mcp_toolkit.dart permissions open-settings` +- If app cannot be instrumented, do not claim screenshot/layout/error inspection success. + +## Visual QA + Source Mapping Rules + +- Always compare before/after screenshot evidence around changes. +- For each reported visual issue, provide coordinate + `inspect_widget_at_point` output. +- Map defects to source using `get_app_errors` top stack frame (`file`, `line`, `column`) when available. +- Do not use `debug_dump_*` unless explicitly requested. + +## Challenge Cases (Always Call Out Explicitly) + +- No running debug app: `doctor` critical failure on `vm_target_reachable`; request app launch before continuing. +- Wrong target URI/token: treat as connection mismatch and retry with exact `app.debugPort.wsUri`. +- Toolkit added but still missing extensions: hot reload is often insufficient, require hot restart/full rerun. +- Non-modifiable app (cannot add toolkit): report inspection as unavailable instead of guessing. diff --git a/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-control/SKILL.md b/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-control/SKILL.md new file mode 100644 index 0000000..b909d7f --- /dev/null +++ b/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-control/SKILL.md @@ -0,0 +1,239 @@ +--- +name: flutter-mcp-toolkit-control +description: Drive a running Flutter app — tap, scroll, type, fill forms, hot-reload, navigate. Use when you need to interact with the UI. +--- +> Calls in this skill run via the `flutter-mcp-toolkit` CLI binary: +> flutter-mcp-toolkit exec --name --args '' +> Output is JSON on stdout. Errors come as `{"error":{"code":..., "message":..., "recovery":...}}`. +> Throughout this skill, calls are written as `tap_widget(selector: "...")` — translate to the CLI form. +> If the binary isn't on PATH, see `flutter-mcp-toolkit-setup`. + +## When to use + +Use this skill when you need to drive a running Flutter app as a user would: +- Tap buttons, icons, list items, or any interactive widget. +- Type text into fields, submit forms, clear inputs. +- Scroll or swipe to reveal off-screen content. +- Navigate between routes programmatically (push, pop, popUntil). +- Dismiss dialogs and bottom sheets. +- Press keyboard keys (Enter, Escape, Tab, arrows, ASCII chars). +- Hot-reload or hot-restart after editing Dart source files. +- Combine reload + screenshot + semantics in one round-trip for fast iteration. + +## Selectors + +Every interaction tool targets a widget by **ref** — a short string like `"s_0"` returned by `semantic_snapshot`. There is no by-text or by-type selector syntax on the tool itself. The workflow is: call `semantic_snapshot`, scan the returned nodes, find the right ref, then pass it. + +Snapshot node fields to filter on: + +| Want to find | Scan field | Example value | +|---|---|---| +| By visible label / text | `label` | `"Login"` | +| By value or hint | `value` / `hint` | `"user@example.com"` | +| By tooltip | `tooltip` | `"Close"` | +| By widget key | `key` | `"[<'submitBtn'>]"` | +| By semantic role / type | `flags` or `actions` | `["tap"]` | + +Example — find the "Login" button ref: +``` +semantic_snapshot() +→ nodes: [{ref:"s_0", label:"Login", actions:["tap"]}, ...] +tap_widget(ref: "s_0") +``` + +Pass `snapshotId` (from the snapshot response) to any interaction call. If the tree has changed, the call returns `stale_snapshot` with both IDs so you know to re-snapshot. Refs are only valid against the most recent snapshot. + +## Recipes + +### Tap a widget by text +``` +semantic_snapshot() +→ find node where label == "Submit" → ref "s_3" +tap_widget(ref: "s_3", snapshotId: ) +``` + +### Fill a login form +``` +semantic_snapshot() → email ref "s_1", password ref "s_2" +fill_form(fields: [{ref:"s_1", text:"user@example.com"}, {ref:"s_2", text:"secret"}], snapshotId: ) +→ one round-trip; stops on first failure +``` + +### Scroll to find an item +``` +scroll(direction: "down", distance: 300) +semantic_snapshot() → item now visible → ref "s_5" +tap_widget(ref: "s_5") +``` + +### Wait for a widget to appear +``` +wait_for(predicate: {kind: "text", text: "Welcome"}, timeoutMs: 8000) +→ returns fresh snapshot when text appears +tap_widget(ref: ) +``` + +### Navigate to a route +``` +navigate(action: "push", route: "/settings", arguments: {tab: "account"}) +semantic_snapshot() → fresh refs in the new screen +``` + +### Hot reload after a code change +``` +hot_reload_and_capture() +→ screenshot + semantic snapshot + errors in one call +``` + +### Press the back hardware button + +`press_key` has no `Back` key. Use `navigate(action: "pop")` for Navigator pop; `handle_dialog(action: "dismiss")` for dialogs; `press_key(key: "Escape")` on desktop. + +``` +navigate(action: "pop") +``` + +## Tool reference + +### tap_widget +Tap a widget by ref. `ref` • string • required. `snapshotId` • integer • optional. `connection` • object • optional. +```json +{"name": "tap_widget", "arguments": {"ref": "s_3", "snapshotId": 7}} +``` +Returns: `{"via": "semantic_action", "ref": "s_3"}` — Failures: `stale_snapshot`, `ref_not_found` + +### long_press +Long-press a widget by ref. `ref` • string • required. `snapshotId` • integer • optional. `connection` • object • optional. +```json +{"name": "long_press", "arguments": {"ref": "s_2"}} +``` +Returns: `{"via": "semantic_action"}` — Failures: `stale_snapshot`, `ref_not_found` + +### enter_text +Enter text into a text field; taps to focus before typing. `ref` • string • required. `text` • string • required. `snapshotId` • integer • optional. `connection` • object • optional. +```json +{"name": "enter_text", "arguments": {"ref": "s_1", "text": "hello@example.com"}} +``` +Returns: `{"via": "editable_state"}` — Failures: `stale_snapshot`, `ref_not_found` + +### fill_form +Batch text entry: fills multiple fields in one call. Stops on first failure. `snapshotId` validated on first field only. `fields` • array of `{ref, text}` • required. `snapshotId` • integer • optional. `connection` • object • optional. +```json +{"name": "fill_form", "arguments": {"fields": [{"ref":"s_1","text":"user"},{"ref":"s_2","text":"pass"}], "snapshotId": 5}} +``` +Returns: `{"filled": 2}` — Failures: `stale_snapshot`, `ref_not_found` + +### scroll +Scroll to reveal content. `"down"` reveals content below (finger swipes up). `direction` • string • required (`up|down|left|right`). `ref` • string • optional (falls back to screen center). `distance` • number • optional • default 300. `snapshotId` • integer • optional. `connection` • object • optional. +```json +{"name": "scroll", "arguments": {"direction": "down", "ref": "s_0", "distance": 500}} +``` +Returns: `{"via": "semantic_action"}` — Failures: `ref_not_found`, `stale_snapshot` + +### swipe +High-velocity fling. Same direction model as `scroll`. Always Tier 2 pointer events. `direction` • string • required. `ref` • string • optional. `distance` • number • optional • default 300. `snapshotId` • integer • optional. `connection` • object • optional. +```json +{"name": "swipe", "arguments": {"direction": "left", "ref": "s_4"}} +``` +Returns: `{"via": "pointer_events"}` — Failures: `ref_not_found`, `web_gesture_not_supported` + +### drag +Drag from one widget to another. Always Tier 2. `fromRef` • string • required. `toRef` • string • required. `snapshotId` • integer • optional. `connection` • object • optional. +```json +{"name": "drag", "arguments": {"fromRef": "s_2", "toRef": "s_7"}} +``` +Returns: `{"via": "pointer_events"}` — Failures: `ref_not_found`, `web_gesture_not_supported` + +### hover +Synthesize a mouse hover. Desktop/web only — no hover concept on mobile. `ref` • string • required. `snapshotId` • integer • optional. `connection` • object • optional. +```json +{"name": "hover", "arguments": {"ref": "s_5"}} +``` +Returns: `{"via": "pointer_events"}` — Failures: `ref_not_found`, platform error on mobile + +### press_key +Synthesize key press (down+up). Accepted: `Enter Escape Tab Backspace Delete Space ArrowUp ArrowDown ArrowLeft ArrowRight` plus single ASCII (`a-z` `0-9`). `key` • string • required. `ctrl/shift/alt/meta` • boolean • optional • default false. `connection` • object • optional. +```json +{"name": "press_key", "arguments": {"key": "Enter"}} +``` +Returns: `{"key": "Enter"}` — Failures: `unsupported_key`, `no_focus` + +### wait_for +Wait for a UI predicate; returns fresh semantic snapshot. Predicates: `{kind:"text",text}` | `{kind:"noText",text}` | `{kind:"time",ms}` | `{kind:"stable",stableWindowMs}`. `predicate` • object • required. `timeoutMs` • integer • optional • default 5000 • max 30000. `connection` • object • optional. +```json +{"name": "wait_for", "arguments": {"predicate": {"kind": "text", "text": "Dashboard"}, "timeoutMs": 8000}} +``` +Returns: fresh semantic snapshot — Failures: `timeout`, `invalid_predicate` + +### navigate +Drive the registered Navigator. Requires `MCPToolkitBinding.instance.navigatorKey = key` in the app. `action` • string • required (`push|pop|popUntil`). `route` • string • required for push/popUntil. `arguments` • object • optional (for push). `connection` • object • optional. +```json +{"name": "navigate", "arguments": {"action": "push", "route": "/profile", "arguments": {"userId": "42"}}} +``` +Returns: `{"action": "push", "route": "/profile"}` — Failures: `navigator_not_configured`, `route_not_found` + +### handle_dialog +Dismiss the topmost popup/dialog route. Only `action: "dismiss"` supported. Requires `navigatorKey = key` on `MCPToolkitBinding.instance` in the app. `action` • string • required (must be `"dismiss"`). `connection` • object • optional. +```json +{"name": "handle_dialog", "arguments": {"action": "dismiss"}} +``` +Returns: `{"dismissed": true}` — Failures: `navigator_not_configured`, `no_dialog` + +### hot_reload_flutter +Hot reload the app. Preserves state. `force` • boolean • optional • default false (reload even without source changes). `connection` • object • optional. +```json +{"name": "hot_reload_flutter", "arguments": {}} +``` +Returns: `"Hot reload completed"` + report JSON — Failures: `vm_not_connected`, `compilation_error` + +### hot_restart_flutter +Full restart. App state not preserved. No required params. `connection` • object • optional. +```json +{"name": "hot_restart_flutter", "arguments": {}} +``` +Returns: `{"report": {"type": "Success", "success": true}}` — Failures: `vm_not_connected` + +### hot_reload_and_capture +Hot reload then capture screenshot + semantics + errors in one call. `compress` • boolean • default true. `includeSemantics` • boolean • default true. `includeErrors` • boolean • default true. `errorsCount` • integer • default 4. `connection` • object • optional. +```json +{"name": "hot_reload_and_capture", "arguments": {"includeErrors": true}} +``` +Returns: screenshot (base64) + semantic snapshot + errors — Failures: `vm_not_connected`, `compilation_error` + +## Patterns + +### Always `wait_for` before `tap_widget` after navigation + +After `navigate(action: "push")` the new route's widgets are not in the tree yet. Use `wait_for` with a text predicate to confirm the destination has rendered, then snapshot and act. + +``` +navigate(action: "push", route: "/checkout") +wait_for(predicate: {kind: "text", text: "Order Summary"}, timeoutMs: 5000) +semantic_snapshot() → tap target widgets +``` + +### Prefer `fill_form` over multiple `enter_text` calls + +Each `enter_text` is a separate VM round-trip. `fill_form` sends all field/text pairs in one call; `snapshotId` is checked once (on the first field). For any form with 2+ fields, always prefer `fill_form`. + +``` +# Avoid: 2 round-trips +enter_text(ref: "s_1", text: "Alice") +enter_text(ref: "s_2", text: "secret") + +# Prefer: 1 round-trip +fill_form(fields: [{ref: "s_1", text: "Alice"}, {ref: "s_2", text: "secret"}]) +``` + +### After `hot_reload_*`, wait for the new tree before continuing + +Hot reload completes asynchronously. Use `wait_for(predicate: {kind:"stable", stableWindowMs:300})` to confirm the tree has settled before calling `semantic_snapshot`. Or use `hot_reload_and_capture` which returns a post-reload snapshot directly. + +``` +hot_reload_flutter() +wait_for(predicate: {kind: "stable", stableWindowMs: 300}) +semantic_snapshot() → interact with reloaded widgets + +# Or in one call (preferred): +hot_reload_and_capture() → screenshot + semantics + errors already post-reload +``` diff --git a/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-custom-tools/SKILL.md b/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-custom-tools/SKILL.md new file mode 100644 index 0000000..9ae5b41 --- /dev/null +++ b/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-custom-tools/SKILL.md @@ -0,0 +1,139 @@ +--- +name: flutter-mcp-toolkit-custom-tools +description: Use this skill when the agent exposes app-specific surfaces by registering custom MCP tools and resources inside the Flutter app (mcp_toolkit dynamic registry — MCPCallEntry, bootstrapFlutter additionalEntries / addEntries). Covers tool vs resource vs evaluate-expression, Map-based handlers, schema strictness, discovery via fmt_list_client_tools_and_resources, fmt_client_tool, fmt_client_resource, and lifecycle pitfalls. +--- +> Calls in this skill run via the `flutter-mcp-toolkit` CLI binary: +> flutter-mcp-toolkit exec --name --args '' +> Output is JSON on stdout. Errors come as `{"error":{"code":..., "message":..., "recovery":...}}`. +> Throughout this skill, calls are written as `tap_widget(selector: "...")` — translate to the CLI form. +> If the binary isn't on PATH, see `flutter-mcp-toolkit-setup`. + +# Custom MCP Toolkit Tools & Resources (Dynamic Registry) + +Use this when bundled MCP tools (screenshot, semantic snapshot, tap, …) are not enough and you need **app-specific** read surfaces or actions — e.g. cart totals, feature flags, curated debug snapshots of internal state. Entries are registered **in the Flutter process** and exposed to the agent through the **dynamic registry**. + +## Pick the right primitive + +| Need | Use | +|------|-----| +| One-off read of a simple value | **`fmt_evaluate_dart_expression`** (no app code change). | +| Stable **read-only** payload (diagnostics, JSON snapshot, “current route”) | **`MCPCallEntry.resource`** + **`fmt_client_resource`**. Prefer resources when the contract is “GET-like” and idempotent. | +| Parameterized or mutating action, or reusable named operation | **`MCPCallEntry.tool`** + **`fmt_client_tool`**. | + +## Handler signature (tools and resources) + +[`MCPCallHandler`](https://github.com/Arenukvern/mcp_flutter/blob/main/mcp_toolkit/lib/src/mcp_models.dart) is `FutureOr Function(ServiceExtensionRequestMap request)` where **`ServiceExtensionRequestMap` is `Map`**. + +- Tool arguments arrive as **string values** keyed by schema property names — mirror the README pattern: `request['n']`, `request['userId']`, then parse (`int.tryParse`, `double.tryParse`, `jsonDecode` for nested blobs if the wire format sends JSON-as-string). +- Do **not** use `request.arguments` — that is not the app-side API. + +## Minimal tool registration + +```dart +import 'package:mcp_toolkit/mcp_toolkit.dart'; + +final tool = MCPCallEntry.tool( + handler: (request) async { + final userId = request['userId'] ?? ''; + final cart = CartRepository.instance.forUser(userId); + return MCPCallResult( + message: 'ok', + parameters: { + 'total': cart.total, + 'items': cart.items.map((i) => i.toJson()).toList(), + }, + ); + }, + definition: MCPToolDefinition( + name: 'cart_get_snapshot', + description: 'Return current cart total and items for a user.', + inputSchema: { + 'type': 'object', + 'additionalProperties': false, + 'properties': { + 'userId': {'type': 'string'}, + }, + 'required': ['userId'], + }, + ), +); + +await MCPToolkitBinding.instance.addEntries(entries: {tool}); +``` + +Prefer **`MCPToolkitBinding.instance.bootstrapFlutter(additionalEntries: { ... }, runApp: ...)`** so tools/resources register in one place with zone/error setup — same entries shape as above. + +Register **after** `initialize()` / **`bootstrapFlutter`** wiring, **once** at bootstrap — not inside `build`, not per-widget `initState`. + +## Custom resources + +Resources are for **read-only** MCP surfaces: diagnostics, config summaries, or JSON blobs the agent polls without treating them as imperative actions. + +```dart +MCPCallEntry.resource( + definition: MCPResourceDefinition( + name: 'app_cart_digest', + description: 'Compact cart summary for agents (read-only).', + mimeType: 'application/json', + ), + handler: (request) async => MCPCallResult( + message: 'Cart digest', + parameters: { + 'itemCount': CartRepository.instance.visibleCount, + 'currency': CartRepository.instance.currencyCode, + }, + ), +), +``` + +- **`name`** must be `snake_case` (letters, digits, underscores). [`resourceUri`](https://github.com/Arenukvern/mcp_flutter/blob/main/mcp_toolkit/lib/src/mcp_models.dart) maps it to a **`visual://localhost/...`** URI (underscore segments become path segments). Agents consume it via **`fmt_client_resource`** using that URI / listing from **`fmt_list_client_tools_and_resources`**. +- Set **`mimeType`** honestly (`application/json` vs `text/plain`) so clients know how to interpret payloads. + +## Schema rules (tools) + +The MCP server enforces strict JSON Schema: + +- Prefer **`additionalProperties: false`** unless you intentionally accept arbitrary keys. Unknown keys **fail validation** — good for catching agent typos. +- Mark **`required`** for anything the handler reads unconditionally. +- Prefer primitives and **`enum`** over unconstrained strings. +- **`parameters`** in **`MCPCallResult`** must be JSON-serializable; non-serializable objects degrade to **`toString()`**. + +## Discovery from the agent side + +1. **`fmt_list_client_tools_and_resources`** — enumerate app-registered tools and resources. +2. **`fmt_client_tool`** — invoke a tool by name with JSON args (CLI: `flutter-mcp-toolkit exec --name fmt_client_tool --args '...'` per your transport). +3. **`fmt_client_resource`** — fetch a registered resource (URI from listing / `resourceUri` convention). + +If something should appear but does not: confirm **`addEntries`** completed (**`await`**), then hot **restart** — reload does not always replay discovery cleanly. + +## Lifecycle gotchas + +- **Hot reload** + **`addEntries`** from widget code → duplicate registrations. Register once in **`main()` / bootstrap**, not in **`build`**. +- **Hot restart** clears VM state; registrations tied to **`bootstrapFlutter`** / **`main`** run again on boot — correct pattern survives restart. +- **Debug mode only** — release builds do not expose these VM service extensions. +- **Naming**: flat global namespace per app — prefix tools/resources (`cart_`, `flags_`, `nav_`) to avoid collisions with builtins or other domains. + +## When the agent authors surfaces for the user’s app + +1. Ensure **`mcp_toolkit`** is in **`pubspec.yaml`**. +2. Add **`lib/mcp_tools/_surfaces.dart`** exporting **`registerXSurfaces()`** that returns **`Set`** or performs **`addEntries`** once. +3. Wire **`registerXSurfaces()`** from **`bootstrapFlutter(..., additionalEntries: ...)`** or call **`addEntries`** immediately after **`initializeFlutterToolkit`** inside **`bootstrapFlutter`**’s chain — **never** from **`StatefulWidget` lifecycle**. +4. Tight schemas (**`additionalProperties: false`**, explicit **`required`**). +5. Hot **restart**, then **`fmt_list_client_tools_and_resources`** before first **`fmt_client_tool`** / **`fmt_client_resource`** call. + +## Safety and scope + +- Treat handlers as **powerful debug hooks**: avoid exposing secrets, full databases, or unchecked filesystem/network IO. +- Keep handlers thin: delegate to domain/services already used by the app (same DI/getters), **don’t** duplicate business logic in MCP-only paths unless intentional. + +## Common traps + +- **`request.arguments`** — wrong shape; use **`request['key']`** on **`Map`**. +- Missing **`await`** on **`addEntries`** → race before discovery lists your surface. +- Returning **`Future`** instances inside **`parameters`** → useless serialization; **`await`** inside the handler. +- **`inputSchema`** out of sync with the handler → agents trust the schema; update both. + +## Related + +- Driving the live app (snapshot / tap / reload): **`flutter-mcp-toolkit-guide`** → **`flutter-mcp-toolkit-inspect`** / **`flutter-mcp-toolkit-control`**. +- Repository **`ARCHITECTURE.md`** → “Dynamic Registry Architecture”. diff --git a/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-debug/SKILL.md b/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-debug/SKILL.md new file mode 100644 index 0000000..0922a84 --- /dev/null +++ b/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-debug/SKILL.md @@ -0,0 +1,386 @@ +--- +name: flutter-mcp-toolkit-debug +description: Diagnose problems in a running Flutter app — read logs, evaluate Dart expressions, interpret error envelopes. Use when something broke. +--- +> Calls in this skill run via the `flutter-mcp-toolkit` CLI binary: +> flutter-mcp-toolkit exec --name --args '' +> Output is JSON on stdout. Errors come as `{"error":{"code":..., "message":..., "recovery":...}}`. +> Throughout this skill, calls are written as `tap_widget(selector: "...")` — translate to the CLI form. +> If the binary isn't on PATH, see `flutter-mcp-toolkit-setup`. + +## When to use + +Use this skill when something broke and you need to understand why: + +- A tool call returned an error envelope (`ok: false`). +- The app behaves unexpectedly and you need runtime log output. +- You need to inspect live app state without changing it (read `AgentState.instance.value`). +- A prior `control` action (tap, hot_reload, navigate) completed without error but the result is wrong. + +Do NOT use this skill for: + +- Reading what is currently on screen — use `flutter-mcp-toolkit-inspect`. +- The toolkit itself failing to connect — load `flutter-mcp-toolkit-setup`. + +## Triage flow + +1. **Error envelope returned?** Read `error.code` first, then `error.descriptor.retryable`. Look the code up in the Error envelope playbook below. +2. **Retryable error?** Run `flutter-mcp-toolkit doctor --json`. If doctor fails, load `flutter-mcp-toolkit-setup`. +3. **Need log output?** Call `get_recent_logs` with `count: 100` and a level filter. Look for stack traces or assertion messages near the timestamp of the failure. +4. **Need live state?** Call `evaluate_dart_expression` with a targeted expression (e.g. `MyBloc.instance.state.toString()`). Do this after logs, not instead of them. +5. **Chaining with inspect?** Order: `semantic_snapshot` → `evaluate_dart_expression` → `get_recent_logs`. Snapshot gives you the current widget tree before expression evaluation mutates nothing; logs give trailing context. +6. **Multiple targets?** If `connection_selection_required`, call `discover_debug_apps`, pick the `targetId`, then pass `connection: {targetId: "..."}` to every subsequent call. + +## Tool reference + +### get_recent_logs + +Retrieve recent `print()` and `debugPrint()` output from the running app's main isolate. + +- `count` • integer • optional, default: 50 — number of log lines to return. +- `connection` • object • optional — connection override; required when multiple debug apps are running. + +``` +get_recent_logs(count: 100) +get_recent_logs(count: 50, connection: {targetId: "ws://127.0.0.1:8181//ws"}) +``` + +Returns: `{"logs": ["[INFO] page loaded", "[ERROR] assertion failed: ..."], "count": 50}` + +Read-only; no code executed. Returns only lines buffered since last app start or hot restart. On failure see `getRecentLogsFailed` in the playbook. + +### evaluate_dart_expression + +Evaluate a Dart expression in the running app's main isolate and return its string representation. + +- `expression` • string • **required** — Dart expression (e.g. `"MyClass.instance.counter"`). +- `connection` • object • optional — connection override. + +``` +evaluate_dart_expression(expression: "Navigator.of(context).canPop()") +evaluate_dart_expression(expression: "AgentState.instance.value.toString()") +``` + +Returns: `{"result": "42"}` — always a string-serialized value. Executes arbitrary code in the live isolate — avoid side-effecting expressions. Debug mode only. On failure see `evaluateExpressionFailed` in the playbook. + +## Connect / multi-app flows + +When `discover_debug_apps` returns more than one entry (or any call returns `connection_selection_required`): + +1. Call `discover_debug_apps()` — read `targets[*].targetId` for each running app. +2. Identify the target by port or hostname. +3. Call `connect_debug_app(connection: {targetId: "ws://127.0.0.1://ws"})` to pin the session. +4. Pass the same `connection` object to every subsequent tool call for the session. + +Connection override pattern — pass `connection` on any call: + +``` +get_recent_logs(count: 50, connection: {targetId: "ws://127.0.0.1:8182//ws"}) +evaluate_dart_expression(expression: "x.toString()", connection: {targetId: "ws://127.0.0.1:8182//ws"}) +``` + +If the target changes (app restarted, port shifted), re-run `discover_debug_apps` to get the new `targetId`. Stale URIs return `connect_failed`. + +## Error envelope playbook + +Every failure returns `{code, message, details, descriptor, recovery}`. Always read `error.descriptor` (not the top-level envelope) for `retryable` and `exitCode`. Run `error.recovery.fix_command` directly when provided. + +### `unexpectedExecutorError` (`unexpected_executor_error`) + +**Means:** unhandled exception in the command executor. +**Causes:** bug in the server; unexpected nil; unrecoverable VM state. +**Recovery:** + +1. `flutter-mcp-toolkit doctor --json` +2. If doctor passes, retry once; if it recurs, file a bug with `error.details`. + +### `connectFailed` (`connect_failed`) + +**Means:** connection to the VM Service WebSocket failed. +**Causes:** wrong port, stale token, app not running. +**Recovery:** + +1. `flutter-mcp-toolkit exec --name get_vm --args '{"connection":{"uri":"ws://127.0.0.1:8181//ws"}}'` +2. Get the exact URI from `app.debugPort.wsUri` in Flutter output. + +### `vmNotConnected` (`vm_not_connected`) + +**Means:** a VM-dependent command was called before a connection was established. +**Recovery:** + +1. `flutter-mcp-toolkit exec --name status --args '{}'` +2. Then `flutter-mcp-toolkit doctor --json`. + +### `connectionSelectionRequired` (`connection_selection_required`) + +**Means:** multiple debug targets exist; an explicit target is required. +**Causes:** more than one Flutter app running in debug mode simultaneously. +**Recovery:** + +1. `flutter-mcp-toolkit exec --name discover_debug_apps --args '{}'` +2. Pick the correct `targetId` from `details.availableTargets`. +3. Retry the original call with `connection: {targetId: ""}`. + +### `discoverDebugAppsFailed` (`discover_debug_apps_failed`) + +**Means:** discovery scan of local VM Service ports failed. +**Recovery:** `flutter-mcp-toolkit doctor --json` + +### `getVmFailed` (`get_vm_failed`) + +**Means:** `get_vm` RPC to the VM Service failed. +**Recovery:** `flutter-mcp-toolkit doctor --json` + +### `getExtensionRpcsFailed` (`get_extension_rpcs_failed`) + +**Means:** listing registered extension RPCs failed. +**Recovery:** `flutter-mcp-toolkit doctor --json` + +### `hotReloadFailed` (`hot_reload_failed`) + +**Means:** hot reload was rejected by the Dart compiler or VM. +**Causes:** compile error in changed files; isolate in bad state. +**Recovery:** + +1. `flutter-mcp-toolkit exec --name get_app_errors --args '{}'` +2. Fix the compile error, then retry. + +### `hotRestartFailed` (`hot_restart_failed`) + +**Means:** full hot restart failed. +**Recovery:** `flutter-mcp-toolkit doctor --json` — if VM is unreachable, restart the app manually. + +### `getActivePortsFailed` (`get_active_ports_failed`) + +**Means:** scan for active debug ports failed. +**Recovery:** `flutter-mcp-toolkit doctor --json` + +### `getAppErrorsFailed` (`get_app_errors_failed`) + +**Means:** retrieving app errors from the toolkit bridge failed. +**Recovery:** `flutter-mcp-toolkit doctor --json` + +### `getScreenshotsFailed` (`get_screenshots_failed`) + +**Means:** screenshot capture failed (wrong mode, host window not available, Simulator window race, etc.). +**Recovery:** + +1. `flutter-mcp-toolkit doctor --json` — check `visual_capture_permission_denied` separately. +2. Check `captureHints` on `get_view_details` or screenshot payloads. Strong signals (`platformViewsDetected`): `UiKitView`, `AppKitView`, `AndroidView`, `HtmlElementView` — use `desktop_window` or `auto` (macOS app, iOS Simulator, or Chrome/web on any host with CDP). Web truth capture: macOS ScreenCaptureKit first, then Chrome CDP (`captureBackend: cdp`); override port with `--web-browser-debugging-port`. Weak signals (`weakSignalsDetected`): `Texture` only — prefer `desktop_window` on macOS host but `auto` does not upgrade. WGPU/custom engines without platform views: set `MCPToolkitBinding.captureHintsContributor` in the app. Image-only `get_screenshots` tools also return routing JSON in `meta` and a leading text block. Showcase: `make showcase-stop` then `make showcase` (macOS `AppKitView`) or `flutter run -d chrome` (web `HtmlElementView` + CDP capture). +3. Run `focus_window` (MCP: `fmt_focus_window`) then retry `get_screenshots` with `mode: desktop_window`. +4. For **`validate-runtime`**, executor recovery retries focus+capture once (`desktopCaptureRetried`). When platform views are detected, validate-runtime does not fall back to `flutter_layer`. Read `capturePlatformViewsDetected`, `captureFocusAttempted`, and `captureFallbackUsed` in `data.summary`. + +### `visualCapturePermissionDenied` (`visual_capture_permission_denied`) + +**Means:** macOS Screen Recording permission is not granted. +**Recovery:** + +1. `flutter-mcp-toolkit permissions request --kind visual_capture` +2. Or open System Settings → Privacy & Security → Screen Recording. + +### `visualCaptureUnsupported` (`visual_capture_unsupported`) + +**Means:** visual capture is not supported on this platform or capture mode. Not retryable. +**Recovery:** `flutter-mcp-toolkit permissions status && flutter-mcp-toolkit doctor --json` + +### `getViewDetailsFailed` (`get_view_details_failed`) + +**Means:** retrieving FlutterView dimensions failed. +**Recovery:** `flutter-mcp-toolkit doctor --json` + +### `debugDumpFailed` (`debug_dump_failed`) + +**Means:** a VM debug-dump RPC (render tree, semantics, layers) failed. +**Recovery:** `flutter-mcp-toolkit doctor --json` + +### `dynamicRegistryDisabled` (`dynamic_registry_disabled`) + +**Means:** a dynamic tool/resource call was made but the dynamic registry is disabled. Not retryable. +**Recovery:** `flutter-mcp-toolkit --dynamics exec --name status --args '{}'` — pass `--dynamics` flag to enable. + +### `dynamicRegistryListFailed` (`dynamic_registry_list_failed`) + +**Means:** listing dynamic tools/resources from the registry failed. +**Recovery:** `flutter-mcp-toolkit doctor --json` + +### `missingToolName` (`missing_tool_name`) + +**Means:** a dynamic tool call was made without providing a tool name. Not retryable. +**Recovery:** include `tool_name` parameter in the call; `flutter-mcp-toolkit schema --name fmt_client_tool`. + +### `dynamicToolFailed` (`dynamic_tool_failed`) + +**Means:** invocation of a dynamic tool failed. +**Recovery:** `flutter-mcp-toolkit doctor --json` + +### `missingResourceUri` (`missing_resource_uri`) + +**Means:** a dynamic resource read was called without a URI. Not retryable. +**Recovery:** include `uri` parameter; `flutter-mcp-toolkit schema --name fmt_client_resource`. + +### `dynamicResourceFailed` (`dynamic_resource_failed`) + +**Means:** reading a dynamic resource failed. +**Recovery:** `flutter-mcp-toolkit doctor --json` + +### `sessionManagerNotConfigured` (`session_manager_not_configured`) + +**Means:** a session command was called but no session manager is wired. Not retryable. +**Recovery:** `flutter-mcp-toolkit doctor --json` — server config issue; reload `flutter-mcp-toolkit-setup`. + +### `sessionNotFound` (`session_not_found`) + +**Means:** the requested session ID does not exist. Not retryable. +**Recovery:** list active sessions; start a new session before referencing it. + +### `invalidCommand` (`invalid_command`) + +**Means:** command name or argument schema is invalid. Not retryable. +**Recovery:** `flutter-mcp-toolkit schema --name ` + +### `stateStoreReadFailed` (`state_store_read_failed`) + +**Means:** reading from persistent state store failed. +**Recovery:** `flutter-mcp-toolkit doctor --json` — check filesystem permissions on state directory. + +### `stateStoreWriteFailed` (`state_store_write_failed`) + +**Means:** writing to persistent state store failed. +**Recovery:** `flutter-mcp-toolkit doctor --json` — check disk space and permissions. + +### `stateLockTimeout` (`state_lock_timeout`) + +**Means:** acquiring the state lock timed out (concurrent agent contention). +**Causes:** another agent or CLI call holds the lock; deadlock. +**Recovery:** wait and retry; if recurring, kill other agent processes holding the lock. + +### `stateLockConflict` (`state_lock_conflict`) + +**Means:** a conflicting state lock was detected. +**Causes:** parallel agents writing simultaneously. +**Recovery:** serialise calls; retry after the conflicting operation completes. + +### `diagnoseFailed` (`diagnose_failed`) + +**Means:** the composite `diagnose` command failed. +**Recovery:** `flutter-mcp-toolkit doctor --json` + +### `explainErrorsFailed` (`explain_errors_failed`) + +**Means:** the error-explanation command failed. +**Recovery:** `flutter-mcp-toolkit doctor --json` + +### `unsupportedSummaryProvider` (`unsupported_summary_provider`) + +**Means:** an unrecognised summary provider was requested. Not retryable. +**Recovery:** `flutter-mcp-toolkit schema --name diagnose` — check allowed `provider` values. + +### `snapshotNotFound` (`snapshot_not_found`) + +**Means:** the referenced snapshot ID does not exist. Not retryable. +**Recovery:** `flutter-mcp-toolkit snapshot create --name --args '{}'` + +### `snapshotInvalid` (`snapshot_invalid`) + +**Means:** snapshot payload is malformed or fails validation. Not retryable. +**Recovery:** recreate the snapshot; do not reuse corrupted files. + +### `staleSnapshot` (`stale_snapshot`) + +**Means:** the provided `snapshotId` no longer matches the current app state. +**Causes:** a hot reload or interaction changed the widget tree after the snapshot was taken. +**Recovery:** + +1. `evaluate_dart_expression(expression: "true")` — verify app is reachable. +2. `semantic_snapshot()` — obtain a fresh snapshot ID, then retry the original call. + +### `bundleBuildFailed` (`bundle_build_failed`) + +**Means:** bundle creation or publish failed. +**Recovery:** `flutter-mcp-toolkit doctor --json` — check build artefacts and output path. + +### `writeBlocked` (`write_blocked`) + +**Means:** a write was blocked because `--no-overwrite` is set and the target already exists. Not retryable. +**Recovery:** retry without `--no-overwrite`, or choose a different `--output`/`--name`. + +### `doctorCriticalFailed` (`doctor_critical_failed`) + +**Means:** one or more critical doctor checks failed. +**Recovery:** `flutter-mcp-toolkit doctor --json` — read `checks[*]` where `status: "fail"` and `critical: true`; load `flutter-mcp-toolkit-setup`. + +### `interactionFailed` (`interaction_failed`) + +**Means:** a tap/scroll/swipe/drag/long_press/enter_text call failed. +**Causes:** stale `ref`; widget not visible or not interactive; toolkit bridge not initialized. +**Recovery:** + +1. `semantic_snapshot()` — get fresh refs. +2. Retry with the new ref. + +### `semanticSnapshotFailed` (`semantic_snapshot_failed`) + +**Means:** `semantic_snapshot` execution failed. +**Recovery:** `flutter-mcp-toolkit doctor --json` — verify `MCPToolkitBinding.initialize()` is called. + +### `evaluateExpressionFailed` (`evaluate_expression_failed`) + +**Means:** `evaluate_dart_expression` execution failed. +**Causes:** expression syntax error; exception thrown at runtime; isolate not reachable. +**Recovery:** simplify the expression; check syntax; `flutter-mcp-toolkit doctor --json`. + +### `getRecentLogsFailed` (`get_recent_logs_failed`) + +**Means:** `get_recent_logs` retrieval failed. +**Recovery:** `flutter-mcp-toolkit doctor --json` — verify toolkit is initialized. + +### `waitTimeout` (`wait_timeout`) + +**Means:** `wait_for` predicate did not match before `timeoutMs` elapsed. +**Causes:** predicate condition never becomes true; app state does not change; `timeoutMs` too short. +**Recovery:** + +1. `semantic_snapshot()` — verify the expected widget state. +2. Increase `timeoutMs` or adjust the predicate. + +### `waitForFailed` (`wait_for_failed`) + +**Means:** `wait_for` execution failed (malformed predicate or toolkit error). +**Recovery:** `flutter-mcp-toolkit schema --name wait_for` + +### `pressKeyFailed` (`press_key_failed`) + +**Means:** `press_key` execution failed. +**Recovery:** `flutter-mcp-toolkit schema --name press_key` + +### `handleDialogFailed` (`handle_dialog_failed`) + +**Means:** `handle_dialog` (dismiss/accept dialog) execution failed. +**Causes:** no dialog present; dialog already dismissed. +**Recovery:** `semantic_snapshot()` — verify a dialog is visible before calling. + +### `navigateFailed` (`navigate_failed`) + +**Means:** `navigate` push/pop/popUntil failed. +**Recovery:** `flutter-mcp-toolkit schema --name navigate` + +### `navigatorNotRegistered` (`navigator_not_registered`) + +**Means:** `navigate` was called but the app did not register a `GlobalKey`. Not retryable. +**Causes:** `MCPToolkitBinding.instance.navigatorKey` was never set in the host app. +**Recovery:** assign `MCPToolkitBinding.instance.navigatorKey = navigatorKey` in the app's `main.dart` and hot restart. + +### `fillFormFailed` (`fill_form_failed`) + +**Means:** `fill_form` orchestration failed (transport or per-field error). +**Recovery:** `flutter-mcp-toolkit schema --name fill_form` + +### `hoverFailed` (`hover_failed`) + +**Means:** `hover` execution failed. +**Recovery:** `flutter-mcp-toolkit doctor --json` + +### `unknown` (`unknown_error`) + +**Means:** fallback for any unrecognised error code. +**Recovery:** `flutter-mcp-toolkit doctor --json` — inspect `error.details` for raw cause. diff --git a/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-guide/SKILL.md b/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-guide/SKILL.md new file mode 100644 index 0000000..2e49481 --- /dev/null +++ b/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-guide/SKILL.md @@ -0,0 +1,79 @@ +--- +name: flutter-mcp-toolkit-guide +description: Entry point for inspecting or driving a running Flutter app from your AI assistant — routes to the right task skill (inspect / control / debug / custom app surfaces) and runs preflight. +--- +> Calls in this skill run via the `flutter-mcp-toolkit` CLI binary: +> flutter-mcp-toolkit exec --name --args '' +> Output is JSON on stdout. Errors come as `{"error":{"code":..., "message":..., "recovery":...}}`. +> Throughout this skill, calls are written as `tap_widget(selector: "...")` — translate to the CLI form. +> If the binary isn't on PATH, see `flutter-mcp-toolkit-setup`. + +## When to use + +Use this skill when the user wants to inspect or drive a running Flutter app +from this conversation. Examples: +- "Tap the login button in my app" +- "Why is the home screen blank?" +- "Take a screenshot and tell me what's broken" +- "Expose my cart / flags / internal state to the agent via MCP" + +If the user is asking about Flutter concepts unrelated to a running app +(architecture questions, package selection), this skill does not apply. + +## Step 1: Preflight + +Always run `flutter-mcp-toolkit doctor --json` first. Parse the output: + +- `status: "ok"` — proceed to Step 2. +- `status: "error"` and `error.code: "binary_not_found"` — load + `flutter-mcp-toolkit-setup` and follow its install instructions. +- `status: "error"` and `error.code: "vm_not_connected"` — load + `flutter-mcp-toolkit-setup` and follow its troubleshooting section. +- Any other error — load `flutter-mcp-toolkit-debug` and read the error + envelope playbook. + +## Step 2: Pick the right skill for the user's intent + +| User intent | Load skill | +|---|---| +| Read state ("what's on screen?", "show me errors", "screenshot") | `flutter-mcp-toolkit-inspect` | +| Drive UI ("tap X", "type into Y", "scroll to Z", "hot reload") | `flutter-mcp-toolkit-control` | +| Diagnose ("why is X failing?", "show recent logs", "evaluate expression") | `flutter-mcp-toolkit-debug` | +| Register app-specific MCP tools/resources (`MCPCallEntry`, `bootstrapFlutter` `additionalEntries`) | `flutter-mcp-toolkit-custom-tools` | +| Harness Script lint/run/Maestro (`*.hs.yaml`, app registry) | Install **flutter_harness** — skill `flutter-mcp-semantic-test` in that repo | +| HS capture bundles / promo video | **flutter_harness** + **flutter_mcp_video** (not bundled in toolkit `init`) | + +If the task spans more than one (e.g. "tap the button and show me what +changed"), load `inspect` AND `control`. Skills are additive. + +## Step 3: Execute + +Each task skill has the tool list, parameter shapes, and example calls. Follow +the prelude at the top of the skill — it tells you whether you're calling MCP +tools or shelling out to the CLI. + +## Tool taxonomy reference + +The core toolkit tools fall into these categories. The full list with +parameter shapes lives in the task skills. + +- **Inspection (read-only):** `discover_debug_apps`, `get_app_errors`, + `get_screenshots`, `get_view_details`, `get_vm`, `get_extension_rpcs`, + `semantic_snapshot`, `inspect_widget_at_point`, `capture_ui_snapshot`, + `connect_debug_app`. → `flutter-mcp-toolkit-inspect`. +- **Interaction (mutating):** `tap_widget`, `long_press`, `enter_text`, + `fill_form`, `scroll`, `swipe`, `drag`, `hover`, `press_key`, `wait_for`, + `navigate`, `handle_dialog`, `hot_reload_flutter`, `hot_restart_flutter`, + `hot_reload_and_capture`. → `flutter-mcp-toolkit-control`. +- **Debug:** `get_recent_logs`, `evaluate_dart_expression`. → + `flutter-mcp-toolkit-debug`. +- **Dynamic registry (app-defined):** after registration in the Flutter app, + list with `list_client_tools_and_resources`, then `client_tool` / + `client_resource` — wire names as **`fmt_*`** when calling MCP. → + `flutter-mcp-toolkit-custom-tools`. + +## When in doubt + +If `doctor` is green but a tool call fails, read the returned `error.code` +and `error.recovery` fields. The full code → recovery table is in +`flutter-mcp-toolkit-debug`. diff --git a/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-inspect/SKILL.md b/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-inspect/SKILL.md new file mode 100644 index 0000000..f4aa90d --- /dev/null +++ b/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-inspect/SKILL.md @@ -0,0 +1,228 @@ +--- +name: flutter-mcp-toolkit-inspect +description: Read state from a running Flutter app — semantic snapshot, view details, errors, screenshots, VM info. Use when you need to understand what the app is showing. +--- +> Calls in this skill run via the `flutter-mcp-toolkit` CLI binary: +> flutter-mcp-toolkit exec --name --args '' +> Output is JSON on stdout. Errors come as `{"error":{"code":..., "message":..., "recovery":...}}`. +> Throughout this skill, calls are written as `tap_widget(selector: "...")` — translate to the CLI form. +> If the binary isn't on PATH, see `flutter-mcp-toolkit-setup`. + +## When to use + +Use this skill for read-only state inspection of a running Flutter app: what is shown on screen, recent errors, available debug targets, VM metadata, and widget tree details. Do not use for driving interaction — that is the interact skill. Start with `discover_debug_apps` when no connection target is established. Start with `semantic_snapshot` when you need to know which widgets are on screen. + +## Recipes + +### Fast inspect cycle (prefer `batch`) + +```bash +flutter-mcp-toolkit batch --steps '[ + {"name":"semantic_snapshot"}, + {"name":"get_app_errors","args":{"count":5}}, + {"name":"get_screenshots","args":{"screenshotMode":"flutter_layer","compress":true}} +]' +``` + +Use `screenshotMode: flutter_layer` on macOS to avoid Screen Recording permission failures. + +### Snapshot the visible UI + +1. Call `semantic_snapshot()`. +2. Read `interactionSurface`: `flutter_widgets` (tap-by-ref works), `hybrid` (sparse semantics), `game_canvas` (use `evaluate_dart_expression` + screenshots). +3. Each interactive node has a stable `ref` (`s_0`, `s_1`, …) and the response includes a `snapshot_id`. +4. Pass refs to interaction tools; pass `snapshot_id` to detect staleness. + +### After a code edit + +Prefer `hot_reload_and_capture` over separate reload + snapshot + screenshot calls. + +### Find an error by message + +1. Call `get_app_errors(count: 10)`. +2. Inspect the `errors` array — each entry has message, stack trace, and timestamp. +3. Match on message text to find the source. + +### List debug-mode apps + +1. Call `discover_debug_apps()`. +2. Read the `targetId` (canonical WebSocket URI) for each active target. +3. Pass the chosen URI as `connection.targetId` on subsequent tool calls. + +### Get widget at coordinates + +1. Call `inspect_widget_at_point(x: 200, y: 400)`. +2. The response identifies the deepest widget and render node at those global logical pixel coordinates. + +### Save a screenshot to a file + +1. Call `get_screenshots()`. +2. If `meta.fileUrls` is non-empty, screenshots are on disk at those paths. Otherwise the response contains base64 `ImageContent` blocks — extract and write manually. +3. To force file output, configure an images output directory on the server before calling. + +## Tool reference + +### discover_debug_apps + +List all active Flutter debug targets with canonical WebSocket URIs. + +- `connection` (object, optional) — accepted by schema, ignored by executor; discovery is always local. + +``` +discover_debug_apps() +``` + +Returns: `{"targets": [{"targetId": "ws://127.0.0.1:8181//ws", "host": "...", "port": 8181}]}` + +- `vm_service_unavailable` — no debug-mode Flutter process found. +- `tool_not_found` — binary predates v3.0.0; run `make build`. + +### get_app_errors + +Retrieve the most recent application errors from the Dart VM. + +- `count` (integer, optional, default: 4) — number of errors to return. +- `connection` (object, optional) — connection override. + +``` +get_app_errors(count: 5) +``` + +Returns: `{"message": "2 errors found", "errors": [{"message": "...", "stack": "..."}]}` + +- `vm_service_unavailable` — app not reachable. +- `connection_selection_required` — multiple targets; supply `connection.targetId`. + +### get_screenshots + +Capture screenshots of all views. + +- `compress` (boolean, optional, default: true) — compress PNG output. +- `mode` (string, optional, default: `auto`) — `auto`, `flutter_layer`, or `desktop_window`. +- `permissionPolicy` (string, optional, default: `check_only`) — `check_only`, `auto_request_once`, or `request_always`. +- `connection` (object, optional) — connection override. + +``` +get_screenshots(mode: "flutter_layer", compress: false) +``` + +Returns: `ImageContent` blocks (base64 PNG) when no output dir configured, or `TextContent` URL refs + `meta.fileUrls` when file output is enabled. + +- `permission_denied` — retry with `permissionPolicy: "auto_request_once"`. +- `vm_service_unavailable` — app not reachable. + +### get_view_details + +Get dimensions, device pixel ratio, and display ID for all views. + +- `connection` (object, optional) — connection override. + +``` +get_view_details() +``` + +Returns: `{"views": [{"id": 0, "width": 1280, "height": 800, "devicePixelRatio": 2.0}]}` + +- `vm_service_unavailable` — app not running. +- `connection_selection_required` — multiple targets; supply `connection.targetId`. + +### get_vm + +Return Dart VM metadata: version, isolates list, pid, and architecture. + +- `connection` (object, optional) — connection override. + +``` +get_vm() +``` + +Returns: `{"type": "VM", "name": "vm", "version": "3.x.x", "isolates": [...]}` + +- `vm_service_unavailable` — app not reachable. +- `connection_selection_required` — multiple targets active. + +### get_extension_rpcs + +List all registered VM service extension RPCs in the running app. + +- `isolateId` (string, optional) — schema-declared but not read by executor; checks all isolates when omitted. +- `isRawResponse` (boolean, optional) — schema-declared but not read by executor. +- `connection` (object, optional) — connection override. + +``` +get_extension_rpcs() +``` + +Returns: `{"extensionRPCs": ["ext.flutter.inspector.getRootWidget", "ext.mcp.toolkit.semantic_snapshot"]}` + +- `vm_service_unavailable` — app not running. +- `connection_selection_required` — multiple targets. + +### semantic_snapshot + +Return a compact accessibility tree of interactive widgets with stable `ref` strings and a `snapshot_id`. + +- `connection` (object, optional) — connection override. + +``` +semantic_snapshot() +``` + +Returns: `{"snapshot_id": 3, "nodes": [{"ref": "s_0", "label": "Increment", "actions": ["tap"]}]}` + +- `vm_service_unavailable` — app not running or `MCPToolkitBinding.initialize()` not called. +- `connection_selection_required` — multiple targets; supply `connection.targetId`. + +### inspect_widget_at_point + +Identify the deepest widget and render node at a global logical coordinate. + +- `x` (integer, required) — global logical X coordinate. +- `y` (integer, required) — global logical Y coordinate. +- `viewId` (integer, optional) — FlutterView ID for multi-view apps. +- `connection` (object, optional) — connection override. + +``` +inspect_widget_at_point(x: 200, y: 400) +``` + +Returns: `{"widget": {"type": "ElevatedButton", "rect": {"left": 180, "top": 380, "right": 280, "bottom": 420}}}` + +- `vm_service_unavailable` — app not reachable. +- `invalid_argument` — coordinates out of view bounds. + +### capture_ui_snapshot + +Capture screenshots, view details, and app errors in one bundled response. + +- `errorsCount` (integer, optional, default: 4) — errors to include. +- `compress` (boolean, optional, default: true) — compress screenshots. +- `includeViewDetails` (boolean, optional, default: true) — include view data. +- `includeErrors` (boolean, optional, default: true) — include app errors. +- `screenshotMode` (string, optional, default: `auto`) — `auto`, `flutter_layer`, or `desktop_window`. +- `permissionPolicy` (string, optional, default: `check_only`) — `check_only`, `auto_request_once`, or `request_always`. +- `connection` (object, optional) — connection override. + +``` +capture_ui_snapshot(errorsCount: 2, includeViewDetails: false) +``` + +Returns: single `TextContent` JSON block with `screenshots`, `viewDetails`, and `errors` keys. + +- `vm_service_unavailable` — app not running. +- `permission_denied` — retry with `permissionPolicy: "auto_request_once"`. + +### connect_debug_app + +Explicitly select and connect to a Flutter debug VM target. Use when multiple apps are running or to pin a specific target for the session. + +- `connection` (object, optional) — pass `connection.targetId` with a WebSocket URI from `discover_debug_apps`. + +``` +connect_debug_app(connection: {targetId: "ws://127.0.0.1:8181//ws"}) +``` + +Returns: `{"connected": true, "targetId": "ws://127.0.0.1:8181//ws", "isolates": [...]}` + +- `target_not_found` — URI doesn't match a running app; re-run `discover_debug_apps` for the exact URI. +- `connection_failed` — VM refused connection; verify the app is still running in debug mode. diff --git a/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-repo-maintainer/SKILL.md b/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-repo-maintainer/SKILL.md new file mode 100644 index 0000000..943a775 --- /dev/null +++ b/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-repo-maintainer/SKILL.md @@ -0,0 +1,127 @@ +--- +name: flutter-mcp-toolkit-repo-maintainer +description: >- + Maintain mcp_flutter releases, CHANGELOG, version pins, docs, and CI. + Use when cutting a release, editing CHANGELOG.md, bumping VERSION, running + release-please, sync-skills, check-contracts, or updating install/docs for + npx skills and flutter-mcp-toolkit init. +--- +> Calls in this skill run via the `flutter-mcp-toolkit` CLI binary: +> flutter-mcp-toolkit exec --name --args '' +> Output is JSON on stdout. Errors come as `{"error":{"code":..., "message":..., "recovery":...}}`. +> Throughout this skill, calls are written as `tap_widget(selector: "...")` — translate to the CLI form. +> If the binary isn't on PATH, see `flutter-mcp-toolkit-setup`. + +# flutter-mcp-toolkit repo maintainer + +Golden path for **this repository** (not end-user Flutter apps). Prefer +release-please on `main`; use manual steps only when the Release PR path is blocked. + +## When to use + +- Cutting a release or promoting `## [Unreleased]` in CHANGELOG.md +- Adding/editing contributor docs, AI agent install docs, or plugin skills +- Verifying version sync or skill asset drift before merge +- Troubleshooting release-please, `release.yml` binaries, or `install.sh` version pins + +## Version touchpoints (must match root `VERSION`) + +| File | Field | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| [VERSION](https://github.com/Arenukvern/mcp_flutter/blob/main/VERSION) | repo pin | +| [plugin/EXPECTED_SERVER_VERSION](https://github.com/Arenukvern/mcp_flutter/blob/main/plugin/EXPECTED_SERVER_VERSION) | installer pin | +| [packages/core/lib/src/runtime_version.dart](https://github.com/Arenukvern/mcp_flutter/blob/main/packages/core/lib/src/runtime_version.dart) | `kFlutterMcpVersion` (`x-release-please-version`) | +| [packages/server_capability_core/lib/src/fmt_capability.dart](https://github.com/Arenukvern/mcp_flutter/blob/main/packages/server_capability_core/lib/src/fmt_capability.dart) | `version` getter | +| [mcp_server_dart/pubspec.yaml](https://github.com/Arenukvern/mcp_flutter/blob/main/mcp_server_dart/pubspec.yaml) | `version:` | +| [mcp_toolkit/pubspec.yaml](https://github.com/Arenukvern/mcp_flutter/blob/main/mcp_toolkit/pubspec.yaml) | `version:` | +| [plugin/.cursor-plugin/plugin.json](https://github.com/Arenukvern/mcp_flutter/blob/main/plugin/.cursor-plugin/plugin.json) | `version` | +| [plugin/.codex-plugin/plugin.json](https://github.com/Arenukvern/mcp_flutter/blob/main/plugin/.codex-plugin/plugin.json) | `version` | +| [plugin/.claude-plugin/plugin.json](https://github.com/Arenukvern/mcp_flutter/blob/main/plugin/.claude-plugin/plugin.json) | `version` | +| [.claude-plugin/marketplace.json](https://github.com/Arenukvern/mcp_flutter/blob/main/.claude-plugin/marketplace.json) | `plugins[0].version` | +| [.release-please-manifest.json](https://github.com/Arenukvern/mcp_flutter/blob/main/.release-please-manifest.json) | `"."` key | +| [mcp_server_dart/lib/src/skill_assets.g.dart](https://github.com/Arenukvern/mcp_flutter/blob/main/mcp_server_dart/lib/src/skill_assets.g.dart) | **generated** — embeds `plugin/.cursor-plugin/plugin.json`, `plugin/.codex-plugin/plugin.json`, `plugin/mcp.json`, and all `plugin/skills/*/SKILL.md` | + +After any version bump in `plugin/*-plugin/plugin.json` or edit under `plugin/skills/`: run `make sync-skills`, then `make check-contracts` (includes `check_version_sync.sh` and `check_skill_assets_drift.sh`). + +**Harness / video (separate repos):** [flutter_harness](https://github.com/Arenukvern/flutter_harness), [flutter_mcp_video](https://github.com/Arenukvern/flutter_mcp_video) — not maintained in this plugin tree. Three-repo layout: [flutter_harness/docs/RELATED_REPOS.md](https://github.com/Arenukvern/flutter_harness/blob/main/docs/RELATED_REPOS.md). + +## Changelog workflow + +1. Add user-facing notes under `## [Unreleased]` in [CHANGELOG.md](https://github.com/Arenukvern/mcp_flutter/blob/main/CHANGELOG.md) (Keep a Changelog sections: Added, Changed, Fixed, Documentation). +2. Use conventional commit titles on `main` (`feat:`, `fix:`, `docs:`) so release-please can aggregate. +3. Do **not** edit the plan file in `.cursor/plans/`. + +### Markdown lint (MD052) + +Keep a Changelog **requires** version headings like `## [3.0.1]`. Linters treat `[3.0.1]` as an undefined **reference link** (MD052: "No link definition found"). + +- **Do not remove** the file-top `` in CHANGELOG.md (release-please must keep it when prepending sections). +- In bullet text, use **backticks** for code identifiers: `` `MCPCallEntry.resourceUri` ``, not `[MCPCallEntry.resourceUri]`. +- Before merge: `bash tool/contracts/check_changelog_markdown.sh` (also in `make check-contracts`). + +## Automated release (preferred) + +```mermaid +flowchart LR + main[merge_to_main] --> rp[release-please.yml] + rp --> pr[Release_PR] + pr --> tag[vX_Y_Z_tag] + tag --> rel[release.yml_binaries] +``` + +1. Merge PRs to `main` with conventional commits. +2. Wait for **Release PR** from [release-please.yml](https://github.com/Arenukvern/mcp_flutter/blob/main/.github/workflows/release-please.yml). +3. **skill assets:** [release_pr_sync_skills.yml](https://github.com/Arenukvern/mcp_flutter/blob/main/.github/workflows/release_pr_sync_skills.yml) auto-commits `skill_assets.g.dart` on Release PRs when drift is detected; posts a checklist comment on PR open. If **skill-assets-drift** still fails, run `make sync-skills` locally and push (release-please bumps `plugin/*-plugin/plugin.json` but not the generated bundle). +4. Review VERSION, CHANGELOG, pubspecs, plugin pins in that PR → merge. +5. release-please creates `vX.Y.Z` + GitHub release notes. +6. [release.yml](https://github.com/Arenukvern/mcp_flutter/blob/main/.github/workflows/release.yml) attaches `flutter_mcp_*` tarballs (does not overwrite release body). + +Config: [release-please-config.json](https://github.com/Arenukvern/mcp_flutter/blob/main/release-please-config.json). + +## Manual release (fallback) + +Use when release-please is unavailable or you must ship from a branch: + +1. Move `## [Unreleased]` bullets into `## [X.Y.Z]` (add date), leave empty `## [Unreleased]`. +2. Bump all version touchpoints above to `X.Y.Z`. +3. Update `.release-please-manifest.json` `"."` to `X.Y.Z`. +4. `make sync-skills` (required whenever plugin manifests or skills change — release-please bumps plugin JSON but not `skill_assets.g.dart`). +5. `make check-contracts` +6. Commit: `chore: release X.Y.Z` +7. Tag: `git tag vX.Y.Z` and push tag (triggers binary workflow). + +Build artifacts locally: `make release-artifacts` or `bash tool/release/build_release_artifacts.sh --version X.Y.Z`. + +## Docs map (single sources of truth) + +| Topic | Canonical doc | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| End-user agent install | [docs/ai_agents/overview.mdx](https://github.com/Arenukvern/mcp_flutter/blob/main/docs/ai_agents/overview.mdx) | +| `npx skills` + lockfile | overview § Install via `npx skills`; [.skills.json.example](https://github.com/Arenukvern/mcp_flutter/blob/main/.skills.json.example) | +| Contributor / releases | [docs/contributing/contribution_guide.mdx](https://github.com/Arenukvern/mcp_flutter/blob/main/docs/contributing/contribution_guide.mdx) | +| Plugin layout | [plugin/README.md](https://github.com/Arenukvern/mcp_flutter/blob/main/plugin/README.md) | +| Marketplace copy SSOT | [docs/ai_agents/marketplace_copy.yaml](https://github.com/Arenukvern/mcp_flutter/blob/main/docs/ai_agents/marketplace_copy.yaml) | +| Distribution / stores | [docs/ai_agents/marketplace_distribution.mdx](https://github.com/Arenukvern/mcp_flutter/blob/main/docs/ai_agents/marketplace_distribution.mdx) | +| Store submission runbook | [docs/contributing/marketplace_submission_runbook.mdx](https://github.com/Arenukvern/mcp_flutter/blob/main/docs/contributing/marketplace_submission_runbook.mdx) | +| Skill bodies | `plugin/skills//SKILL.md` → `make sync-skills` | + +Avoid duplicating install tables in README — link to overview. + +## Skills maintenance + +- Canonical skills: `plugin/skills/` (repo root `skills/` → symlink for `npx skills`). +- New bundled skill: add `plugin/skills//SKILL.md`, append `id` to `expectedSkillIds` in [build_skill_assets.dart](https://github.com/Arenukvern/mcp_flutter/blob/main/mcp_server_dart/tool/build_skill_assets.dart), run `make sync-skills`. +- Local Cursor copy: `.cursor/skills//` may symlink to `plugin/skills//`. + +### HyperFrames promo (flutter_mcp_video repo) + +Video skill and projects live in **[flutter_mcp_video](https://github.com/Arenukvern/flutter_mcp_video)** (`skills/hyperframes-video/`, `projects/video-projects//`). Not bundled in toolkit `make sync-skills`. + +When shipping a promo there: edit the video repo; run `bash tool/check_doc_paths.sh` in that repo before merge. Toolkit repo only hosts shared brand assets under `plugin/assets/` (symlink targets for v7-weaver). + +## Pre-merge checklist + +- [ ] `make check-contracts` +- [ ] `make sync-skills` if `plugin/skills/` or `plugin/*-plugin/plugin.json` changed +- [ ] CHANGELOG `[Unreleased]` updated for user-visible changes +- [ ] No secrets in committed configs diff --git a/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-setup/SKILL.md b/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-setup/SKILL.md new file mode 100644 index 0000000..576ce4f --- /dev/null +++ b/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp-toolkit-setup/SKILL.md @@ -0,0 +1,222 @@ +--- +name: flutter-mcp-toolkit-setup +description: Verify the flutter-mcp-toolkit install, run doctor preflight, troubleshoot connection issues. Use when the toolkit isn't responding or first-time setup. +--- +> Calls in this skill run via the `flutter-mcp-toolkit` CLI binary: +> flutter-mcp-toolkit exec --name --args '' +> Output is JSON on stdout. Errors come as `{"error":{"code":..., "message":..., "recovery":...}}`. +> Throughout this skill, calls are written as `tap_widget(selector: "...")` — translate to the CLI form. +> If the binary isn't on PATH, see `flutter-mcp-toolkit-setup`. + +## When to use + +Use this skill when: + +- First-time install: `flutter-mcp-toolkit` is not yet on PATH. +- `doctor --json` returns any check with `"status": "fail"`. +- MCP server fails to connect or tools return `vm_not_connected` / `connect_failed`. +- Visual capture or toolkit-bridge commands are returning unexpected errors. + +--- + +## Verify install + +```bash +flutter-mcp-toolkit --version +``` + +Expected output: version string (e.g. `flutter-mcp-toolkit 3.0.0`). + +If you get `command not found`, the binary is not on PATH: + +```bash +# Binary is built to mcp_server_dart/build/ inside the repo +export PATH="$PATH:/path/to/mcp_flutter/mcp_server_dart/build" +# Or rebuild from source +cd /path/to/mcp_flutter && make build +``` + +Then verify with `flutter-mcp-toolkit --version`. + +--- + +## Run doctor + +Always run doctor before any VM-dependent command: + +```bash +flutter-mcp-toolkit doctor --json +``` + +Flags: `--target ` (test a specific URI), global `--vm-service-uri ` (same as `--target` when omitted on `doctor`), `--timeout-ms ` (default: 2500). + +```bash +# Global URI works for doctor (same as validate-runtime) +flutter-mcp-toolkit --vm-service-uri 'ws://127.0.0.1:8181//ws' doctor --json +``` + +Sample green output: + +```json +{ + "summary": { "criticalFailures": 0 }, + "checks": [ + { "id": "vm_target_reachable", "status": "pass", "critical": true }, + { "id": "mcp_toolkit_extensions", "status": "pass", "critical": true }, + { "id": "dynamic_registry_available", "status": "pass", "critical": false } + ] +} +``` + +**Triage:** `criticalFailures > 0` means VM/setup is blocked — not that every tool is broken. `dynamic_registry_available: pass` with `vm_target_reachable: fail` usually means a stale URI after hot restart; run `discover_debug_apps` and pass the new `targetId`. + +Read `error.descriptor` (not top-level) for retry policy and exit codes. Each check includes `fix_command` — run it directly. + +--- + +## Recover by error code + +### `binary_not_found` + +Binary missing or not on PATH. Rebuild and add to PATH: + +```bash +cd /path/to/mcp_flutter && make build +export PATH="$PATH:/path/to/mcp_flutter/mcp_server_dart/build" +``` + +### `vm_not_connected` + +Flutter app not running, stale token after restart, or URI not resolved: + +```bash +flutter-mcp-toolkit exec --name discover_debug_apps --args '{}' +flutter-mcp-toolkit exec --name status --args '{}' +flutter-mcp-toolkit doctor --json --target ws://127.0.0.1:8181//ws +``` + +After a successful auto re-attach, `meta.recovery.reattachedTo` shows the new endpoint. + +### `connect_failed` + +Wrong port, app not started, or stale token. Pass explicit URI from `app.debugPort.wsUri`: + +```bash +flutter-mcp-toolkit exec --name get_vm --args '{"connection":{"uri":"ws://127.0.0.1:8181//ws"}}' +``` + +### `connection_selection_required` + +Multiple debug targets detected. List with `discover_debug_apps`, then pass the chosen URI from `details.availableTargets` explicitly to `get_vm`. + +### `hot_reload_failed` + +Dart compilation error or VM disconnected. Check errors, fix, then retry: + +```bash +flutter-mcp-toolkit exec --name get_app_errors --args '{}' +``` + +### `visual_capture_unsupported` + +macOS screen recording permission not granted or unsupported platform: + +```bash +flutter-mcp-toolkit permissions request --kind visual_capture +``` + +--- + +## Connection issues (deeper troubleshooting) + +**Port conflicts**: VM service defaults to 8181. Override if another process holds it: + +```bash +flutter run --debug --host-vmservice-port=8182 -d macos +flutter-mcp-toolkit --dart-vm-port 8182 doctor --json +``` + +Use `flutter run --machine` and copy `app.debugPort.wsUri` when you need the exact websocket URI (recommended for `validate-runtime` and `exec`). + +**Flutter app not in debug mode**: Release/profile builds don't expose the VM service. Always use `flutter run --debug`. + +**`mcp_toolkit` not initialized**: Doctor's `mcp_toolkit_extensions` check will fail. Add before `runApp` — use `flutter-mcp-toolkit codegen-init` to generate the boilerplate (see CLI surface below). After adding, hot restart (not hot reload — binding init requires a full restart). + +**Multiple apps / wrong target**: Pass `--target` with the exact websocket URI: + +```bash +flutter-mcp-toolkit doctor --json --target ws://127.0.0.1:8181//ws +``` + +--- + +## CLI surface + +The binary is `flutter-mcp-toolkit` (built to `mcp_server_dart/build/`). + +| Subcommand | Purpose | Minimal example | +| --------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------ | +| `exec` | Run a single named command against the VM | `flutter-mcp-toolkit exec --name get_vm --args '{}'` | +| `batch` | Run multiple commands in one call | `flutter-mcp-toolkit batch --steps '[{"name":"get_vm"},{"name":"status"}]'` | +| `schema` | Print the JSON schema for a named command | `flutter-mcp-toolkit schema --name hot_reload_flutter` | +| `capabilities` | List all registered capabilities | `flutter-mcp-toolkit capabilities` | +| `serve` | Start the MCP server (stdio transport) | `flutter-mcp-toolkit serve` | +| `snapshot create` | Capture and save a named snapshot | `flutter-mcp-toolkit snapshot create --name baseline --args '{}'` | +| `snapshot diff` | Diff two snapshots | `flutter-mcp-toolkit snapshot diff --from baseline --to current` | +| `bundle create` | Package a snapshot into a publishable bundle | `flutter-mcp-toolkit bundle create --from-snapshot baseline --output ./out` | +| `doctor` | Run preflight checks (VM + toolkit + registry) | `flutter-mcp-toolkit doctor --json` | +| `permissions status` | Check a permission (e.g. visual_capture) | `flutter-mcp-toolkit permissions status --kind visual_capture` | +| `permissions request` | Request a permission | `flutter-mcp-toolkit permissions request --kind visual_capture` | +| `permissions open-settings` | Open OS settings for a permission | `flutter-mcp-toolkit permissions open-settings --kind visual_capture` | +| `validate-runtime` | End-to-end VM + toolkit + capture smoke test | `flutter-mcp-toolkit validate-runtime --target ws://127.0.0.1:8181//ws` | +| `init ` | Install skills + MCP server config for an AI agent | `flutter-mcp-toolkit init claude-code` | +| `codegen-init` | Add toolkit dependency and emit `main.dart` boilerplate | `flutter-mcp-toolkit codegen-init` | + +Global flags (before the subcommand): `--dart-vm-port `, `--dart-vm-host `, `--vm-service-uri `, `--log-level `, `--dumps`, `-h/--help`. + +**VM targeting:** Global `--vm-service-uri` applies to `doctor` and `validate-runtime` when subcommand `--target` is omitted. If both are set and differ, `--target` wins (stderr warning). + +**`validate-runtime` screenshots:** the first capture uses `auto` (often `desktop_window` on macOS). If that step fails with a retryable `get_screenshots_failed`, the CLI retries once with `flutter_layer`. On success, `data.summary.captureFallbackUsed` is `true` in the JSON envelope. + +--- + +### `init ` + +Install the flutter-mcp-toolkit skills + MCP server config for an AI agent. + +Targets: `claude-code` | `cursor` | `codex` | `cline` | `agents-skills` | `all`. + +```bash +flutter-mcp-toolkit init claude-code # install for Claude Code (project-scoped) +flutter-mcp-toolkit init cursor --scope user # install user-globally for Cursor +flutter-mcp-toolkit init all --mode cli # install for every detected agent in CLI mode +``` + +Mode auto-detects (MCP if registered, else CLI). Override with `--mode mcp|cli|auto`. + +**Alternative (skills only, open ecosystem):** `npx skills add Arenukvern/mcp_flutter -a cursor -y` installs the same `SKILL.md` bundles via [skills.sh](https://skills.sh); it does not write `mcp.json` — run `init` afterward or configure `mcpServers` manually. See [AI agent overview](https://github.com/Arenukvern/mcp_flutter/blob/main/docs/ai_agents/overview.mdx). + +--- + +### `codegen-init` + +From a Flutter project root, add `flutter_mcp_toolkit` as a dependency and emit +the boilerplate snippet for `lib/main.dart`. + +```bash +cd my-flutter-app +flutter-mcp-toolkit codegen-init # runs `flutter pub add` + prints snippet +flutter-mcp-toolkit codegen-init --no-pub-add # snippet only, skip pub add +``` + +--- + +## Reinstall / upgrade + +The install script is idempotent — re-running it replaces the binary in place: + +```bash +curl -fsSL https://raw.githubusercontent.com/Arenukvern/mcp_flutter/main/install.sh | bash +``` + +After reinstall, verify with `flutter-mcp-toolkit --version`. diff --git a/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp/SKILL.md b/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp/SKILL.md new file mode 100644 index 0000000..5fb96ed --- /dev/null +++ b/example/.claude/skills/flutter-mcp-toolkit/flutter-mcp/SKILL.md @@ -0,0 +1,91 @@ +--- +name: flutter-mcp +description: Use this skill whenever inspecting, interacting with, or live-editing a running Flutter app via the Flutter MCP toolkit server (`mcpServers` key **`flutter-mcp-toolkit`**, or legacy **`flutter-inspector`**). Covers preflight, snapshot/tap/enter/scroll loop, hot-reload validation, and error envelope parsing. +--- +> Calls in this skill run via the `flutter-mcp-toolkit` CLI binary: +> flutter-mcp-toolkit exec --name --args '' +> Output is JSON on stdout. Errors come as `{"error":{"code":..., "message":..., "recovery":...}}`. +> Throughout this skill, calls are written as `tap_widget(selector: "...")` — translate to the CLI form. +> If the binary isn't on PATH, see `flutter-mcp-toolkit-setup`. + +# Flutter MCP + +Golden path for agents driving a live Flutter app via the **`flutter-mcp-toolkit`** MCP server entry. Older configs may use the legacy **`flutter-inspector`** `mcpServers` registry id for the same binary (that id is **not** the Claude subagent **`flutter-mcp-toolkit-runtime`**). + +## When to use + +- User references a running Flutter app (debug mode) and wants to inspect, screenshot, interact with, or hot-reload it. +- User pastes a VM service URI (`ws://127.0.0.1:8181/.../ws`) or mentions port 8181. +- You need runtime proof (before/after screenshots) that a code edit took effect. + +## Preflight (always first) + +Before any VM-dependent call: + +1. Call `doctor` (or run `flutter-mcp-toolkit doctor --json`) — parses env, ports, app reachability. +2. Confirm required toolkit extensions exist on the target: + - `ext.mcp.toolkit.app_errors` + - `ext.mcp.toolkit.view_details` + - `ext.mcp.toolkit.view_screenshots` + - `ext.mcp.toolkit.inspect_widget_at_point` + +If missing, stop and report the instrumentation gap — do **not** guess: + +- Add `mcp_toolkit` to `pubspec.yaml`. +- Ensure `MCPToolkitBinding.instance..initialize()..initializeFlutterToolkit();` runs before `runApp`. +- Hot **restart** (not reload) — reload is often insufficient for extension registration. + +## Tool naming (v3.0.0+) + +All MCP tools surface under the `fmt_` capability prefix +(`fmt_tap_widget`, `fmt_hot_reload_and_capture`, etc.). The prefix is +mandatory in `tools/call`. Skill-local references below use the bare name +for readability — when invoking, prepend `fmt_`. Dynamic-registry host +tools (`fmt_list_client_tools_and_resources`, `fmt_client_tool`, +`fmt_client_resource`) use the same prefix for a single consistent surface. + +## Interaction loop (Playwright-style) + +1. `fmt_semantic_snapshot` → returns `s_0..s_N` refs + `snapshot_id`. +2. `fmt_tap_widget` / `fmt_enter_text` / `fmt_scroll` / `fmt_swipe` / `fmt_long_press` / `fmt_drag` — act on a ref. **Always pass `snapshotId`** — you get a structured `stale_snapshot` error if the tree moved, instead of a silent wrong tap. +3. `fmt_evaluate_dart_expression` — read state directly (e.g. `AgentState.instance.counter`). +4. `fmt_hot_reload_and_capture` — after a code edit, returns reload status + screenshot + fresh snapshot + errors in one response. Prefer this over manual reload + separate capture. + +## Error envelope contract + +Errors are `{code, message, details, descriptor, recovery}`. Parse `error.descriptor` (not the top-level object) for the machine-readable shape. Strict schemas default to `additionalProperties: false` — unknown params reject. + +Common codes and recovery: + +- `connection_selection_required` — retry with `arguments.connection.targetId` or exact `arguments.connection.uri` from `app.debugPort.wsUri`. +- `target_not_found` — refresh targets, then prefer exact `arguments.connection.uri`. +- `stale_snapshot` — call `fmt_semantic_snapshot` again, then retry the action with the new `snapshot_id`. +- `tool_not_found` — confirm the prefixed name (`fmt_`); v3.0.0 dropped legacy unprefixed names. +- Empty screenshot output — verify the server was not started with `--no-images`. +- Missing view resource/tool — verify the server was not started with `--no-resources`. + +## Visual QA + +- Before/after screenshots are the proof artifact for any UI claim. Capture before with `fmt_capture_ui_snapshot` (or one of the `visual://localhost/...` resources), edit, `fmt_hot_reload_and_capture`, compare. +- For each reported visual issue, attach coordinate + `fmt_inspect_widget_at_point` output. +- Map defects to source via `fmt_get_app_errors` top stack frame (`file`, `line`, `column`) when available. +- Do **not** use `fmt_debug_dump_*` unless explicitly requested (server must be started with `--dumps`; high token cost). + +## Permissions (macOS) + +Screen Recording permission belongs to the process running `flutter-mcp-toolkit` (or the MCP server host). If visual capture is denied: + +```bash +flutter-mcp-toolkit permissions status +flutter-mcp-toolkit permissions request +flutter-mcp-toolkit permissions open-settings +``` + +## Non-modifiable apps + +If the target app cannot be instrumented (third-party binary, restricted env), report flutter-mcp as unavailable for that app. Do **not** claim screenshot/layout/error inspection success. + +## Related + +- For the dynamic-tools side (registering custom MCP tools from inside the Flutter app), see the `flutter-mcp-toolkit-custom-tools` skill. +- For routing across setup / inspect / control / debug skills, see `flutter-mcp-toolkit-guide`. diff --git a/example/.claude/skills/grill-me/SKILL.md b/example/.claude/skills/grill-me/SKILL.md new file mode 100644 index 0000000..bd04394 --- /dev/null +++ b/example/.claude/skills/grill-me/SKILL.md @@ -0,0 +1,10 @@ +--- +name: grill-me +description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me". +--- + +Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. + +Ask the questions one at a time. + +If a question can be answered by exploring the codebase, explore the codebase instead. diff --git a/example/.flutter_mcp/cache/macos_helper/macos_visual_capture_-585b0c45b152b847 b/example/.flutter_mcp/cache/macos_helper/macos_visual_capture_-585b0c45b152b847 new file mode 100755 index 0000000000000000000000000000000000000000..cb05ae2e82282e175f1df7bdf695e09dfe8b5e10 GIT binary patch literal 149896 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 String { + return value.lowercased().replacingOccurrences( + of: "[^a-z0-9]+", + with: "", + options: .regularExpression + ) +} + +func matchesCandidate(_ value: String, candidates: Set) -> Bool { + let normalized = normalizeOwnerName(value) + if normalized.isEmpty { + return false + } + for candidate in candidates { + let normalizedCandidate = normalizeOwnerName(candidate) + if normalizedCandidate.isEmpty { + continue + } + if normalized == normalizedCandidate || + normalized.contains(normalizedCandidate) || + normalizedCandidate.contains(normalized) { + return true + } + } + return false +} + +func matchesOwner(_ owner: String, candidates: Set) -> Bool { + if candidates.isEmpty { + return true + } + return matchesCandidate(owner, candidates: candidates) +} + +func matchesWindow( + _ window: SCWindow, + candidates: Set, + expectedPid: Int32? +) -> Bool { + if let expectedPid { + return window.owningApplication?.processID == expectedPid + } + if candidates.isEmpty { + return true + } + let owner = window.owningApplication?.applicationName ?? "" + let title = window.title ?? "" + return matchesCandidate(owner, candidates: candidates) || + matchesCandidate(title, candidates: candidates) +} + +func eligibleWindows( + _ windows: [SCWindow], + candidates: Set, + expectedPid: Int32? +) -> [SCWindow] { + return windows.filter { window in + let frame = window.frame + return matchesWindow( + window, + candidates: candidates, + expectedPid: expectedPid + ) && + frame.width > 8 && + frame.height > 8 + } +} + +func largestWindow(_ windows: [SCWindow]) -> SCWindow? { + return windows.max { lhs, rhs in + lhs.frame.width * lhs.frame.height < rhs.frame.width * rhs.frame.height + } +} + +func windowSummary(_ window: SCWindow) -> [String: Any] { + [ + "owner": window.owningApplication?.applicationName ?? "", + "ownerPid": Int(window.owningApplication?.processID ?? 0), + "title": window.title ?? "", + "width": window.frame.size.width, + "height": window.frame.size.height, + "windowId": Int(window.windowID), + ] +} + +func permissionStatusPayload(_ status: String, message: String) { + emit([ + "ok": true, + "status": status, + "message": message, + "canRequest": true, + "canOpenSettings": true, + "details": [:], + ]) +} + +func activateSimulatorApp() { + let source = """ + tell application "Simulator" to activate + """ + if let script = NSAppleScript(source: source) { + var error: NSDictionary? + _ = script.executeAndReturnError(&error) + } +} + +func activateChromeApp() { + let source = """ + tell application "Google Chrome" to activate + """ + if let script = NSAppleScript(source: source) { + var error: NSDictionary? + _ = script.executeAndReturnError(&error) + } +} + +func isChromeCandidateSet(_ candidates: Set) -> Bool { + for candidate in candidates { + let normalized = normalizeOwnerName(candidate) + if normalized.contains("chrome") || normalized.contains("chromium") { + return true + } + } + return false +} + +@MainActor +func focusWindow(candidates: Set, expectedPid: Int32?) async { + _ = NSApplication.shared + + if let expectedPid { + if let app = NSRunningApplication(processIdentifier: expectedPid) { + _ = app.activate(options: [.activateAllWindows, .activateIgnoringOtherApps]) + let owner = app.localizedName ?? "" + if owner.lowercased().contains("simulator") { + activateSimulatorApp() + } else if owner.lowercased().contains("chrome") || + owner.lowercased().contains("chromium") { + activateChromeApp() + } + emit([ + "ok": true, + "activatedPid": Int(expectedPid), + "owner": owner, + ]) + return + } + } + + if candidates.contains("simulator") || candidates.isEmpty { + activateSimulatorApp() + emit([ + "ok": true, + "activated": "simulator", + ]) + return + } + + if isChromeCandidateSet(candidates) { + activateChromeApp() + emit([ + "ok": true, + "activated": "chrome", + ]) + return + } + + emit([ + "ok": false, + "error": "focus_target_not_found", + "details": ["candidates": Array(candidates).sorted()], + ]) +} + +func openSettings() { + guard let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture") else { + emit([ + "ok": false, + "error": "settings_url_invalid", + ]) + return + } + NSWorkspace.shared.open(url) + emit([ + "ok": true, + "status": "denied", + "message": "Opened Screen Recording settings.", + "canRequest": true, + "canOpenSettings": true, + "details": ["opened": true], + ]) +} + +@MainActor +func capture(candidates: Set, expectedPid: Int32?) async { + _ = NSApplication.shared + + guard CGPreflightScreenCaptureAccess() else { + emit([ + "ok": false, + "error": "screen_recording_not_granted", + "permissionStatus": "not_determined", + "details": [ + "action": "request_permission_or_open_settings" + ], + ]) + return + } + + do { + let onScreenContent = try await SCShareableContent.excludingDesktopWindows( + false, + onScreenWindowsOnly: true + ) + + let onScreenMatches = eligibleWindows( + onScreenContent.windows, + candidates: candidates, + expectedPid: expectedPid + ) + var selectionSource = "on_screen" + var captureVisibility = "on_screen" + var selectedWindow = largestWindow(onScreenMatches) + var allWindowsContent: SCShareableContent? + + if selectedWindow == nil { + let anyContent = try await SCShareableContent.excludingDesktopWindows( + false, + onScreenWindowsOnly: false + ) + allWindowsContent = anyContent + let allMatches = eligibleWindows( + anyContent.windows, + candidates: candidates, + expectedPid: expectedPid + ) + selectedWindow = largestWindow(allMatches) + if selectedWindow != nil { + selectionSource = "all_windows_fallback" + captureVisibility = "offscreen_or_hidden" + } + } + + guard let window = selectedWindow else { + let visibleOwners = Array(Set(onScreenContent.windows.compactMap { + let owner = $0.owningApplication?.applicationName ?? "" + return owner.isEmpty ? nil : owner + })).sorted() + let visibleWindows = onScreenContent.windows.prefix(20).map(windowSummary) + let allOwners = Array(Set((allWindowsContent?.windows ?? []).compactMap { + let owner = $0.owningApplication?.applicationName ?? "" + return owner.isEmpty ? nil : owner + })).sorted() + let allCandidateWindows = eligibleWindows( + allWindowsContent?.windows ?? [], + candidates: candidates, + expectedPid: expectedPid + ).prefix(20).map(windowSummary) + var details: [String: Any] = [ + "candidates": Array(candidates).sorted(), + "visibleOwners": visibleOwners, + "visibleWindows": Array(visibleWindows), + "allOwners": allOwners, + "allCandidateWindows": Array(allCandidateWindows), + ] + if let expectedPid { + details["expectedPid"] = Int(expectedPid) + } + emit([ + "ok": false, + "error": "window_not_found", + "details": details, + ]) + return + } + + let filter = SCContentFilter(desktopIndependentWindow: window) + let configuration = SCStreamConfiguration() + configuration.showsCursor = false + configuration.ignoreShadowsSingleWindow = false + let pointPixelScale = CGFloat(filter.pointPixelScale) + configuration.width = Int(filter.contentRect.width * pointPixelScale) + configuration.height = Int(filter.contentRect.height * pointPixelScale) + + let image = try await SCScreenshotManager.captureImage( + contentFilter: filter, + configuration: configuration + ) + let bitmap = NSBitmapImageRep(cgImage: image) + guard let pngData = bitmap.representation(using: .png, properties: [:]) else { + emit([ + "ok": false, + "error": "png_encoding_failed", + "details": ["windowId": window.windowID], + ]) + return + } + + emit([ + "ok": true, + "permissionStatus": "granted", + "appName": window.owningApplication?.applicationName ?? "", + "windowOwnerPid": Int(window.owningApplication?.processID ?? 0), + "windowId": Int(window.windowID), + "windowTitle": window.title ?? "", + "windowSelectionSource": selectionSource, + "windowCaptureVisibility": captureVisibility, + "windowBounds": [ + "x": window.frame.origin.x, + "y": window.frame.origin.y, + "width": window.frame.size.width, + "height": window.frame.size.height, + ], + "pngBase64": pngData.base64EncodedString(), + ]) + } catch { + emit([ + "ok": false, + "error": "capture_failed", + "details": ["message": "\(error)"], + ]) + } +} + +@main +struct VisualCaptureHelper { + static func main() async { + let args = Array(CommandLine.arguments.dropFirst()) + guard let command = args.first else { + emit([ + "ok": false, + "error": "missing_command", + ]) + return + } + + switch command { + case "status": + permissionStatusPayload( + CGPreflightScreenCaptureAccess() ? "granted" : "not_determined", + message: "macOS Screen Recording preflight completed." + ) + case "request": + let granted = CGRequestScreenCaptureAccess() + permissionStatusPayload( + granted ? "granted" : "denied", + message: granted + ? "Screen Recording permission granted." + : "Screen Recording permission not granted." + ) + case "open-settings": + openSettings() + case "focus": + var expectedPid: Int32? + var candidates = Set() + var index = 1 + while index < args.count { + let argument = args[index] + if argument == "--pid", index + 1 < args.count { + expectedPid = Int32(args[index + 1]) + index += 2 + continue + } + candidates.insert(argument.lowercased()) + index += 1 + } + await focusWindow(candidates: candidates, expectedPid: expectedPid) + case "capture": + var expectedPid: Int32? + var candidates = Set() + var index = 1 + while index < args.count { + let argument = args[index] + if argument == "--pid", index + 1 < args.count { + expectedPid = Int32(args[index + 1]) + index += 2 + continue + } + candidates.insert(argument.lowercased()) + index += 1 + } + await capture(candidates: candidates, expectedPid: expectedPid) + default: + emit([ + "ok": false, + "error": "unknown_command", + "details": ["command": command], + ]) + } + } +} diff --git a/example/.gitignore b/example/.gitignore index 24476c5..6c31954 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/example/CLAUDE.md b/example/CLAUDE.md new file mode 100644 index 0000000..e1a4aaf --- /dev/null +++ b/example/CLAUDE.md @@ -0,0 +1,55 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Structure + +This is the **example app** for the `flutter_curve` package (located at `../`). It's an interactive demo that visualizes physics-based animation curves (Spring, Bounce, Gravity, Cubic easing) with real-time parameter controls. + +The parent package (`../`) contains the actual curve implementations — this directory is only the demo app. + +## Commands + +```bash +# Install dependencies +flutter pub get + +# Run the app +flutter run + +# Analyze / lint +flutter analyze + +# Run tests +flutter test + +# Build for web +flutter build web --base-href // + +# Full web deploy to GitHub Pages (see Makefile) +make deploy OUTPUT= TOKEN= +``` + +## Architecture + +**State management:** Riverpod (`ProviderScope` wraps the app in `main.dart`). Currently only one provider exists: `springIsAdvancedModeProvider` (StateProvider) in `spring_option.dart`. + +**Platform routing:** `main.dart` checks `kIsWeb` to route between `AppPage` (mobile) and `WebPage` (web). Both render `CurvePanel` as their core content. + +**CurvePanel** (`widgets/curve_panel.dart`) is the central orchestrator — it holds the selected `CurveType`, animation duration, and composes: +- `CurveIllustration` — CustomPaint line chart of the curve +- `BallAnimation` — animated circle moving along the curve +- `EffectAnimation` — 4 effect demos (opacity, rotate, translate, scale) +- `CurveOption` — parameter sliders (one subclass per curve type) +- `CodePreview` — generated Dart code for the current config + +**CurveOption** and **CodePreview** are abstract base classes with a factory constructor that returns the appropriate subclass based on `CurveType`. Each curve type has its own `*_option.dart` and `*_code_preview.dart`. + +**Supported curve types** (CurveType enum): `spring`, `bounce`, `forceWithGravity`, `gravity`, `easeIn`, `easeOut`, `easeInOut`. + +## Key Relationships + +- `curve_option/curve_option.dart` — abstract base + `CurveOptionItem` (reusable labeled slider widget) +- `code_preview/code_preview.dart` — abstract base; subclasses generate code strings for each curve +- `style.dart` — `SizeConstant` holds shared layout dimensions +- The `flutter_curve` package is referenced via `path: ../` in `pubspec.yaml` diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 118ee1d..7947a98 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -44,6 +44,8 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.example" // You can update the following values to match your application needs. +nvim + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/example/ios/Flutter/Debug.xcconfig +++ b/example/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/example/ios/Flutter/Release.xcconfig +++ b/example/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile new file mode 100644 index 0000000..e549ee2 --- /dev/null +++ b/example/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/example/lib/main.dart b/example/lib/main.dart index 8daa261..855cd7c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,12 +1,25 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mcp_toolkit/mcp_toolkit.dart'; import 'src/curve_app/app/app_page.dart'; import 'src/curve_app/web/web_page.dart'; void main() { - runApp(const ProviderScope(child: MyApp())); + runZonedGuarded( + () { + WidgetsFlutterBinding.ensureInitialized(); + MCPToolkitBinding.instance + ..initialize() + ..initializeFlutterToolkit(); + runApp(const ProviderScope(child: MyApp())); + }, + (error, stack) => + MCPToolkitBinding.instance.handleZoneError(error, stack), + ); } class MyApp extends StatelessWidget { diff --git a/example/lib/src/curve_app/widgets/ball_animation.dart b/example/lib/src/curve_app/widgets/ball_animation.dart index a15b513..5af2aa9 100644 --- a/example/lib/src/curve_app/widgets/ball_animation.dart +++ b/example/lib/src/curve_app/widgets/ball_animation.dart @@ -2,11 +2,10 @@ import 'package:flutter/material.dart'; /// Curved ball animation. class BallAnimation extends StatefulWidget { - const BallAnimation( - {super.key, - required this.curve, - required this.duration, - required this.width}); + const BallAnimation({super.key, + required this.curve, + required this.duration, + required this.width}); final Curve curve; final Duration duration; @@ -30,6 +29,7 @@ class _BallAnimationState extends State alignment: Alignment.center, child: Stack( children: [ + /// Guide line. Positioned( left: _movingCircleRadius - _guideLineCircleRadius, @@ -37,7 +37,8 @@ class _BallAnimationState extends State top: 0, bottom: 0, child: CustomPaint( - painter: GuideLinePainter(circleRadius: _guideLineCircleRadius), + painter: GuideLinePainter( + circleRadius: _guideLineCircleRadius), )), /// Moving Circle. @@ -67,11 +68,10 @@ class _BallAnimationState extends State } class _CircleMoveAnimation extends StatefulWidget { - const _CircleMoveAnimation( - {required this.curve, - required this.duration, - required this.child, - required this.maxMoveValue}); + const _CircleMoveAnimation({required this.curve, + required this.duration, + required this.child, + required this.maxMoveValue}); final Curve curve; @@ -90,7 +90,8 @@ class _CircleMoveAnimationState extends State<_CircleMoveAnimation> late final _controller = AnimationController( vsync: this, duration: widget.duration, - )..repeat(); + ) + ..repeat(); @override Widget build(BuildContext context) { @@ -126,6 +127,7 @@ class _CircleMoveAnimationState extends State<_CircleMoveAnimation> } } + class GuideLinePainter extends CustomPainter { GuideLinePainter({super.repaint, required this.circleRadius}); diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc index e71a16d..f6f23bf 100644 --- a/example/linux/flutter/generated_plugin_registrant.cc +++ b/example/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake index 2e1de87..f16b4c3 100644 --- a/example/linux/flutter/generated_plugins.cmake +++ b/example/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/example/macos/Flutter/Flutter-Debug.xcconfig b/example/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b..4b81f9b 100644 --- a/example/macos/Flutter/Flutter-Debug.xcconfig +++ b/example/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/Flutter-Release.xcconfig b/example/macos/Flutter/Flutter-Release.xcconfig index c2efd0b..5caa9d1 100644 --- a/example/macos/Flutter/Flutter-Release.xcconfig +++ b/example/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..8236f57 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/example/macos/Podfile b/example/macos/Podfile new file mode 100644 index 0000000..ff5ddb3 --- /dev/null +++ b/example/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock new file mode 100644 index 0000000..beea7f5 --- /dev/null +++ b/example/macos/Podfile.lock @@ -0,0 +1,22 @@ +PODS: + - FlutterMacOS (1.0.0) + - url_launcher_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - FlutterMacOS (from `Flutter/ephemeral`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + +EXTERNAL SOURCES: + FlutterMacOS: + :path: Flutter/ephemeral + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + +SPEC CHECKSUMS: + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + url_launcher_macos: c83b920a14ed6c0ec4d6069c3ec3e19222607405 + +PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 + +COCOAPODS: 1.16.2 diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index 27e0f50..e3ec45d 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 53444F5394B20D90B2B5C3D4 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78B582CF6BBC70B9E63B936B /* Pods_RunnerTests.framework */; }; + E39F46DD26B83190F87518DD /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3E8800A69160B3C038B12B9E /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -64,7 +66,7 @@ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -76,8 +78,16 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 3D00C97B4400EB7E0A2A18C4 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 3E8800A69160B3C038B12B9E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 48E0DCE5F48CF809F73CECB2 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 78B582CF6BBC70B9E63B936B /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 8877C7351B2B81AD93DC67D7 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + C6F65745A44F31F2DE8753D3 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + DCF2ECBF491F4E25DF5145B1 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + F5C4AB57BB0DDC34851E8714 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 53444F5394B20D90B2B5C3D4 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -92,6 +103,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + E39F46DD26B83190F87518DD /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,6 +137,7 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + 35DBACBC3D2E322BA14A9C91 /* Pods */, ); sourceTree = ""; }; @@ -172,9 +185,25 @@ path = Runner; sourceTree = ""; }; + 35DBACBC3D2E322BA14A9C91 /* Pods */ = { + isa = PBXGroup; + children = ( + 8877C7351B2B81AD93DC67D7 /* Pods-Runner.debug.xcconfig */, + F5C4AB57BB0DDC34851E8714 /* Pods-Runner.release.xcconfig */, + DCF2ECBF491F4E25DF5145B1 /* Pods-Runner.profile.xcconfig */, + 3D00C97B4400EB7E0A2A18C4 /* Pods-RunnerTests.debug.xcconfig */, + C6F65745A44F31F2DE8753D3 /* Pods-RunnerTests.release.xcconfig */, + 48E0DCE5F48CF809F73CECB2 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 3E8800A69160B3C038B12B9E /* Pods_Runner.framework */, + 78B582CF6BBC70B9E63B936B /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -186,6 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 9CB1E5E8F3F3312F0F559D6E /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -204,11 +234,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + C36DC009E505E4F8B59E912E /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + C5A6F39736D11E8F2B17E4B6 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -227,7 +259,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { @@ -328,6 +360,67 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 9CB1E5E8F3F3312F0F559D6E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + C36DC009E505E4F8B59E912E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + C5A6F39736D11E8F2B17E4B6 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -379,6 +472,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 3D00C97B4400EB7E0A2A18C4 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -393,6 +487,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = C6F65745A44F31F2DE8753D3 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -407,6 +502,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 48E0DCE5F48CF809F73CECB2 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -457,7 +553,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -536,7 +632,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -583,7 +679,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 397f3d3..ac78810 100644 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ diff --git a/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/example/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/example/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/example/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift index d53ef64..b3c1761 100644 --- a/example/macos/Runner/AppDelegate.swift +++ b/example/macos/Runner/AppDelegate.swift @@ -1,9 +1,13 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/example/pubspec.lock b/example/pubspec.lock index fee253f..d552f49 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,42 +5,42 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" cupertino_icons: dependency: "direct main" description: @@ -49,14 +49,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + dart_mcp: + dependency: transitive + description: + name: dart_mcp + sha256: "2b34cbd60e3b64e7f770a453b84c5f569f096f6044ea343ddffc99f6b366de45" + url: "https://pub.dev" + source: hosted + version: "0.3.3" + equatable: + dependency: transitive + description: + name: equatable + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + url: "https://pub.dev" + source: hosted + version: "2.0.8" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" flutter: dependency: "direct main" description: flutter @@ -68,7 +84,7 @@ packages: path: ".." relative: true source: path - version: "0.0.2" + version: "1.0.0" flutter_lints: dependency: "direct dev" description: @@ -95,6 +111,14 @@ packages: description: flutter source: sdk version: "0.0.0" + from_json_to_json: + dependency: transitive + description: + name: from_json_to_json + sha256: "792db26a52983c62da63b473629a4a65022da83dc69252162c5dccdd27b271ac" + url: "https://pub.dev" + source: hosted + version: "0.3.0" gap: dependency: "direct main" description: @@ -103,30 +127,46 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + is_dart_empty_or_not: + dependency: transitive + description: + name: is_dart_empty_or_not + sha256: "31fef508da3e89ab8cd4634d7149f72a0a2e54c841c96b72f393dfc882fdde81" + url: "https://pub.dev" + source: hosted + version: "0.2.3" + json_rpc_2: + dependency: transitive + description: + name: json_rpc_2 + sha256: "82dfd37d3b2e5030ae4729e1d7f5538cbc45eb1c73d618b9272931facac3bec1" + url: "https://pub.dev" + source: hosted + version: "4.1.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -139,34 +179,42 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + mcp_toolkit: + dependency: "direct main" + description: + name: mcp_toolkit + sha256: d5cb1502ddf39dcfe6fad1f393f68c172e200d88b3cddd5a6d05655bd984d111 url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.3.0" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.18.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" plugin_platform_interface: dependency: transitive description: @@ -187,23 +235,23 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" state_notifier: dependency: transitive description: @@ -216,34 +264,58 @@ packages: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "1.4.0" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 + url: "https://pub.dev" + source: hosted + version: "2.3.1" url_launcher: dependency: "direct main" description: @@ -312,18 +384,18 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "15.0.0" web: dependency: transitive description: @@ -333,5 +405,5 @@ packages: source: hosted version: "0.5.1" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.10.0-0 <4.0.0" flutter: ">=3.19.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 2f03158..a9eda03 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -40,6 +40,7 @@ dependencies: gap: ^3.0.1 url_launcher: ^6.2.5 flutter_riverpod: ^2.5.1 + mcp_toolkit: ^0.3.0 dev_dependencies: flutter_test: diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc index 8b6d468..4f78848 100644 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake index b93c4c3..88b22e5 100644 --- a/example/windows/flutter/generated_plugins.cmake +++ b/example/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From eeaf52ecffdbca2506e6413e17f777101958b262 Mon Sep 17 00:00:00 2001 From: drown0315 Date: Sun, 28 Jun 2026 14:09:09 +0800 Subject: [PATCH 2/5] no-mistakes(review): Fix stray nvim text, add .gitignore for flutter_mcp, remove GPU validation --- .../macos_visual_capture_-585b0c45b152b847 | Bin 149896 -> 0 bytes ...cos_visual_capture_-585b0c45b152b847.swift | 403 ------------------ example/.gitignore | 3 + example/android/app/build.gradle | 1 - .../xcshareddata/xcschemes/Runner.xcscheme | 1 - 5 files changed, 3 insertions(+), 405 deletions(-) delete mode 100755 example/.flutter_mcp/cache/macos_helper/macos_visual_capture_-585b0c45b152b847 delete mode 100644 example/.flutter_mcp/cache/macos_helper/macos_visual_capture_-585b0c45b152b847.swift diff --git a/example/.flutter_mcp/cache/macos_helper/macos_visual_capture_-585b0c45b152b847 b/example/.flutter_mcp/cache/macos_helper/macos_visual_capture_-585b0c45b152b847 deleted file mode 100755 index cb05ae2e82282e175f1df7bdf695e09dfe8b5e10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 149896 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 String { - return value.lowercased().replacingOccurrences( - of: "[^a-z0-9]+", - with: "", - options: .regularExpression - ) -} - -func matchesCandidate(_ value: String, candidates: Set) -> Bool { - let normalized = normalizeOwnerName(value) - if normalized.isEmpty { - return false - } - for candidate in candidates { - let normalizedCandidate = normalizeOwnerName(candidate) - if normalizedCandidate.isEmpty { - continue - } - if normalized == normalizedCandidate || - normalized.contains(normalizedCandidate) || - normalizedCandidate.contains(normalized) { - return true - } - } - return false -} - -func matchesOwner(_ owner: String, candidates: Set) -> Bool { - if candidates.isEmpty { - return true - } - return matchesCandidate(owner, candidates: candidates) -} - -func matchesWindow( - _ window: SCWindow, - candidates: Set, - expectedPid: Int32? -) -> Bool { - if let expectedPid { - return window.owningApplication?.processID == expectedPid - } - if candidates.isEmpty { - return true - } - let owner = window.owningApplication?.applicationName ?? "" - let title = window.title ?? "" - return matchesCandidate(owner, candidates: candidates) || - matchesCandidate(title, candidates: candidates) -} - -func eligibleWindows( - _ windows: [SCWindow], - candidates: Set, - expectedPid: Int32? -) -> [SCWindow] { - return windows.filter { window in - let frame = window.frame - return matchesWindow( - window, - candidates: candidates, - expectedPid: expectedPid - ) && - frame.width > 8 && - frame.height > 8 - } -} - -func largestWindow(_ windows: [SCWindow]) -> SCWindow? { - return windows.max { lhs, rhs in - lhs.frame.width * lhs.frame.height < rhs.frame.width * rhs.frame.height - } -} - -func windowSummary(_ window: SCWindow) -> [String: Any] { - [ - "owner": window.owningApplication?.applicationName ?? "", - "ownerPid": Int(window.owningApplication?.processID ?? 0), - "title": window.title ?? "", - "width": window.frame.size.width, - "height": window.frame.size.height, - "windowId": Int(window.windowID), - ] -} - -func permissionStatusPayload(_ status: String, message: String) { - emit([ - "ok": true, - "status": status, - "message": message, - "canRequest": true, - "canOpenSettings": true, - "details": [:], - ]) -} - -func activateSimulatorApp() { - let source = """ - tell application "Simulator" to activate - """ - if let script = NSAppleScript(source: source) { - var error: NSDictionary? - _ = script.executeAndReturnError(&error) - } -} - -func activateChromeApp() { - let source = """ - tell application "Google Chrome" to activate - """ - if let script = NSAppleScript(source: source) { - var error: NSDictionary? - _ = script.executeAndReturnError(&error) - } -} - -func isChromeCandidateSet(_ candidates: Set) -> Bool { - for candidate in candidates { - let normalized = normalizeOwnerName(candidate) - if normalized.contains("chrome") || normalized.contains("chromium") { - return true - } - } - return false -} - -@MainActor -func focusWindow(candidates: Set, expectedPid: Int32?) async { - _ = NSApplication.shared - - if let expectedPid { - if let app = NSRunningApplication(processIdentifier: expectedPid) { - _ = app.activate(options: [.activateAllWindows, .activateIgnoringOtherApps]) - let owner = app.localizedName ?? "" - if owner.lowercased().contains("simulator") { - activateSimulatorApp() - } else if owner.lowercased().contains("chrome") || - owner.lowercased().contains("chromium") { - activateChromeApp() - } - emit([ - "ok": true, - "activatedPid": Int(expectedPid), - "owner": owner, - ]) - return - } - } - - if candidates.contains("simulator") || candidates.isEmpty { - activateSimulatorApp() - emit([ - "ok": true, - "activated": "simulator", - ]) - return - } - - if isChromeCandidateSet(candidates) { - activateChromeApp() - emit([ - "ok": true, - "activated": "chrome", - ]) - return - } - - emit([ - "ok": false, - "error": "focus_target_not_found", - "details": ["candidates": Array(candidates).sorted()], - ]) -} - -func openSettings() { - guard let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture") else { - emit([ - "ok": false, - "error": "settings_url_invalid", - ]) - return - } - NSWorkspace.shared.open(url) - emit([ - "ok": true, - "status": "denied", - "message": "Opened Screen Recording settings.", - "canRequest": true, - "canOpenSettings": true, - "details": ["opened": true], - ]) -} - -@MainActor -func capture(candidates: Set, expectedPid: Int32?) async { - _ = NSApplication.shared - - guard CGPreflightScreenCaptureAccess() else { - emit([ - "ok": false, - "error": "screen_recording_not_granted", - "permissionStatus": "not_determined", - "details": [ - "action": "request_permission_or_open_settings" - ], - ]) - return - } - - do { - let onScreenContent = try await SCShareableContent.excludingDesktopWindows( - false, - onScreenWindowsOnly: true - ) - - let onScreenMatches = eligibleWindows( - onScreenContent.windows, - candidates: candidates, - expectedPid: expectedPid - ) - var selectionSource = "on_screen" - var captureVisibility = "on_screen" - var selectedWindow = largestWindow(onScreenMatches) - var allWindowsContent: SCShareableContent? - - if selectedWindow == nil { - let anyContent = try await SCShareableContent.excludingDesktopWindows( - false, - onScreenWindowsOnly: false - ) - allWindowsContent = anyContent - let allMatches = eligibleWindows( - anyContent.windows, - candidates: candidates, - expectedPid: expectedPid - ) - selectedWindow = largestWindow(allMatches) - if selectedWindow != nil { - selectionSource = "all_windows_fallback" - captureVisibility = "offscreen_or_hidden" - } - } - - guard let window = selectedWindow else { - let visibleOwners = Array(Set(onScreenContent.windows.compactMap { - let owner = $0.owningApplication?.applicationName ?? "" - return owner.isEmpty ? nil : owner - })).sorted() - let visibleWindows = onScreenContent.windows.prefix(20).map(windowSummary) - let allOwners = Array(Set((allWindowsContent?.windows ?? []).compactMap { - let owner = $0.owningApplication?.applicationName ?? "" - return owner.isEmpty ? nil : owner - })).sorted() - let allCandidateWindows = eligibleWindows( - allWindowsContent?.windows ?? [], - candidates: candidates, - expectedPid: expectedPid - ).prefix(20).map(windowSummary) - var details: [String: Any] = [ - "candidates": Array(candidates).sorted(), - "visibleOwners": visibleOwners, - "visibleWindows": Array(visibleWindows), - "allOwners": allOwners, - "allCandidateWindows": Array(allCandidateWindows), - ] - if let expectedPid { - details["expectedPid"] = Int(expectedPid) - } - emit([ - "ok": false, - "error": "window_not_found", - "details": details, - ]) - return - } - - let filter = SCContentFilter(desktopIndependentWindow: window) - let configuration = SCStreamConfiguration() - configuration.showsCursor = false - configuration.ignoreShadowsSingleWindow = false - let pointPixelScale = CGFloat(filter.pointPixelScale) - configuration.width = Int(filter.contentRect.width * pointPixelScale) - configuration.height = Int(filter.contentRect.height * pointPixelScale) - - let image = try await SCScreenshotManager.captureImage( - contentFilter: filter, - configuration: configuration - ) - let bitmap = NSBitmapImageRep(cgImage: image) - guard let pngData = bitmap.representation(using: .png, properties: [:]) else { - emit([ - "ok": false, - "error": "png_encoding_failed", - "details": ["windowId": window.windowID], - ]) - return - } - - emit([ - "ok": true, - "permissionStatus": "granted", - "appName": window.owningApplication?.applicationName ?? "", - "windowOwnerPid": Int(window.owningApplication?.processID ?? 0), - "windowId": Int(window.windowID), - "windowTitle": window.title ?? "", - "windowSelectionSource": selectionSource, - "windowCaptureVisibility": captureVisibility, - "windowBounds": [ - "x": window.frame.origin.x, - "y": window.frame.origin.y, - "width": window.frame.size.width, - "height": window.frame.size.height, - ], - "pngBase64": pngData.base64EncodedString(), - ]) - } catch { - emit([ - "ok": false, - "error": "capture_failed", - "details": ["message": "\(error)"], - ]) - } -} - -@main -struct VisualCaptureHelper { - static func main() async { - let args = Array(CommandLine.arguments.dropFirst()) - guard let command = args.first else { - emit([ - "ok": false, - "error": "missing_command", - ]) - return - } - - switch command { - case "status": - permissionStatusPayload( - CGPreflightScreenCaptureAccess() ? "granted" : "not_determined", - message: "macOS Screen Recording preflight completed." - ) - case "request": - let granted = CGRequestScreenCaptureAccess() - permissionStatusPayload( - granted ? "granted" : "denied", - message: granted - ? "Screen Recording permission granted." - : "Screen Recording permission not granted." - ) - case "open-settings": - openSettings() - case "focus": - var expectedPid: Int32? - var candidates = Set() - var index = 1 - while index < args.count { - let argument = args[index] - if argument == "--pid", index + 1 < args.count { - expectedPid = Int32(args[index + 1]) - index += 2 - continue - } - candidates.insert(argument.lowercased()) - index += 1 - } - await focusWindow(candidates: candidates, expectedPid: expectedPid) - case "capture": - var expectedPid: Int32? - var candidates = Set() - var index = 1 - while index < args.count { - let argument = args[index] - if argument == "--pid", index + 1 < args.count { - expectedPid = Int32(args[index + 1]) - index += 2 - continue - } - candidates.insert(argument.lowercased()) - index += 1 - } - await capture(candidates: candidates, expectedPid: expectedPid) - default: - emit([ - "ok": false, - "error": "unknown_command", - "details": ["command": command], - ]) - } - } -} diff --git a/example/.gitignore b/example/.gitignore index 6c31954..b917480 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -23,6 +23,9 @@ migrate_working_dir/ # is commented out by default. #.vscode/ +# Flutter MCP cache +.flutter_mcp/ + # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 7947a98..9feca59 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -44,7 +44,6 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.example" // You can update the following values to match your application needs. -nvim // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion flutter.minSdkVersion diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index ac78810..15368ec 100644 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -59,7 +59,6 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" - enableGPUValidationMode = "1" allowLocationSimulation = "YES"> From 990fedc0789701ef53311658cdc64f04980c4548 Mon Sep 17 00:00:00 2001 From: drown0315 Date: Sun, 28 Jun 2026 14:13:25 +0800 Subject: [PATCH 3/5] no-mistakes(document): Sync example/CLAUDE.md with MCP toolkit integration --- example/CLAUDE.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/example/CLAUDE.md b/example/CLAUDE.md index e1a4aaf..6c1c4ff 100644 --- a/example/CLAUDE.md +++ b/example/CLAUDE.md @@ -34,7 +34,9 @@ make deploy OUTPUT= TOKEN= **State management:** Riverpod (`ProviderScope` wraps the app in `main.dart`). Currently only one provider exists: `springIsAdvancedModeProvider` (StateProvider) in `spring_option.dart`. -**Platform routing:** `main.dart` checks `kIsWeb` to route between `AppPage` (mobile) and `WebPage` (web). Both render `CurvePanel` as their core content. +**MCP toolkit:** `main.dart` initializes `MCPToolkitBinding` (via the `mcp_toolkit` package) inside a `runZonedGuarded` wrapper. Zone errors are forwarded to `MCPToolkitBinding.instance.handleZoneError()` for capture and reporting. + +**Platform routing:** After MCP toolkit setup, `main.dart` checks `kIsWeb` to route between `AppPage` (mobile) and `WebPage` (web). Both render `CurvePanel` as their core content. **CurvePanel** (`widgets/curve_panel.dart`) is the central orchestrator — it holds the selected `CurveType`, animation duration, and composes: - `CurveIllustration` — CustomPaint line chart of the curve From ea37bdbaba13dad608f4e9e79a51f753ae17cb46 Mon Sep 17 00:00:00 2001 From: drown0315 Date: Sun, 28 Jun 2026 14:13:59 +0800 Subject: [PATCH 4/5] no-mistakes(lint): Fix dart format issues in changed files --- example/lib/main.dart | 5 ++-- .../src/curve_app/widgets/ball_animation.dart | 26 +++++++++---------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 855cd7c..81d023d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -9,7 +9,7 @@ import 'src/curve_app/app/app_page.dart'; import 'src/curve_app/web/web_page.dart'; void main() { - runZonedGuarded( + runZonedGuarded( () { WidgetsFlutterBinding.ensureInitialized(); MCPToolkitBinding.instance @@ -17,8 +17,7 @@ void main() { ..initializeFlutterToolkit(); runApp(const ProviderScope(child: MyApp())); }, - (error, stack) => - MCPToolkitBinding.instance.handleZoneError(error, stack), + (error, stack) => MCPToolkitBinding.instance.handleZoneError(error, stack), ); } diff --git a/example/lib/src/curve_app/widgets/ball_animation.dart b/example/lib/src/curve_app/widgets/ball_animation.dart index 5af2aa9..a15b513 100644 --- a/example/lib/src/curve_app/widgets/ball_animation.dart +++ b/example/lib/src/curve_app/widgets/ball_animation.dart @@ -2,10 +2,11 @@ import 'package:flutter/material.dart'; /// Curved ball animation. class BallAnimation extends StatefulWidget { - const BallAnimation({super.key, - required this.curve, - required this.duration, - required this.width}); + const BallAnimation( + {super.key, + required this.curve, + required this.duration, + required this.width}); final Curve curve; final Duration duration; @@ -29,7 +30,6 @@ class _BallAnimationState extends State alignment: Alignment.center, child: Stack( children: [ - /// Guide line. Positioned( left: _movingCircleRadius - _guideLineCircleRadius, @@ -37,8 +37,7 @@ class _BallAnimationState extends State top: 0, bottom: 0, child: CustomPaint( - painter: GuideLinePainter( - circleRadius: _guideLineCircleRadius), + painter: GuideLinePainter(circleRadius: _guideLineCircleRadius), )), /// Moving Circle. @@ -68,10 +67,11 @@ class _BallAnimationState extends State } class _CircleMoveAnimation extends StatefulWidget { - const _CircleMoveAnimation({required this.curve, - required this.duration, - required this.child, - required this.maxMoveValue}); + const _CircleMoveAnimation( + {required this.curve, + required this.duration, + required this.child, + required this.maxMoveValue}); final Curve curve; @@ -90,8 +90,7 @@ class _CircleMoveAnimationState extends State<_CircleMoveAnimation> late final _controller = AnimationController( vsync: this, duration: widget.duration, - ) - ..repeat(); + )..repeat(); @override Widget build(BuildContext context) { @@ -127,7 +126,6 @@ class _CircleMoveAnimationState extends State<_CircleMoveAnimation> } } - class GuideLinePainter extends CustomPainter { GuideLinePainter({super.repaint, required this.circleRadius}); From b1fe04819980b848b76db06abc18766b27da830d Mon Sep 17 00:00:00 2001 From: drown0315 Date: Sun, 28 Jun 2026 14:17:10 +0800 Subject: [PATCH 5/5] no-mistakes: apply CI fixes --- example/lib/src/curve_app/widgets/curve_panel.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/example/lib/src/curve_app/widgets/curve_panel.dart b/example/lib/src/curve_app/widgets/curve_panel.dart index 1bd7368..89143d8 100644 --- a/example/lib/src/curve_app/widgets/curve_panel.dart +++ b/example/lib/src/curve_app/widgets/curve_panel.dart @@ -116,12 +116,6 @@ class _CurvePanelState extends State { width: width, ); break; - default: - curveOptionWidget = BounceOption( - onChanged: _onCurveChanged, - width: width, - ); - break; } return kIsWeb