Flutter와 AI의 만남: 다양한 LLM 제공자와 MCP 통합하기

MCP Dev Studio·2025년 5월 1일

이 글은 Model Context Protocol(MCP) 시리즈의 여섯 번째 포스트이자 mcp_llm의 네 번째 글로, Flutter와 AI의 만남: LlmServer와 mcp_server 통합하기에 이어 다양한 LLM 제공자와 MCP 생태계를 통합하는 방법에 대해 심층적으로 알아봅니다.

Flutter와 다양한 AI 제공자 통합

목차

다양한 LLM 제공자 통합의 이점

현대 AI 애플리케이션 개발에서는 단일 LLM 제공자에 의존하기보다 여러 제공자의 장점을 활용하는 것이 중요해졌습니다. 각 제공자는 고유한 강점과 특성을 가지고 있어, 다양한 사용 사례에 최적화된 경험을 제공할 수 있습니다.

주요 LLM 제공자별 특성

OpenAI (GPT 모델)

  • 코드 생성 및 디버깅에 탁월
  • 도구 호출(function calling) 기능이 잘 구현됨
  • 다양한 도메인 지식을 갖춤

Anthropic (Claude 모델)

  • 매우 긴 컨텍스트 처리 가능 (최대 200K 토큰)
  • 복잡한 문서 분석 능력
  • 안전성과 편향 제어에 중점

이러한 다양한 제공자를 MCP 생태계와 통합함으로써 애플리케이션은 각 제공자의 강점을 활용하면서도 일관된 인터페이스를 유지할 수 있습니다.

LlmCapability 시스템과 MCP 기능 매핑

mcp_llm 패키지는 다양한 LLM 제공자의 기능을 추상화하는 LlmCapability 시스템을 제공합니다. 이 시스템은 각 제공자의 고유한 기능을 표준화된 MCP 도구로 매핑하는 데 중요한 역할을 합니다.

LlmCapability의 주요 구성 요소

class ProviderCapabilities {
  final bool supportsStreaming;
  final bool supportsToolCalls;
  final int maxContextLength;
  final Map<String, dynamic> specificCapabilities;

  ProviderCapabilities({
    required this.supportsStreaming,
    required this.supportsToolCalls,
    required this.maxContextLength,
    required this.specificCapabilities,
  });
}

이 클래스는 LLM 제공자의 기능을 표현하며, MCP 도구 생성과 기능 활성화에 사용됩니다. specificCapabilities 맵은 제공자별 고유 기능을 저장합니다.

제공자 기능 평가 구현

// Assess provider capabilities
ProviderCapabilities _assessProviderCapabilities(String providerName) {
  final capabilities = ProviderCapabilities(
    supportsStreaming: true,
    supportsToolCalls: true,
    maxContextLength: _getMaxContextLength(providerName),
    specificCapabilities: {},
  );

  // Add provider-specific capabilities
  switch (providerName.toLowerCase()) {
    case 'openai':
      capabilities.specificCapabilities['vision'] = true;
      capabilities.specificCapabilities['functionCalling'] = true;
      capabilities.specificCapabilities['codeCompletion'] = true;
      break;
    case 'claude':
      capabilities.specificCapabilities['vision'] = true;
      capabilities.specificCapabilities['documentAnalysis'] = true;
      capabilities.specificCapabilities['longContext'] = true;
      break;
  }

  return capabilities;
}

이 함수는 제공자 이름을 기반으로 해당 제공자의 기능을 평가하고, 이를 MCP 도구 생성과 제공자 선택에 활용합니다.

MultiProviderManager 구현하기

여러 LLM 제공자를 효과적으로 관리하기 위해 MultiProviderManager 클래스를 구현했습니다. 이 클래스는 다양한 제공자를 등록하고, 상태를 관리하며, 요청을 적절한 제공자로 라우팅하는 역할을 합니다.

기본 구조

class MultiProviderManager {
  // Core components
  late McpLlm _mcpLlm;
  mcp.Client? _mcpClient;
  final Map<String, LlmClient> _llmClients = {};

