Model Context Protocol (SSE/STDIO), the mcp_client & llm_shared packages, BLoC state management, go_router, and the 4-app Flutter ecosystem.
MCP (Model Context Protocol) is an open standard created by Anthropic for connecting LLMs (Claude, GPT, Gemini) to external data sources and tools. Think of it like USB-C for AI — a universal interface so any LLM can access any data source without custom integration code per pair.
get_inventory, place_order)| Transport | How it works | Used for |
|---|---|---|
| STDIO | Parent process communicates with MCP server child process via stdin/stdout | Desktop apps, CLI tools, local processes |
| SSE | Long-lived HTTP connection; server pushes events via Server-Sent Events | Web, mobile, cloud-hosted MCP servers |
mcp_client package abstracts this — same API regardless of transportQ: How is MCP different from function calling in OpenAI or tool_use in Claude?
A: Function calling and tool_use are LLM-specific APIs — you define tools in the LLM's API format and they only work with that specific model. MCP is a transport-level standard: the tool definitions live in a separate MCP server, and any MCP-compatible LLM can discover and use them without rewriting tool definitions. MCPVave's llm_shared package routes the same MCP tool calls to Claude, GPT, or Gemini depending on user preference — zero code change per model.
// mcp_client package API (simplified)
class McpClient {
final McpTransport transport; // SseTransport or StdioTransport
McpClient({required this.transport});
Future<void> connect() async {
await transport.connect();
await _initialize(); // MCP handshake: version negotiation
}
// Discover what tools the server exposes
Future<List<McpTool>> listTools() async {
final response = await _request('tools/list', {});
return (response['tools'] as List).map(McpTool.fromJson).toList();
}
// Call a tool on the MCP server
Future<McpResult> callTool(String name, Map<String, dynamic> arguments) async {
return _request('tools/call', {'name': name, 'arguments': arguments});
}
// Read a resource (data)
Future<McpResource> readResource(String uri) async {
return _request('resources/read', {'uri': uri});
}
}
// Unified interface: swap LLM providers without code change
abstract class LlmProvider {
Future<LlmResponse> chat(List<LlmMessage> messages, {List<McpTool>? tools});
Stream<String> chatStream(List<LlmMessage> messages);
}
class ClaudeProvider implements LlmProvider {
@override
Future<LlmResponse> chat(List<LlmMessage> messages, {List<McpTool>? tools}) {
// Anthropic API — messages API with tool_use
}
}
class GptProvider implements LlmProvider {
@override
Future<LlmResponse> chat(List<LlmMessage> messages, {List<McpTool>? tools}) {
// OpenAI API — chat completions with function_call
}
}
// Usage: same code works for any provider
final llm = GptProvider(); // or ClaudeProvider(), GeminiProvider()
final tools = await mcpClient.listTools();
final response = await llm.chat(messages, tools: tools);
// If LLM wants to call a tool → mcpClient.callTool(name, args)
// Order BLoC
class OrderBloc extends Bloc<OrderEvent, OrderState> {
final OrderRepository orderRepository;
final OrderWebSocketService wsService;
OrderBloc({required this.orderRepository, required this.wsService})
: super(OrderInitial()) {
on<OrderSubscribeRequested>((event, emit) async {
emit(OrderLoading());
try {
// Initial fetch
final order = await orderRepository.getOrder(event.orderId);
emit(OrderLoaded(order: order));
// Subscribe to real-time updates
await emit.forEach(
wsService.orderUpdates(event.orderId),
onData: (updatedOrder) => OrderLoaded(order: updatedOrder),
onError: (_, __) => OrderError('Connection lost'),
);
} catch (e) {
emit(OrderError(e.toString()));
}
});
}
}
// States
abstract class OrderState {}
class OrderInitial extends OrderState {}
class OrderLoading extends OrderState {}
class OrderLoaded extends OrderState { final Order order; OrderLoaded({required this.order}); }
class OrderError extends OrderState { final String message; OrderError(this.message); }
final router = GoRouter(
initialLocation: '/splash',
redirect: (context, state) {
final isLoggedIn = context.read<AuthBloc>().state is AuthAuthenticated;
final isGoingToAuth = state.uri.toString().startsWith('/auth');
if (!isLoggedIn && !isGoingToAuth) return '/auth/login';
if (isLoggedIn && isGoingToAuth) return '/home';
return null; // no redirect
},
routes: [
GoRoute(path: '/splash', builder: (_, __) => const SplashScreen()),
GoRoute(path: '/auth/login', builder: (_, __) => const LoginScreen()),
GoRoute(
path: '/orders/:orderId',
builder: (context, state) {
final orderId = state.pathParameters['orderId']!;
return OrderDetailScreen(orderId: orderId);
},
),
// Deep link: zoober://orders/abc123
// → opens OrderDetailScreen directly
],
);
Q: How do deep links work with go_router?
A: Deep links are URL-like identifiers that open a specific screen in the app. When a customer receives an SMS "Your order is ready: zoober://orders/abc123", tapping it launches the app and go_router matches the URL to the /orders/:orderId route, opening that order directly. go_router handles both web URLs (https://zoober.in/orders/abc) and custom scheme links (zoober://orders/abc) with the same route definition — no platform-specific code needed.
MAJOR.MINOR.PATCH → e.g., 2.3.1versionCode (integer, increments each release) for Play Store/App Storepubspec.yaml versionflutter build appbundle --release (Android) or flutter build ipa --release (iOS)Q: What is a staged rollout and why use it?
A: Instead of releasing to all users at once, staged rollout deploys to a percentage of users first (10%). If crash rate or 1-star reviews spike, you halt the rollout and fix the issue before it affects everyone. At 30K users, a critical bug affecting 100% at launch = 30,000 bad experiences. At 10% rollout = 3,000, with the ability to halt before the remaining 90% update.
Q: How do you handle a critical bug that slipped into production?
A: If the bug is UI-only (not a security issue), we can push a patch via the backend (feature flags). If it requires app code change: halt the staged rollout, hotfix the code, fast-track a new release. Play Store allows expedited reviews for critical fixes. For iOS, Apple's App Store has an emergency review process. We also maintain the previous APK internally for sideloading to internal testers during the fix cycle.
Flutter's platform channel system bridges Dart code to native Android (Kotlin) or iOS (Swift) code — used for features unavailable in Flutter's SDK:
// Flutter (Dart) side
const platform = MethodChannel('com.voltvave.zoober/biometrics');
Future<bool> authenticateWithBiometrics() async {
try {
final bool authenticated = await platform.invokeMethod('authenticate');
return authenticated;
} on PlatformException catch (e) {
// Biometrics not available on device
return false;
}
}
// Android (Kotlin) side — in MainActivity.kt
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.voltvave.zoober/biometrics")
.setMethodCallHandler { call, result ->
if (call.method == "authenticate") {
// Use BiometricPrompt API
BiometricPrompt(this, ...).authenticate(promptInfo)
}
}
Used in Zoober for: biometric authentication (fingerprint/face ID), native payment SDKs (Juspay's native Android SDK for UPI intent flow), and background location tracking for riders.