LogoSkills

mcp-toolkit-guide Templates

Reference implementations for the cocode-standard dynamic tools listed in `SKILL.md`. Ship these in a shared internal package (e.g. `packages/dev_tooling/mcp_entries`) so every app gets them by add...

MCP Toolkit โ€” Cocode Dynamic Tool Templates#

Reference implementations for the cocode-standard dynamic tools listed in SKILL.md. Ship these in a shared internal package (e.g. packages/dev_tooling/mcp_entries) so every app gets them by adding one import.

Targets mcp_toolkit 3.x (mcp_flutter v3.1.1). Key API facts baked into these templates: addEntries takes a Set<MCPCallEntry> and is async; MCPCallEntry.tool takes a MCPToolDefinition (typed ObjectSchema, not a raw JSON map); handlers receive the request and return an MCPCallResult(message:, parameters:). Tool names must match ^[a-zA-Z0-9_-]+$ โ€” dots are rejected by MCP clients, so cocode names use underscores (bloc_dump_state, not bloc.dump_state).

1. Binding bootstrap#

// lib/main_dev_tools.dart
import 'package:flutter/foundation.dart';
import 'package:mcp_toolkit/mcp_toolkit.dart';
import 'package:dev_tooling/mcp_entries.dart';

Future<void> registerCocodeMcpTools() async {
  if (!kDebugMode) return;
  final binding = MCPToolkitBinding.instance
    ..initialize()
    ..initializeFlutterToolkit();
  await binding.addEntries(entries: cocodeMcpEntries);
}

Call registerCocodeMcpTools() early in main(). Alternatively use the v3 golden path, which wraps init + entries + runApp in one call:

await MCPToolkitBinding.instance.bootstrapFlutter(
  additionalEntries: cocodeMcpEntries,
  runApp: () => runApp(const App()),
);

2. Cocode entry set#

// packages/dev_tooling/lib/mcp_entries.dart
import 'package:mcp_toolkit/mcp_toolkit.dart';

import 'tools/bloc.dart';
import 'tools/db.dart';
import 'tools/auth.dart';
import 'tools/flags.dart';
import 'tools/network.dart';
import 'tools/locale.dart';
import 'tools/analytics.dart';
import 'tools/nav.dart';

final Set<MCPCallEntry> cocodeMcpEntries = {
  ...blocEntries,
  ...dbEntries,
  ...authEntries,
  ...flagsEntries,
  ...networkEntries,
  ...localeEntries,
  ...analyticsEntries,
  ...navEntries,
};

3. bloc_dump_state example#

// packages/dev_tooling/lib/tools/bloc.dart
import 'package:mcp_toolkit/mcp_toolkit.dart';

final blocEntries = <MCPCallEntry>{
  MCPCallEntry.tool(
    definition: MCPToolDefinition(
      name: 'bloc_dump_state',
      description: 'Returns JSON snapshot of one or all registered BLoCs.',
      inputSchema: ObjectSchema(
        properties: {
          'type': StringSchema(
            description: 'BLoC runtimeType name; omit for all',
          ),
        },
      ),
    ),
    handler: (final request) {
      final typeFilter = request['type'];
      final snapshot = <String, Object?>{};
      for (final entry in CocodeBlocRegistry.instance.all) {
        if (typeFilter != null && entry.runtimeType.toString() != typeFilter) {
          continue;
        }
        snapshot[entry.runtimeType.toString()] =
            _redact(entry.state.toJson());
      }
      return MCPCallResult(
        message: 'BLoC state snapshot (${snapshot.length} blocs)',
        parameters: {'blocs': snapshot},
      );
    },
  ),
  MCPCallEntry.tool(
    definition: MCPToolDefinition(
      name: 'bloc_add_event',
      description: 'Dispatch an event into a registered BLoC.',
      inputSchema: ObjectSchema(
        required: ['type', 'event'],
        properties: {
          'type': StringSchema(),
          'event': ObjectSchema(),
        },
      ),
    ),
    handler: (final request) {
      final typeName = request['type'] as String;
      final eventJson = request['event'] as Map<String, dynamic>;
      final entry = CocodeBlocRegistry.instance.byTypeName(typeName);
      if (entry == null) {
        return MCPCallResult(
          message: 'No BLoC registered as $typeName',
          parameters: {'ok': false},
        );
      }
      entry.addJsonEvent(eventJson);
      return MCPCallResult(message: 'dispatched', parameters: {'ok': true});
    },
  ),
};

Map<String, Object?> _redact(Map<String, Object?> input) {
  // Centralise PII redaction here.
  const piiKeys = {'email', 'phone', 'auth_token', 'access_token'};
  return {
    for (final entry in input.entries)
      entry.key: piiKeys.contains(entry.key) ? '<redacted>' : entry.value,
  };
}

Requires a small CocodeBlocRegistry helper that keeps weak refs to currently-instantiated BLoCs and serialises their state. Implementation is project-specific.