  // State streams
  final _connectionStateController = StreamController<bool>.broadcast();
  final _providerStateController = StreamController<Map<String, ProviderStatus>>.broadcast();
  
  // Provider capabilities
  final Map<String, ProviderCapabilities> _providerCapabilities = {};
  
  // Public access to state streams
  Stream<bool> get connectionState => _connectionStateController.stream;
  Stream<Map<String, ProviderStatus>> get providerStatus => _providerStateController.stream;
  
  // Available providers
  List<String> get availableProviders => _llmClients.keys.toList();
  
  // Constructor
  MultiProviderManager() {
    _mcpLlm = McpLlm();
    _mcpLlm.registerProvider('openai', OpenAiProviderFactory());
    _mcpLlm.registerProvider('claude', ClaudeProviderFactory());
  }
  
  // Additional methods...
}

이 클래스는 여러 LLM 제공자를 등록하고, MCP 클라이언트와 통합하며, 제공자의 상태를 모니터링하는 기능을 제공합니다.

초기화 및 설정

// Setup MCP and LLM providers
Future<void> initialize({
  required String mcpServerUrl,
  String? mcpAuthToken,
  String? openaiApiKey,
  String? claudeApiKey,
}) async {
  try {
    // Setup MCP client
    await _setupMcpClient(mcpServerUrl, mcpAuthToken);

    // Setup OpenAI client if API key is provided
    if (openaiApiKey != null && openaiApiKey.isNotEmpty) {
      await _setupLlmClient('openai', openaiApiKey);
    }

    // Setup Claude client if API key is provided
    if (claudeApiKey != null && claudeApiKey.isNotEmpty) {
      await _setupLlmClient('claude', claudeApiKey);
    }

    // Update connection state
    _updateConnectionState();
  } catch (e) {
    rethrow;
  }
}

이 메서드는 MCP 클라이언트를 설정하고, 제공된 API 키를 사용하여 각 LLM 제공자의 클라이언트를 초기화합니다.

제공자별 LLM 클라이언트 설정

// Setup LLM client for a provider
Future<void> _setupLlmClient(String providerName, String apiKey) async {
  try {
    // Get default model for the provider
    final model = _getDefaultModel(providerName);

    // Create LLM client with MCP integration
    final llmClient = await _mcpLlm.createClient(
      providerName: providerName,
      config: LlmConfiguration(
        apiKey: apiKey,
        model: model,
        options: {
          'temperature': 0.7,
          'max_tokens': 1500,
        },
      ),
      mcpClient: _mcpClient,
      systemPrompt: 'You are a helpful assistant with access to various tools. Provide concise and accurate responses.',
    );

    // Store the client
    _llmClients[providerName] = llmClient;

    // Store provider capabilities
    _providerCapabilities[providerName] = _assessProviderCapabilities(providerName);

    // Update provider status
    _updateProviderStatus(providerName, ProviderStatus.ready);
  } catch (e) {
    _updateProviderStatus(providerName, ProviderStatus.error, error: e.toString());
  }
}

이 메서드는 특정 제공자에 대한 LLM 클라이언트를 설정하고, 해당 제공자의 기능을 평가하여 저장합니다.

제공자별 MCP 도구 구현 차이 처리

각 LLM 제공자는 도구 호출 방식과 기능 구현에 차이가 있습니다. 예를 들어, OpenAI는 'function calling'을, Claude는 보다 일반적인 도구 호출 메커니즘을 사용합니다. 이러한 차이를 효과적으로 처리하기 위한 방법을 살펴보겠습니다.

제공자별 채팅 구현

// Send chat message to provider
Future<LlmResponse> chat(String provider, String message, {bool enableTools = true}) async {
  final client = _llmClients[provider];
  if (client == null) {
    throw Exception('Provider $provider not available');
  }

  try {
    _updateProviderStatus(provider, ProviderStatus.processing);

    final response = await client.chat(
      message,
      enableTools: enableTools,
    );

    _updateProviderStatus(provider, ProviderStatus.ready);
    return response;
  } catch (e) {
    _updateProviderStatus(provider, ProviderStatus.error, error: e.toString());
    rethrow;
  }
}

