--- 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 — AgentCallEntry, 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. --- # 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**. > **Migration:** The legacy call-entry type was removed in intentcall Phase 6b. Use **`AgentCallEntry`** or **`mcpToolkitTool` / `mcpToolkitResource`**. See **`flutter-mcp-toolkit-intentcall-migration`**. > **Boundary:** Change app discovery, Flutter VM-service extensions, or > app-owned debug surfaces here. Change canonical registry/session semantics, > schema policy, platform projection, or publish behavior in the upstream > IntentCall repository. ## 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”) | **`AgentCallEntry.resource`** + **`fmt_client_resource`**. Prefer resources when the contract is “GET-like” and idempotent. | | Parameterized or mutating action, or reusable named operation | **`AgentCallEntry.tool`** + **`fmt_client_tool`**. | ## Handler signatures ### Native `AgentCallEntry` (preferred for new code) Handlers receive **`AgentArguments`** (`Map`) and return **`AgentResult`**: ```dart import 'package:mcp_toolkit/mcp_toolkit.dart'; final tool = AgentCallEntry.tool( namespace: 'app', name: 'cart_get_snapshot', description: 'Return current cart total and items for a user.', inputSchema: const { 'type': 'object', 'additionalProperties': false, 'properties': { 'userId': {'type': 'string'}, }, 'required': ['userId'], }, handler: (final args) async { final userId = args['userId']?.toString() ?? ''; final cart = CartRepository.instance.forUser(userId); return AgentResult.success( message: 'ok', data: { 'total': cart.total, 'items': cart.items.map((final i) => i.toJson()).toList(), }, ); }, ); await MCPToolkitBinding.instance.addEntries(entries: {tool}); ``` ### Legacy `MCPToolDefinition` + `MCPCallResult` (bridge) Built-in toolkits still use **`mcpToolkitTool`** / **`mcpToolkitResource`** to adapt `Map` service-extension handlers: ```dart final tool = mcpToolkitTool( namespace: 'app', handler: (final request) async { final userId = request['userId'] ?? ''; return MCPCallResult( message: 'ok', parameters: {'userId': userId}, ); }, definition: MCPToolDefinition( name: 'cart_get_snapshot', description: 'Return current cart total and items for a user.', inputSchema: ObjectSchema( properties: { 'userId': StringSchema(), }, required: ['userId'], ), ), ); ``` - Tool arguments on the wire are **strings** keyed by schema property names — parse with `int.tryParse`, `jsonDecode`, etc. - Do **not** use `request.arguments` on the app side. Prefer **`MCPToolkitBinding.instance.addEntries(entries: { ... })` before `bootstrapFlutter(runApp: ...)`** so tools/resources register in one place with zone/error setup. Register **once** at bootstrap — not inside `build`, not per-widget `initState`. ## Custom resources ```dart final resource = AgentCallEntry.resource( namespace: 'app', name: 'app_cart_digest', description: 'Compact cart summary for agents (read-only).', mimeType: 'application/json', handler: (final args) async => AgentResult.success( message: 'Cart digest', data: { 'itemCount': CartRepository.instance.visibleCount, 'currency': CartRepository.instance.currencyCode, }, ), ); ``` Or via bridge: ```dart mcpToolkitResource( namespace: 'app', definition: MCPResourceDefinition( name: 'app_cart_digest', description: 'Compact cart summary for agents (read-only).', mimeType: 'application/json', ), handler: (final request) async => MCPCallResult( message: 'Cart digest', parameters: {'itemCount': 3}, ), ), ``` - **`name`** must be `snake_case`. Published resource URIs follow **`visual://localhost/...`** conventions; agents use **`fmt_client_resource`** and listings from **`fmt_list_client_tools_and_resources`**. ## Schema rules (tools) The MCP server enforces strict JSON Schema: - Prefer **`additionalProperties: false`** unless you intentionally accept arbitrary keys. - Mark **`required`** for anything the handler reads unconditionally. - **`AgentResult.data`** / **`MCPCallResult.parameters`** must be JSON-serializable. ## Discovery from the agent side 1. **`fmt_list_client_tools_and_resources`** — enumerate app-registered tools and resources. 2. **`fmt_client_tool`** — invoke a dynamic tool by name with JSON args. 3. **`fmt_client_resource`** — fetch a registered resource URI from the listing. If something should appear but does not: confirm **`addEntries`** completed (**`await`**), then hot **restart**. ## Lifecycle gotchas - **Hot reload** + **`addEntries`** from widget code → duplicate registrations. Register once in **`main()` / bootstrap**. - **Debug mode only** — release builds do not expose VM service extensions. - **Naming**: the app uses a flat global namespace. Prefix tools/resources (`cart_`, `flags_`, `nav_`). If a built-in entry already uses a name, the custom entry is skipped without warning; the built-in handles the call and the custom handler does not run. - **Naming the running instance** is `MCPToolkitBinding.instance.setAppIdentity(label: 'Fleet app · staging')`, called again whenever the name changes (sign-in, workspace switch). Discovery reads that label, and a tool of your own called `app_identity` is one of the collisions above. ## 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`** returning **`Set`** or calling **`addEntries`** once. 3. Wire from **`addEntries(...)` before `bootstrapFlutter(...)`** — never from **`StatefulWidget` lifecycle**. 4. Tight schemas; hot **restart**; then **`fmt_list_client_tools_and_resources`** before first client invoke. ## Safety and scope - Treat handlers as **powerful debug hooks**: avoid secrets, unchecked IO. - Keep handlers thin: delegate to existing app services. ## Common traps - Mixing **`AgentArguments`** with **`Map`** — pick one API (native vs `mcpToolkitTool`). - Missing **`await`** on **`addEntries`** → race before discovery. - **`inputSchema`** out of sync with the handler → agents trust the schema; update both. ## Related - **`flutter-mcp-toolkit-intentcall-migration`** — CLI migrator, breaking upgrade - **`flutter-mcp-toolkit-guide`** → inspect / control / debug skills - Repository **`ARCHITECTURE.md`** → “Dynamic Registry Architecture”