flutter-mcp-toolkit-custom-tools
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
- 01Choose tools or read-only resources
- 02Register entries at app bootstrap
- 03Define strict JSON schemas
- 04Discover and invoke app entries
- 05Avoid lifecycle and naming conflicts
Use cases
- 01Expose cart summaries to an agent
- 02Provide read-only route or diagnostics data
- 03Add parameterized app actions
- 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-toolsAfter installing, type/flutter-mcp-toolkit-custom-toolsin Claude Code to confirm it loaded.
Details
From the project READMECustom 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
AgentCallEntryormcpToolkitTool/mcpToolkitResource. Seeflutter-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
| Need | Use |
|---|---|
| One-off read of a simple value | fmt_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 operation | AgentCallEntry.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.argumentson 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},
),
),
namemust besnake_case. Published resource URIs followvisual://localhost/...conventions; agents usefmt_client_resourceand listings fromfmt_list_client_tools_and_resources.
Schema rules (tools)
The MCP server enforces strict JSON Schema:
- Prefer
additionalProperties: falseunless you intentionally accept arbitrary keys. - Mark
requiredfor anything the handler reads unconditionally. AgentResult.data/MCPCallResult.parametersmust be JSON-serializable.
Discovery from the agent side
fmt_list_client_tools_and_resources— enumerate app-registered tools and resources.fmt_client_tool— invoke a dynamic tool by name with JSON args.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 +
addEntriesfrom widget code → duplicate registrations. Register once inmain()/ 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 calledapp_identityis one of the collisions above.
When the agent authors surfaces for the user’s app
- Ensure
mcp_toolkitis inpubspec.yaml. - Add
lib/mcp_tools/<domain>_surfaces.dartreturningSet<AgentCallEntry>or callingaddEntriesonce. - Wire from
addEntries(...)beforebootstrapFlutter(...)— never fromStatefulWidgetlifecycle. - Tight schemas; hot restart; then
fmt_list_client_tools_and_resourcesbefore 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
AgentArgumentswithMap<String, String>— pick one API (native vsmcpToolkitTool). - Missing
awaitonaddEntries→ race before discovery. inputSchemaout of sync with the handler → agents trust the schema; update both.
Related
flutter-mcp-toolkit-intentcall-migration— CLI migrator, breaking upgradeflutter-mcp-toolkit-guide→ inspect / control / debug skills- Repository
ARCHITECTURE.md→ “Dynamic Registry Architecture”