이 메서드는 선택된 제공자를 사용하여 채팅 메시지를 처리하고, 제공자의 상태를 적절히 업데이트합니다.

스트리밍 응답 처리

// Stream chat responses
Stream<LlmResponseChunk> streamChat(String provider, String message, {bool enableTools = true}) {
  final client = _llmClients[provider];
  if (client == null) {
    throw Exception('Provider $provider not available');
  }

  try {
    _updateProviderStatus(provider, ProviderStatus.processing);

    final responseStream = client.streamChat(
      message,
      enableTools: enableTools,
    );

    // Transform stream to update status when complete
    final transformedStream = responseStream.transform(
        StreamTransformer<LlmResponseChunk, LlmResponseChunk>.fromHandlers(
            handleData: (data, sink) {
              sink.add(data);

              if (data.isDone == true) {
                _updateProviderStatus(provider, ProviderStatus.ready);
              }
            },
            handleError: (error, stackTrace, sink) {
              _updateProviderStatus(provider, ProviderStatus.error, error: error.toString());
              sink.addError(error, stackTrace);
            },
            handleDone: (sink) {
              _updateProviderStatus(provider, ProviderStatus.ready);
              sink.close();
            }
        )
    );

    return transformedStream;
  } catch (e) {
    _updateProviderStatus(provider, ProviderStatus.error, error: e.toString());
    rethrow;
  }
}

이 메서드는 스트리밍 응답을 처리하고, 완료 또는 오류 시 제공자 상태를 업데이트합니다.

제공자 전환 시 MCP 연결 유지하기

애플리케이션에서 LLM 제공자를 전환할 때도 MCP 연결과 컨텍스트를 유지하는 것이 중요합니다. 이를 위한 구현을 살펴보겠습니다.

MCP 클라이언트 설정

// Setup MCP client connection
Future<void> _setupMcpClient(String serverUrl, String? authToken) async {
  try {
    // Create MCP client instance
    _mcpClient = mcp.McpClient.createClient(
      name: 'multi_provider_app',
      version: '1.0.0',
      capabilities: const mcp.ClientCapabilities(
        roots: true,
        rootsListChanged: true,
        sampling: true,
      ),
    );

    // Create transport for MCP connection
    final headers = authToken != null && authToken.isNotEmpty
        ? {'Authorization': 'Bearer $authToken'}
        : null;

    final transport = await mcp.McpClient.createSseTransport(
      serverUrl: serverUrl,
      headers: headers,
    );

    // Setup connection state change event handler
    _mcpClient!.onNotification('connection_state_changed', (params) {
      _updateConnectionState();
    });

    // Connect to MCP server with retry
    await _mcpClient!.connectWithRetry(
      transport,
      maxRetries: 3,
      delay: const Duration(seconds: 2),
    );

    // Update initial connection state
    _updateConnectionState();
  } catch (e) {
    rethrow;
  }
}

이 메서드는 MCP 클라이언트를 설정하고, 연결 상태 변경을 모니터링합니다.

여러 제공자 간 실행

// Execute query across multiple providers
Future<Map<String, LlmResponse>> executeAcrossProviders(
    String query,
    {
      List<String>? providers,
      bool enableTools = true,
    }
    ) async {
  final targetProviders = providers ?? _llmClients.keys.toList();

  final futures = <String, Future<LlmResponse>>{};
  for (final provider in targetProviders) {
    if (_llmClients.containsKey(provider)) {
      futures[provider] = chat(provider, query, enableTools: enableTools);
    }
  }

  final responses = <String, LlmResponse>{};
  for (final provider in futures.keys) {
    try {
      responses[provider] = await futures[provider]!;
    } catch (_) {
      // Continue with other providers even if one fails
    }
  }

  return responses;
}

이 메서드는 동일한 쿼리를 여러 제공자에게 실행하고 결과를 비교할 수 있게 합니다.

스마트 LLM 라우팅 구현하기

쿼리 내용에 따라 가장 적합한 LLM 제공자를 자동으로 선택하는 스마트 라우팅 기능을 구현해 보겠습니다.

