Model Context Protocol (MCP)

What MCP is

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.

Without MCP (N×M integrations): Claude ──── custom code ────▶ MongoDB Claude ──── custom code ────▶ REST API Claude ──── custom code ────▶ File system GPT ──── custom code ────▶ MongoDB GPT ──── custom code ────▶ REST API ... With MCP (N+M integrations): Claude ─┐ ┌─▶ MongoDB MCP Server GPT ─┤── MCP Protocol ──┤─▶ REST API MCP Server Gemini ─┘ └─▶ File system MCP Server Any LLM speaks MCP → connects to any MCP server

MCP concepts

SSE vs STDIO Transports

TransportHow it worksUsed for
STDIOParent process communicates with MCP server child process via stdin/stdoutDesktop apps, CLI tools, local processes
SSELong-lived HTTP connection; server pushes events via Server-Sent EventsWeb, mobile, cloud-hosted MCP servers
STDIO transport (desktop/CLI): Flutter Desktop App │ spawns process ▼ mcp_client package ←──── stdin/stdout ────▶ MCP Server process (Dart) (Node.js or Python) SSE transport (web/mobile): Flutter Web / Mobile App │ HTTP GET /mcp/sse ▼ mcp_client package ←──── SSE events ────▶ MCP Server (HTTP) (Dart) (hosted on our backend) │ │ POST /mcp/messages (JSON-RPC) ▼ MCP Server processes tool call → returns result

Why both transports for 6-platform support

Interview Q&A

Q: 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.

Internal Packages: mcp_client & llm_shared

mcp_client (Dart package)

// 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});
  }
}

llm_shared (Dart package)

// 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)

BLoC State Management

Why BLoC over Provider or Riverpod?

BLoC pattern: UI Widget │ dispatches event │ e.g., OrderStatusRequested(orderId: 'abc') ▼ ┌─────────────────────────────┐ │ OrderBloc │ │ │ │ on<OrderStatusRequested> │ │ → fetch from repo │ │ → emit OrderLoaded(order) │ │ or OrderError(message) │ └─────────────────────────────┘ │ emits state ▼ BlocBuilder<OrderBloc, OrderState> → rebuilds only affected widgets
// 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); }

go_router — Declarative Routing

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
  ],
);
Interview Q&A

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.

4-App Flutter Ecosystem

Zoober 4-app architecture: Shared packages: ┌─────────────────────────────────────────────┐ │ domain/ — Order, Product, User models │ data/ — API clients, repositories │ auth/ — JWT handling, token refresh └─────────────────────────────────────────────┘ │ imported by all 4 apps ▼ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ Customer │ │ Merchant │ │ Rider │ │ Admin │ │ App │ │ App │ │ App │ │ Dashboard │ │ │ │ │ │ │ │ │ │ Browse │ │ Orders │ │ Pickup │ │ Analytics │ │ Order │ │ Catalog │ │ Navigate │ │ Store mgmt │ │ Track │ │ Inventory │ │ Deliver │ │ Users │ └────────────┘ └────────────┘ └────────────┘ └────────────┘ Android+iOS Android+iOS Android Web+Desktop +Web +Web

Why 4 separate apps instead of one?

15+ Releases — Release Process

Semantic versioning

Release checklist

Interview Q&A

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.

Platform Channels

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.