Skip to content

Latest commit

 

History

History
270 lines (219 loc) · 7.8 KB

File metadata and controls

270 lines (219 loc) · 7.8 KB

Dynamic Tool Registry

Dynamic Tool Registry lets your Flutter app expose app-specific MCP tools/resources at runtime, without rebuilding the MCP server.

mcp_flutter owns the Flutter bridge: VM-service extensions, DTD discovery, fmt_* tools, and app-owned debug surfaces. Canonical IntentCall registry, session, schema, platform projection, and publish semantics live upstream in the IntentCall repository.

Why It Exists

Built-in MCP tools are generic. Real projects usually need custom actions like:

  • inspect app state that only your code understands
  • run domain-specific debug checks
  • expose read-only app snapshots to the AI assistant

Dynamic registry exists so these capabilities can live in the Flutter app (where context and access control already exist), while still being callable through MCP.

When To Use It

Use Dynamic Tool Registry when:

  • you need app-specific diagnostics beyond built-in tools
  • you have repeated debugging workflows worth turning into tools
  • your project has internal state/config that should be exposed in a controlled shape

Do not use it when:

  • built-in tools (get_vm, get_app_errors, get_view_details) already solve the need
  • the action should run in production user builds
  • the handler would perform risky side effects without explicit safeguards

Architecture (ASCII)

+---------------------+          stdio MCP           +---------------------------+
| AI Client           | <--------------------------> | mcp_server_dart           |
| (Cursor/Cline/etc.) |                              | - static tools            |
+---------------------+                              | - dynamic registry bridge |
                                                     +-------------+-------------+
                                                                   |
                                                     VM Service + DTD events
                                                                   |
                                                     +-------------v-------------+
                                                     | Flutter App               |
                                                     | + mcp_toolkit             |
                                                     | + MCPToolkitBinding       |
                                                     +---------------------------+

Lifecycle (ASCII)

App start
  |
  | MCPToolkitBinding.initialize()
  | MCPToolkitBinding.initializeFlutterToolkit()
  v
Register entries with addEntries(...)
  |
  v
Server discovers dynamic entries via VM/DTD
  |
  v
AI calls fmt_list_client_tools_and_resources
  |
  +--> fmt_client_tool(toolName, arguments)
  |
  +--> fmt_client_resource(resourceUri)

Minimal Correct Setup

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mcp_toolkit/mcp_toolkit.dart';

Future<void> main() async {
  await MCPToolkitBinding.instance.bootstrapFlutter(
    additionalEntries: kDebugMode
        ? await buildDynamicEntries()
        : const <AgentCallEntry>{},
    runApp: () => runApp(const MyApp()),
  );
}

Register A Dynamic Tool

import 'package:mcp_toolkit/mcp_toolkit.dart';

Future<Set<AgentCallEntry>> buildDynamicEntries() async {
  return {
    AgentCallEntry.tool(
      namespace: 'app',
      name: 'say_hello',
      description: 'Return a greeting for a provided name',
      inputSchema: const {
        'type': 'object',
        'additionalProperties': false,
        'properties': {
          'name': {'type': 'string'},
        },
      },
      handler: (final args) async {
        final name = args['name']?.toString() ?? 'World';
        return AgentResult.success(
          message: 'Hello, $name!',
          data: {'greeting': 'Hello, $name!'},
        );
      },
    ),
  };
}

If the app cannot use bootstrapFlutter, register once after initialize() and initializeFlutterToolkit():

Future<void> registerDynamicEntries() async {
  final sayHello = AgentCallEntry.tool(
    namespace: 'app',
    name: 'say_hello',
    description: 'Return a greeting for a provided name',
    inputSchema: const {
      'type': 'object',
      'additionalProperties': false,
      'properties': {
        'name': {'type': 'string'},
      },
    },
    handler: (final args) async {
      final name = args['name']?.toString() ?? 'World';
      return AgentResult.success(
        message: 'Hello, $name!',
        data: {'greeting': 'Hello, $name!'},
      );
    },
  );

  await MCPToolkitBinding.instance.addEntries(entries: {sayHello});
}

Register A Dynamic Resource

AgentCallEntry.resource is name-based. URI is derived from name:

  • app_config -> visual://localhost/app/config
  • user_debug_state -> visual://localhost/user/debug/state
import 'package:mcp_toolkit/mcp_toolkit.dart';

Future<void> registerConfigResource() async {
  final appConfig = AgentCallEntry.resource(
    namespace: 'app',
    name: 'app_config',
    description: 'Read current app configuration',
    mimeType: 'application/json',
    handler: (_) async => AgentResult.success(
      message: 'App configuration snapshot',
      data: {
        'config': {'apiBaseUrl': 'https://example.com'},
      },
    ),
  );

  await MCPToolkitBinding.instance.addEntries(entries: {appConfig});
}

How To Use It From AI Clients

  1. Discover currently registered dynamic entries:
{
  "name": "fmt_list_client_tools_and_resources",
  "arguments": {}
}
  1. Execute by exact tool name:
{
  "name": "fmt_client_tool",
  "arguments": {
    "toolName": "say_hello",
    "arguments": {"name": "Anton"}
  }
}
  1. Read by exact resource URI:
{
  "name": "fmt_client_resource",
  "arguments": {
    "resourceUri": "visual://localhost/app/config"
  }
}

Multi-App / Multi-Target Correctness

If multiple debug targets are running, dynamic calls may return connection_selection_required.

Retry with explicit nested connection:

{
  "name": "fmt_client_tool",
  "arguments": {
    "toolName": "say_hello",
    "arguments": {"name": "Anton"},
    "connection": {
      "targetId": "ws://127.0.0.1:59490/<token>/ws"
    }
  }
}

Connection fields supported:

  • targetId (preferred, full VM websocket URI)
  • mode (auto, manual, uri)
  • host
  • port
  • uri
  • forceReconnect

Use It Correctly

Rules that prevent most issues:

  • register entries only after initialize() has run
  • keep names stable (snake_case) and unique
  • always provide clear description and strict inputSchema
  • keep handlers deterministic and response payloads compact
  • return structured JSON in parameters for machine use
  • guard debug-only tools with kDebugMode
  • avoid hidden side effects; make destructive actions explicit in name/description

Common Mistakes

Symptom Likely cause Fix
Tool/resource not visible Entry not registered or client cached old tool list Ensure addEntries(...) executes, then reload MCP server/client if needed
tool not found Name mismatch Copy exact toolName from fmt_list_client_tools_and_resources
resource not found URI mismatch Copy exact resourceUri from fmt_list_client_tools_and_resources
connection_selection_required Multiple debug targets Retry with arguments.connection.targetId
Unexpectedly large responses Handler returns verbose payload Return summary fields and only required details

Quick Checklist

  • [ ] MCPToolkitBinding.instance.initialize() is called
  • [ ] initializeFlutterToolkit() is called (if needed)
  • [ ] dynamic entries are registered via addEntries(...)
  • [ ] names/descriptions/schemas are explicit
  • [ ] dynamic entries are verified via fmt_list_client_tools_and_resources
  • [ ] multi-target retries include explicit connection.targetId