최적 제공자 선택

// Select best provider for a query
String selectProviderForQuery(String query, {Set<String>? requiredCapabilities}) {
  if (_llmClients.isEmpty) {
    throw Exception('No LLM clients available');
  }

  // Code-related queries
  if (query.toLowerCase().contains('code') ||
      query.toLowerCase().contains('programming') ||
      query.toLowerCase().contains('function')) {
    if (_llmClients.containsKey('openai')) {
      return 'openai';
    }
  }

  // Creative content
  if (query.toLowerCase().contains('story') ||
      query.toLowerCase().contains('creative') ||
      query.toLowerCase().contains('write a')) {
    if (_llmClients.containsKey('claude')) {
      return 'claude';
    }
  }

  // Check for required capabilities
  if (requiredCapabilities != null && requiredCapabilities.isNotEmpty) {
    for (final provider in _llmClients.keys) {
      final capabilities = _providerCapabilities[provider];
      if (capabilities == null) continue;

      bool hasAllCapabilities = true;
      for (final capability in requiredCapabilities) {
        if (capabilities.specificCapabilities[capability] != true) {
          hasAllCapabilities = false;
          break;
        }
      }

      if (hasAllCapabilities) {
        return provider;
      }
    }
  }

  // Default to ready provider from current status
  for (final provider in _llmClients.keys) {
    if (_providerStatus[provider] == ProviderStatus.ready) {
      return provider;
    }
  }

  // Fallback to first provider
  return _llmClients.keys.first;
}

이 메서드는 쿼리 내용과 필요한 기능을 기반으로 최적의 제공자를 선택합니다.

스마트 실행 구현

// Auto-select provider and execute query
Future<ProviderResponse> smartExecute(
    String query,
    {
      Set<String>? requiredCapabilities,
      bool enableTools = true,
    }
    ) async {
  final provider = selectProviderForQuery(query, requiredCapabilities: requiredCapabilities);

  try {
    final response = await chat(provider, query, enableTools: enableTools);
    return ProviderResponse(
      provider: provider,
      response: response,
      error: null,
    );
  } catch (e) {
    return ProviderResponse(
      provider: provider,
      response: null,
      error: e.toString(),
    );
  }
}

이 메서드는 자동으로 최적의 제공자를 선택하고 쿼리를 실행합니다.

통합 UI 컴포넌트 개발

다양한 LLM 제공자를 통합하는 UI를 구현해 보겠습니다. 이 UI는 사용자가 제공자를 선택하고, 응답을 비교하며, 실시간 상태를 확인할 수 있게 합니다.

멀티 프로바이더 채팅 화면

class MultiProviderChatScreen extends StatefulWidget {
  const MultiProviderChatScreen({Key? key}) : super(key: key);

  
  State<MultiProviderChatScreen> createState() => _MultiProviderChatScreenState();
}

class _MultiProviderChatScreenState extends State<MultiProviderChatScreen> {
  final TextEditingController _textController = TextEditingController();
  final List<ChatMessage> _messages = [];

  late MultiProviderManager _providerManager;
  String _selectedProvider = '';
  bool _isLoading = true;
  bool _isStreaming = false;
  bool _mcpConnected = false;
  Map<String, ProviderStatus> _providerStatus = {};

  
  void initState() {
    super.initState();
    logger.debug('Initializing chat screen');
    _initialize();
  }

  // Initialize the manager
  Future<void> _initialize() async {
    try {
      // Manager setup and initialization
      // ...
    } catch (e) {
      // Error handling
      // ...
    }
  }

  // Handle message sending
  void _sendMessage(String text) async {
    if (text.trim().isEmpty) return;

    // Various message handling methods
    // ...
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Multi-Provider AI Chat'),
        actions: [
          // Provider selector dropdown
          // ...
          // MCP connection status indicator
          // ...
        ],
      ),
      body: Column(
        children: [
          // Message list
          // ...
          // Progress indicator
          // ...
          // Text input
          // ...
        ],
      ),
    );
  }
}

