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_toolkit3.x (mcp_flutter v3.1.1). Key API facts baked into these templates:addEntriestakes aSet<MCPCallEntry>and isasync;MCPCallEntry.tooltakes aMCPToolDefinition(typedObjectSchema, not a raw JSON map); handlers receive the request and return anMCPCallResult(message:, parameters:). Tool names must match^[a-zA-Z0-9_-]+$โ dots are rejected by MCP clients, so cocode names use underscores (bloc_dump_state, notbloc.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
CocodeBlocRegistryhelper 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.