4. db_seed_fixture example#

// packages/dev_tooling/lib/tools/db.dart
import 'package:get_it/get_it.dart';
import 'package:mcp_toolkit/mcp_toolkit.dart';
import 'package:my_app/data/local/app_database.dart';
import 'package:my_app/data/local/fixtures.dart';

final dbEntries = <MCPCallEntry>{
  MCPCallEntry.tool(
    definition: MCPToolDefinition(
      name: 'db_seed_fixture',
      description: 'Seed the embedded Drift database with a named fixture.',
      inputSchema: ObjectSchema(
        required: ['name'],
        properties: {
          'name': StringSchema(
            description: 'One of: ${fixtureRegistry.keys.join(', ')}',
          ),
        },
      ),
    ),
    handler: (final request) async {
      final name = request['name'] as String;
      final fixture = fixtureRegistry[name];
      if (fixture == null) {
        return MCPCallResult(
          message: 'Unknown fixture: $name',
          parameters: {'ok': false},
        );
      }
      final db = GetIt.I<AppDatabase>();
      await fixture.apply(db);
      return MCPCallResult(
        message: 'seeded $name (${fixture.recordCount} rows)',
        parameters: {'ok': true, 'rows': fixture.recordCount},
      );
    },
  ),
  MCPCallEntry.tool(
    definition: MCPToolDefinition(
      name: 'db_reset',
      description: 'Drop all data and reapply default seed.',
      inputSchema: ObjectSchema(),
    ),
    handler: (final request) async {
      final db = GetIt.I<AppDatabase>();
      await db.resetForTesting();
      await defaultSeed.apply(db);
      return MCPCallResult(message: 'reset complete', parameters: {'ok': true});
    },
  ),
};

5. nav_go_named example#

// packages/dev_tooling/lib/tools/nav.dart
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:mcp_toolkit/mcp_toolkit.dart';

final navEntries = <MCPCallEntry>{
  MCPCallEntry.tool(
    definition: MCPToolDefinition(
      name: 'nav_go_named',
      description: 'Navigate via GoRouter by route name.',
      inputSchema: ObjectSchema(
        required: ['name'],
        properties: {
          'name': StringSchema(),
          'pathParameters': ObjectSchema(),
          'queryParameters': ObjectSchema(),
        },
      ),
    ),
    handler: (final request) {
      final router = GetIt.I<GoRouter>();
      router.goNamed(
        request['name'] as String,
        pathParameters:
            Map<String, String>.from(request['pathParameters'] ?? {}),
        queryParameters:
            Map<String, dynamic>.from(request['queryParameters'] ?? {}),
      );
      return MCPCallResult(message: 'navigated', parameters: {'ok': true});
    },
  ),
};

6. network_offline toggle#

// packages/dev_tooling/lib/tools/network.dart
import 'package:dio/dio.dart';
import 'package:get_it/get_it.dart';
import 'package:mcp_toolkit/mcp_toolkit.dart';

final networkEntries = <MCPCallEntry>{
  MCPCallEntry.tool(
    definition: MCPToolDefinition(
      name: 'network_offline',
      description: 'Toggle a global interceptor that fails every HTTP request.',
      inputSchema: ObjectSchema(
        required: ['value'],
        properties: {'value': BooleanSchema()},
      ),
    ),
    handler: (final request) {
      final on = request['value'] == true || request['value'] == 'true';
      final dio = GetIt.I<Dio>();
      dio.interceptors.removeWhere((i) => i is OfflineSimulationInterceptor);
      if (on) {
        dio.interceptors.add(OfflineSimulationInterceptor());
      }
      return MCPCallResult(message: 'offline=$on', parameters: {'offline': on});
    },
  ),
};

class OfflineSimulationInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    handler.reject(
      DioException(
        requestOptions: options,
        type: DioExceptionType.connectionError,
        message: 'offline simulation',
      ),
    );
  }
}

7. .mcp.json (per-project)#

{
   " mcpServers " : {
     " mcp_flutter " : {
       " command " :  " flutter-mcp-toolkit-server " ,   // installed by install.sh, NOT via pub
       " args " : [ " --resources " ,  " --images " ,  " --dynamics " ]
      // --images enables fmt_get_screenshots; --dynamics exposes dynamically
      // registered app tools; add --dumps only when you need fmt_debug_dump_*.
    }
  }
}

8. Smoke prompt#

Use mcp_toolkit dynamic tools (fmt_client_tool) to:
  1. db_reset
  2. db_seed_fixture { name:  " feed_with_10 "   }
  3. nav_go_named { name:  " feed "   }
  4. bloc_dump_state { type:  " FeedBloc "   }
Then call cc-pixel-loop:capture --route=/feed --label=feed-after-seed.
Confirm bloc.state.posts.length == 10 and the screenshot shows 10 cards.