이 화면은 다양한 LLM 제공자와 상호작용하고, 상태를 시각적으로 표시하며, 다양한 명령어를 처리합니다.

제공자 상태 표시

// Provider selector
DropdownButton<String>(
  value: _selectedProvider.isEmpty ? null : _selectedProvider,
  hint: const Text('Select Provider'),
  onChanged: (String? newValue) {
    if (newValue != null) {
      logger.info('User selected provider: $newValue');
      setState(() {
        _selectedProvider = newValue;
      });
    }
  },
  items: _providerManager.availableProviders
      .map<DropdownMenuItem<String>>((String value) {
    // Show provider status with icon
    IconData iconData;
    Color iconColor;

    switch (_providerStatus[value]) {
      case ProviderStatus.ready:
        iconData = Icons.check_circle;
        iconColor = Colors.green;
        break;
      case ProviderStatus.processing:
        iconData = Icons.hourglass_top;
        iconColor = Colors.orange;
        break;
      case ProviderStatus.error:
        iconData = Icons.error;
        iconColor = Colors.red;
        break;
      case ProviderStatus.initializing:
        iconData = Icons.pending;
        iconColor = Colors.blue;
        break;
      case ProviderStatus.unknown:
      default:
        iconData = Icons.help;
        iconColor = Colors.grey;
        break;
    }

    return DropdownMenuItem<String>(
      value: value,
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(iconData, color: iconColor, size: 16),
          const SizedBox(width: 8),
          Text(value),
        ],
      ),
    );
  }).toList(),
),

이 드롭다운은 사용 가능한 제공자 목록과 각 제공자의 현재 상태를 시각적으로 표시합니다.

명령어 처리

UI는 다음과 같은 특수 명령어를 처리합니다:

  • /provider [name]: 특정 제공자로 전환
  • /compare [query]: 여러 제공자에서 동일한 쿼리 실행 및 비교
  • /stream [query]: 스트리밍 응답 활성화
  • /smart [query]: 자동 제공자 선택 및 실행

다음 단계

이 글에서는 다양한 LLM 제공자를 MCP 생태계와 통합하는 방법을 살펴보았습니다. 이 기능을 기반으로 다음과 같은 고급 주제를 탐색할 수 있습니다:

  1. MCP 플러그인 시스템 구축하기: 복잡한 도구와 리소스 플러그인 개발 및 MCP 생태계와 통합
  2. 멀티 MCP 환경 구성 및 관리: 여러 MCP 클라이언트/서버로 구성된 분산 환경 설계
  3. MCP 기반 병렬 처리 시스템: MCP 도구를 활용한 병렬 태스크 처리 및 결과 집계
  4. MCP 기반 RAG 시스템: 문서 검색과 LLM 응답을 통합한 지식 기반 시스템 구축

결론

다양한 LLM 제공자를 MCP 생태계와 통합함으로써 각 제공자의 강점을 활용하면서도 일관된 개발 경험을 유지할 수 있습니다. MultiProviderManager와 같은 관리 클래스를 사용하면 여러 제공자를 효과적으로 관리하고, 쿼리 내용에 따라 최적의 제공자를 선택하며, 제공자 간 전환 시에도 MCP 연결을 유지할 수 있습니다.

Flutter 애플리케이션에서 이러한 통합을 구현함으로써 개발자는 다양한 AI 기능을 제공하면서도 코드 복잡성을 관리할 수 있습니다. MCP 프로토콜은 이러한 통합의 핵심으로, 다양한 제공자 간의 표준화된 인터페이스를 제공합니다.

다음 글에서는 MCP 플러그인 시스템을 구축하고 확장하는 방법에 대해 자세히 알아보겠습니다.


참고 자료


개발자 후원하기

이 글이 도움이 되셨다면, 패트론을 통해 개발 활동을 지원해 주세요. 여러분의 후원은 더 많은 무료 콘텐츠를 만드는 데 큰 힘이 됩니다.

Support on Patreon

태그: #Flutter #AI #MCP #LLM #Dart #OpenAI #Claude #ModelContextProtocol #AIIntegration #MultiProvider

0개의 댓글