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.

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;
  binding.initializeFlutterToolkit();
  binding.addEntries(entries: cocodeMcpEntries);
}

Call registerCocodeMcpTools() early in main().

2. Cocode entry set#

// packages/dev_tooling/lib/mcp_entries.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:mcp_toolkit/mcp_toolkit.dart';
import 'package:my_app/router.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 Iterable<MCPCallEntry> cocodeMcpEntries = [
  ...blocEntries,
  ...dbEntries,
  ...authEntries,
  ...flagsEntries,
  ...networkEntries,
  ...localeEntries,
  ...analyticsEntries,
  ...navEntries,
];

3. bloc.dump_state example#

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

final blocEntries = <MCPCallEntry>[
  MCPCallEntry.tool(
    name: 'bloc.dump_state',
    description: 'Returns JSON snapshot of one or all registered BLoCs.',
    inputSchema: {
      'type': 'object',
      'properties': {
        'type': {'type': 'string', 'description': 'BLoC runtimeType name; omit for all'},
      },
    },
    handler: (args) async {
      final typeFilter = args?['type'] as String?;
      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.text(jsonEncode(snapshot));
    },
  ),
  MCPCallEntry.tool(
    name: 'bloc.add_event',
    description: 'Dispatch an event into a registered BLoC.',
    inputSchema: {
      'type': 'object',
      'required': ['type', 'event'],
      'properties': {
        'type': {'type': 'string'},
        'event': {'type': 'object'},
      },
    },
    handler: (args) async {
      final typeName = args!['type'] as String;
      final eventJson = args['event'] as Map<String, dynamic>;
      final entry = CocodeBlocRegistry.instance.byTypeName(typeName);
      if (entry == null) {
        return MCPCallResult.error('No BLoC registered as $typeName');
      }
      entry.addJsonEvent(eventJson);
      return MCPCallResult.text('dispatched');
    },
  ),
];

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(
    name: 'db.seed_fixture',
    description: 'Seed the embedded Drift database with a named fixture.',
    inputSchema: {
      'type': 'object',
      'required': ['name'],
      'properties': {
        'name': {
          'type': 'string',
          'enum': fixtureRegistry.keys.toList(),
        },
      },
    },
    handler: (args) async {
      final name = args!['name'] as String;
      final fixture = fixtureRegistry[name];
      if (fixture == null) {
        return MCPCallResult.error('Unknown fixture: $name');
      }
      final db = GetIt.I<AppDatabase>();
      await fixture.apply(db);
      return MCPCallResult.text('seeded $name (${fixture.recordCount} rows)');
    },
  ),
  MCPCallEntry.tool(
    name: 'db.reset',
    description: 'Drop all data and reapply default seed.',
    handler: (_) async {
      final db = GetIt.I<AppDatabase>();
      await db.resetForTesting();
      await defaultSeed.apply(db);
      return MCPCallResult.text('reset complete');
    },
  ),
];

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';
import 'package:my_app/router.dart';

final navEntries = <MCPCallEntry>[
  MCPCallEntry.tool(
    name: 'nav.go_named',
    description: 'Navigate via GoRouter by route name.',
    inputSchema: {
      'type': 'object',
      'required': ['name'],
      'properties': {
        'name': {'type': 'string'},
        'pathParameters': {'type': 'object'},
        'queryParameters': {'type': 'object'},
      },
    },
    handler: (args) async {
      final router = GetIt.I<GoRouter>();
      router.goNamed(
        args!['name'] as String,
        pathParameters: Map<String, String>.from(args['pathParameters'] ?? {}),
        queryParameters: Map<String, dynamic>.from(args['queryParameters'] ?? {}),
      );
      return MCPCallResult.text('navigated');
    },
  ),
];

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(
    name: 'network.offline',
    description: 'Toggle a global interceptor that fails every HTTP request.',
    inputSchema: {
      'type': 'object',
      'required': ['value'],
      'properties': {'value': {'type': 'boolean'}},
    },
    handler: (args) async {
      final on = args!['value'] as bool;
      final dio = GetIt.I<Dio>();
      dio.interceptors.removeWhere((i) => i is OfflineSimulationInterceptor);
      if (on) {
        dio.interceptors.add(OfflineSimulationInterceptor());
      }
      return MCPCallResult.text('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 " :  " mcp_server_dart " ,
       " args " : [ " --images " ]   // enable screenshot tool
    }
  }
}

8. Smoke prompt#

Use mcp_toolkit dynamic tools 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-flutter-pixel-loop:capture --route=/feed --label=feed-after-seed.
Confirm bloc.state.posts.length == 10 and the screenshot shows 10 cards.