mcpField
F
flutter-mcp-toolkit-custom-tools

flutter-mcp-toolkit-custom-tools

github.com/arenukvernDevelopment39460 installsUpdated 2w ago
Repository

About flutter-mcp-toolkit-custom-tools

A skill for Flutter developers adding app-specific MCP tools and resources through the dynamic registry. It explains when to use each entry type, how to register handlers, and how agents discover them.

Key features

  1. 01Choose tools or read-only resources
  2. 02Register entries at app bootstrap
  3. 03Define strict JSON schemas
  4. 04Discover and invoke app entries
  5. 05Avoid lifecycle and naming conflicts

Use cases

  1. 01Expose cart summaries to an agent
  2. 02Provide read-only route or diagnostics data
  3. 03Add parameterized app actions
  4. 04Troubleshoot missing dynamic entries

MCP support

Covers registering custom tools and resources in the Flutter process and exposing them to MCP clients through the dynamic registry.

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/arenukvern/mcp_flutter --skill flutter-mcp-toolkit-custom-tools

After installing, type/flutter-mcp-toolkit-custom-toolsin Claude Code to confirm it loaded.

Details

From the project README

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

NeedUse
One-off read of a simple valuefmt_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 operationAgentCallEntry.tool + fmt_client_tool.

Handler signatures

Native AgentCallEntry (preferred for new code)

Handlers receive AgentArguments (Map<String, Object?>) and return AgentResult:

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<String, String> service-extension handlers:

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

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:

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/<domain>_surfaces.dart returning Set<AgentCallEntry> 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<String, String> — 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”

Comments

Markdown supported