이 글은 Model Context Protocol(MCP) 시리즈의 여섯 번째 포스트이자 mcp_llm의 네 번째 글로, Flutter와 AI의 만남: LlmServer와 mcp_server 통합하기에 이어 다양한 LLM 제공자와 MCP 생태계를 통합하는 방법에 대해 심층적으로 알아봅니다.
현대 AI 애플리케이션 개발에서는 단일 LLM 제공자에 의존하기보다 여러 제공자의 장점을 활용하는 것이 중요해졌습니다. 각 제공자는 고유한 강점과 특성을 가지고 있어, 다양한 사용 사례에 최적화된 경험을 제공할 수 있습니다.
OpenAI (GPT 모델)
Anthropic (Claude 모델)
이러한 다양한 제공자를 MCP 생태계와 통합함으로써 애플리케이션은 각 제공자의 강점을 활용하면서도 일관된 인터페이스를 유지할 수 있습니다.
mcp_llm 패키지는 다양한 LLM 제공자의 기능을 추상화하는 LlmCapability 시스템을 제공합니다. 이 시스템은 각 제공자의 고유한 기능을 표준화된 MCP 도구로 매핑하는 데 중요한 역할을 합니다.
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 도구 생성과 제공자 선택에 활용합니다.
여러 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 제공자의 클라이언트를 초기화합니다.
// 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 클라이언트를 설정하고, 해당 제공자의 기능을 평가하여 저장합니다.
각 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;
}
}
이 메서드는 스트리밍 응답을 처리하고, 완료 또는 오류 시 제공자 상태를 업데이트합니다.
애플리케이션에서 LLM 제공자를 전환할 때도 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 제공자를 자동으로 선택하는 스마트 라우팅 기능을 구현해 보겠습니다.
// 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(),
);
}
}
이 메서드는 자동으로 최적의 제공자를 선택하고 쿼리를 실행합니다.
다양한 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 생태계와 통합하는 방법을 살펴보았습니다. 이 기능을 기반으로 다음과 같은 고급 주제를 탐색할 수 있습니다:
다양한 LLM 제공자를 MCP 생태계와 통합함으로써 각 제공자의 강점을 활용하면서도 일관된 개발 경험을 유지할 수 있습니다. MultiProviderManager와 같은 관리 클래스를 사용하면 여러 제공자를 효과적으로 관리하고, 쿼리 내용에 따라 최적의 제공자를 선택하며, 제공자 간 전환 시에도 MCP 연결을 유지할 수 있습니다.
Flutter 애플리케이션에서 이러한 통합을 구현함으로써 개발자는 다양한 AI 기능을 제공하면서도 코드 복잡성을 관리할 수 있습니다. MCP 프로토콜은 이러한 통합의 핵심으로, 다양한 제공자 간의 표준화된 인터페이스를 제공합니다.
다음 글에서는 MCP 플러그인 시스템을 구축하고 확장하는 방법에 대해 자세히 알아보겠습니다.
이 글이 도움이 되셨다면, 패트론을 통해 개발 활동을 지원해 주세요. 여러분의 후원은 더 많은 무료 콘텐츠를 만드는 데 큰 힘이 됩니다.
태그: #Flutter #AI #MCP #LLM #Dart #OpenAI #Claude #ModelContextProtocol #AIIntegration #MultiProvider