이 글은 Model Context Protocol(MCP) 시리즈의 다섯 번째 포스트이자 mcp_llm의 세 번째 글로, Flutter와 AI의 만남: LlmClient와 mcp_client 통합하기에 이어 LlmServer와 mcp_server의 통합에 대해 심층적으로 알아봅니다. MCP 생태계에서 서버 측 통합을 통해 AI 기능을 서비스로 제공하는 방법을 배워봅시다.
지난 글에서는 LlmClient와 mcp_client의 통합을 통해 클라이언트 측에서 AI 기능과 MCP 도구를 활용하는 방법을 살펴보았습니다. 이번에는 서버 측 통합인 LlmServer와 mcp_server의 통합에 초점을 맞추겠습니다.
LlmServer는 mcp_llm 패키지의 서버 측 핵심 컴포넌트로, AI 모델의 기능을 서비스로 제공하는 역할을 합니다. 주요 기능은 다음과 같습니다:
mcp_server는 Model Context Protocol(MCP)의 서버 구현체로, 다음과 같은 기능을 제공합니다:
LlmServer와 mcp_server의 통합은 AI 기능을 표준화된 방식으로 외부에 제공할 수 있게 합니다. 이 통합을 통해:
이러한 통합은 확장 가능하고 표준화된 AI 서비스 구축의 기반이 됩니다.
LlmServer와 mcp_server의 통합 아키텍처를 이해하는 것이 중요합니다. 이 통합은 다음과 같은 구조로 이루어집니다:
┌────────────────┐ ┌────────────────┐
│ LlmServer │◄─────►│ mcp_server │
└───────┬────────┘ └───────┬────────┘
│ │
▼ ▼
┌────────────────┐ ┌────────────────┐
│ LLM Provider │ │ MCP Clients │
│ (Claude, GPT) │ │ │
└────────────────┘ └────────────────┘
mcp_server가 클라이언트로부터 요청을 받습니다.LlmServer로 전달됩니다.LlmServer는 요청을 처리하고 LLM 제공자와 통신합니다.LlmServer로 반환됩니다.LlmServer는 응답을 처리하고 결과를 mcp_server로 전달합니다.mcp_server는 최종 결과를 클라이언트에게 반환합니다.서버 측 통합을 구성하는 핵심 컴포넌트들을 살펴보겠습니다:
이들 컴포넌트가 함께 작동하여 확장 가능한 AI 서비스 환경을 구축합니다.
이제 LlmServer와 mcp_server를 통합하는 서버 애플리케이션을 구현해보겠습니다. 다음은 완전한 서버 구현 코드입니다:
import 'dart:async';
import 'dart:io';
import 'package:mcp_llm/mcp_llm.dart';
import 'package:mcp_server/mcp_server.dart' as mcp;
import 'package:dotenv/dotenv.dart';
Future<void> main() async {
// 로거 설정 - 서버 로그를 구조적으로 기록하는 유틸리티
final logger = Logger.getLogger('mcp_llm.server');
try {
// 환경 변수 로드 - 서버 구성에 필요한 설정 가져오기
final env = DotEnv()..load();
final apiKey = env['OPENAI_API_KEY'] ?? '';
final serverPort = int.tryParse(env['MCP_SERVER_PORT'] ?? '8999') ?? 8999;
final authToken = env['MCP_AUTH_TOKEN'] ?? 'test_token';
final logLevelStr = env['LOG_LEVEL'] ?? 'info';
// 로그 레벨 설정 - 다양한 상세도로 로깅 가능
final LogLevel logLevel;
switch (logLevelStr.toLowerCase()) {
case 'trace': logLevel = LogLevel.trace; break;
case 'debug': logLevel = LogLevel.debug; break;
case 'info': logLevel = LogLevel.info; break;
case 'warning': logLevel = LogLevel.warning; break;
case 'error': logLevel = LogLevel.error; break;
default: logLevel = LogLevel.info;
}
logger.setLevel(logLevel);
// API 키 확인 - 필수 설정으로 없으면 서버 시작 불가
if (apiKey.isEmpty) {
logger.error('OPENAI_API_KEY가 설정되지 않았습니다.');
exit(1);
}
logger.info('AI 서비스 서버 시작 중...');
// McpLlm 인스턴스 생성 - 모든 LLM 기능의 진입점
final mcpLlm = McpLlm();
// LLM 프로바이더 등록 - 다양한 AI 모델 지원 가능
mcpLlm.registerProvider('openai', OpenAiProviderFactory());
logger.debug('OpenAI 프로바이더를 등록했습니다.');
// MCP 서버 생성 - Model Context Protocol 서버 설정
final mcpServer = mcp.McpServer.createServer(
name: 'ai_service',
version: '1.0.0',
capabilities: mcp.ServerCapabilities(
tools: true,
toolsListChanged: true,
resources: true,
resourcesListChanged: true,
prompts: true,
promptsListChanged: true,
sampling: true,
),
);
// 커스텀 도구 등록 - 특정 기능을 수행하는 도구 추가
final pluginManager = PluginManager();
await pluginManager.registerPlugin(EchoToolPlugin());
await pluginManager.registerPlugin(CalculatorToolPlugin());
// LlmServer 생성 - AI 기능을 서비스로 제공하는 서버
final llmServer = await mcpLlm.createServer(
providerName: 'openai', // 사용할 LLM 프로바이더
config: LlmConfiguration(
apiKey: apiKey,
model: 'gpt-4o', // 사용할 모델
options: {
'temperature': 0.3, // 응답의 무작위성/창의성 조절 (0~1)
'max_tokens': 2000, // 최대 출력 토큰 수
},
),
storageManager: MemoryStorage(),
pluginManager: pluginManager,
mcpServer: mcpServer, // MCP 서버 통합
);
logger.info('LlmServer가 생성되었습니다.');
// 코어 LLM 플러그인 등록 - 기본 AI 기능 활성화
await llmServer.registerCoreLlmPlugins(
registerCompletionTool: true, // 텍스트 생성 도구
registerStreamingTool: true, // 스트리밍 응답 도구
registerEmbeddingTool: true, // 임베딩 생성 도구
registerRetrievalTools: true, // 검색 관련 도구
registerWithServer: true, // MCP 서버에 자동 등록
);
logger.info('LLM 코어 플러그인이 등록되었습니다.');
// 자동 생성 도구 추가 - LLM이 도구를 자동으로 설계/구현
// 비동기 실행으로 변경하되 테스트 부분은 제거
_generateAutomaticTool(llmServer, logger);
// SSE 트랜스포트 생성 - 서버와 클라이언트 간 통신 채널
final transport = mcp.McpServer.createSseTransport(
endpoint: '/sse',
messagesEndpoint: '/message',
port: serverPort,
authToken: authToken, // 클라이언트 인증을 위한 토큰
);
// MCP 서버와 트랜스포트 연결
mcpServer.connect(transport);
logger.info('MCP 서버가 트랜스포트에 연결되었습니다.');
// 서버 정보 출력
logger.info('AI 서비스가 포트 $serverPort에서 실행 중입니다.');
logger.info('서버 URL: http://localhost:$serverPort/sse');
// 인증 토큰 설정 확인
if (authToken.isNotEmpty) {
logger.debug('인증 토큰이 설정되었습니다: $authToken');
}
// 도구 목록 출력
final tools = mcpServer.getTools();
logger.info('');
logger.info('사용 가능한 도구 목록:');
for (final tool in tools) {
logger.info('- ${tool.name}: ${tool.description}');
}
// 서버 중지 처리 - Ctrl+C 등으로 종료 시
ProcessSignal.sigint.watch().listen((_) async {
logger.info('서버 종료 중...');
await mcpLlm.shutdown();
exit(0);
});
// 이벤트 루프 유지 - 서버 계속 실행
try {
logger.info('서버가 요청을 처리할 준비가 되었습니다. Ctrl+C로 서버를 종료할 수 있습니다.');
await Future.delayed(Duration(days: 365));
} catch (e) {
logger.error('서버 실행 중 오류 발생: $e');
}
} catch (e, stack) {
logger.error('서버 시작 중 오류 발생: $e');
logger.debug('스택 트레이스: $stack');
exit(1);
}
}
// 자동 도구 생성 함수 - LLM을 활용한 도구 자동 설계 및 구현
void _generateAutomaticTool(LlmServer server, Logger logger) async {
try {
logger.info('감정 분석 도구 자동 생성 중...');
// 도구 설명 작성 - LLM이 이 설명을 기반으로 도구를 생성
final toolDescription = """
텍스트의 감정을 분석하는 도구를 만들어주세요. 이 도구는 다음 기능을 제공해야 합니다:
1. 입력된 텍스트의 감정 분석 (긍정, 부정, 중립)
2. 감정 점수 (-1.0부터 1.0까지, -1이 가장 부정적, 1이 가장 긍정적)
3. 주요 감정 단어 추출
4. 분석 신뢰도 (0.0부터 1.0까지)
다양한 언어의 텍스트를 지원해야 하며, 결과는 텍스트 형식 또는 JSON 형식으로 제공할 수 있어야 합니다.
도구 이름은 반드시 sentiment_analyzer로 설정해주세요.
""";
// LLM 기반 도구 자동 생성 시도
try {
logger.info('LLM 기반 도구 자동 생성 시도 중...');
// 도구 생성
final success = await server.generateAndRegisterTool(
toolDescription,
registerWithServer: true, // 서버에 자동 등록
);
logger.info('자동 도구 생성 결과: ${success ? "성공" : "실패"}');
if (!success) {
logger.info('자동 생성 실패...');
}
} catch (e) {
logger.error('도구 생성 중 오류 발생: $e');
// 오류 발생 시 직접 등록
logger.info('오류...');
}
} catch (e) {
logger.error('감정 분석 도구 설정 중 오류 발생: $e');
}
}
class EchoToolPlugin extends BaseToolPlugin {
EchoToolPlugin() : super(
name: 'echo',
version: '1.0.0',
description: 'Echoes back the input message with optional transformation',
inputSchema: {
'type': 'object',
'properties': {
'message': {
'type': 'string',
'description': 'Message to echo back'
},
'uppercase': {
'type': 'boolean',
'description': 'Whether to convert to uppercase',
'default': false
}
},
'required': ['message']
},
);
Future<LlmCallToolResult> onExecute(Map<String, dynamic> arguments) async {
final message = arguments['message'] as String;
final uppercase = arguments['uppercase'] as bool? ?? false;
final result = uppercase ? message.toUpperCase() : message;
Logger.getLogger('LlmServerDemo').debug(message);
return LlmCallToolResult([
LlmTextContent(text: result),
]);
}
}
class CalculatorToolPlugin extends BaseToolPlugin {
CalculatorToolPlugin() : super(
name: 'calculator',
version: '1.0.0',
description: 'Performs basic arithmetic operations',
inputSchema: {
'type': 'object',
'properties': {
'operation': {
'type': 'string',
'description': 'The operation to perform (add, subtract, multiply, divide)',
'enum': ['add', 'subtract', 'multiply', 'divide']
},
'a': {
'type': 'number',
'description': 'First number'
},
'b': {
'type': 'number',
'description': 'Second number'
}
},
'required': ['operation', 'a', 'b']
},
);
Future<LlmCallToolResult> onExecute(Map<String, dynamic> arguments) async {
final operation = arguments['operation'] as String;
final a = (arguments['a'] as num).toDouble();
final b = (arguments['b'] as num).toDouble();
double result;
switch (operation) {
case 'add':
result = a + b;
break;
case 'subtract':
result = a - b;
break;
case 'multiply':
result = a * b;
break;
case 'divide':
if (b == 0) {
throw Exception('Division by zero');
}
result = a / b;
break;
default:
throw Exception('Unknown operation: $operation');
}
Logger.getLogger('LlmServerDemo').debug('$result');
return LlmCallToolResult([
LlmTextContent(text: result.toString()),
]);
}
}
위 코드에서 볼 수 있듯이, 두 가지 방식으로 도구를 등록하고 있습니다:
PluginManager를 통해 플러그인을 등록하고 관리합니다.registerCoreLlmPlugins 메서드를 통해 기본 AI 기능을 도구로 등록합니다.위의 예시에서는 두 가지 커스텀 도구가 등록되어 있습니다:
echo: 메시지를 그대로 반환하거나 대문자로 변환하는 간단한 도구calculator: 기본 산술 연산을 수행하는 계산기 도구이러한 도구들은 클라이언트가 MCP 프로토콜을 통해 호출할 수 있습니다.
커스텀 도구를 구현하기 위해서는 BaseToolPlugin 클래스를 상속하여 구현합니다:
class EchoToolPlugin extends BaseToolPlugin {
EchoToolPlugin() : super(
name: 'echo',
version: '1.0.0',
description: 'Echoes back the input message with optional transformation',
inputSchema: {
'type': 'object',
'properties': {
'message': {
'type': 'string',
'description': 'Message to echo back'
},
'uppercase': {
'type': 'boolean',
'description': 'Whether to convert to uppercase',
'default': false
}
},
'required': ['message']
},
);
Future<LlmCallToolResult> onExecute(Map<String, dynamic> arguments) async {
final message = arguments['message'] as String;
final uppercase = arguments['uppercase'] as bool? ?? false;
final result = uppercase ? message.toUpperCase() : message;
Logger.getLogger('LlmServerDemo').debug(message);
return LlmCallToolResult([
LlmTextContent(text: result),
]);
}
}
각 플러그인은 다음 요소를 정의해야 합니다:
코어 LLM 플러그인은 registerCoreLlmPlugins 메서드를 통해 등록됩니다:
// 코어 LLM 플러그인 등록 - 기본 AI 기능 활성화
await llmServer.registerCoreLlmPlugins(
registerCompletionTool: true, // 텍스트 생성 도구
registerStreamingTool: true, // 스트리밍 응답 도구
registerEmbeddingTool: true, // 임베딩 생성 도구
registerRetrievalTools: true, // 검색 관련 도구
registerWithServer: true, // MCP 서버에 자동 등록
);
이 메서드는 다음과 같은 플러그인들을 등록합니다:
이러한 플러그인들은 LLM의 핵심 기능을 MCP 도구로 노출하여 클라이언트가 사용할 수 있게 합니다.
코드의 가장 흥미로운 부분 중 하나는 LLM을 활용하여 도구를 자동으로 생성하는 _generateAutomaticTool 함수입니다:
// 자동 도구 생성 함수 - LLM을 활용한 도구 자동 설계 및 구현
void _generateAutomaticTool(LlmServer server, Logger logger) async {
try {
logger.info('감정 분석 도구 자동 생성 중...');
// 도구 설명 작성 - LLM이 이 설명을 기반으로 도구를 생성
final toolDescription = """
텍스트의 감정을 분석하는 도구를 만들어주세요. 이 도구는 다음 기능을 제공해야 합니다:
1. 입력된 텍스트의 감정 분석 (긍정, 부정, 중립)
2. 감정 점수 (-1.0부터 1.0까지, -1이 가장 부정적, 1이 가장 긍정적)
3. 주요 감정 단어 추출
4. 분석 신뢰도 (0.0부터 1.0까지)
다양한 언어의 텍스트를 지원해야 하며, 결과는 텍스트 형식 또는 JSON 형식으로 제공할 수 있어야 합니다.
도구 이름은 반드시 sentiment_analyzer로 설정해주세요.
""";
// LLM 기반 도구 자동 생성 시도
try {
logger.info('LLM 기반 도구 자동 생성 시도 중...');
// 도구 생성
final success = await server.generateAndRegisterTool(
toolDescription,
registerWithServer: true, // 서버에 자동 등록
);
logger.info('자동 도구 생성 결과: ${success ? "성공" : "실패"}');
if (!success) {
logger.info('자동 생성 실패...');
}
} catch (e) {
logger.error('도구 생성 중 오류 발생: $e');
// 오류 발생 시 로깅
logger.info('오류...');
}
} catch (e) {
logger.error('감정 분석 도구 설정 중 오류 발생: $e');
}
}
이 함수는 자연어 설명만으로 LLM이 도구를 자동으로 설계하고 구현하도록 합니다. 이를 통해 개발자는 복잡한 도구를 간단하게 생성할 수 있습니다.
서버의 안정적인 운영을 위해 다음과 같은 기능들이 구현되어 있습니다:
Logger 클래스를 사용하여 다양한 레벨의 로그를 기록합니다.// 서버 중지 처리 - Ctrl+C 등으로 종료 시
ProcessSignal.sigint.watch().listen((_) async {
logger.info('서버 종료 중...');
await mcpLlm.shutdown();
exit(0);
});
또한 서버 시작 시 도구 목록을 출력하여 현재 상태를 확인할 수 있습니다:
// 도구 목록 출력
final tools = mcpServer.getTools();
logger.info('');
logger.info('사용 가능한 도구 목록:');
for (final tool in tools) {
logger.info('- ${tool.name}: ${tool.description}');
}
이 글에서는 LlmServer와 mcp_server의 통합에 대해 자세히 살펴보았습니다. 주요 내용을 요약하자면:
다음 단계로 탐색할 수 있는 주제들은 다음과 같습니다:
이 글에서는 AI 기능을 서버 측에서 제공하기 위한 LlmServer와 mcp_server의 통합에 대해 살펴보았습니다. 두 컴포넌트의 통합을 통해 다음과 같은 주요 기능을 구현할 수 있게 되었습니다:
서버 측 통합은 확장 가능하고 관리하기 쉬운 AI 서비스 아키텍처를 구축하는 데 있어 핵심적인 부분입니다. 이전 글에서 살펴본 LlmClient와 mcp_client의 클라이언트 측 통합과 함께, 완전한 MCP 생태계를 구축하여 강력한 AI 애플리케이션을 개발할 수 있습니다.
다음 글에서는 다양한 LLM 제공자를 MCP 생태계와 통합하는 방법에 대해 자세히 알아보겠습니다. 각 제공자의 특성과 장단점, 그리고 Model Context Protocol을 활용한 통합 전략을 살펴볼 예정입니다.
이 글이 도움이 되셨다면, 패트론을 통해 개발 활동을 지원해 주세요. 여러분의 후원은 더 많은 무료 콘텐츠를 만드는 데 큰 힘이 됩니다.
태그: #Flutter #AI #MCP #LLM #Dart #Claude #OpenAI #ModelContextProtocol #AIIntegration #mcp_server #LlmServer