Flutter와 AI의 만남: LlmServer와 mcp_server 통합하기

MCP Dev Studio·2025년 4월 30일

이 글은 Model Context Protocol(MCP) 시리즈의 다섯 번째 포스트이자 mcp_llm의 세 번째 글로, Flutter와 AI의 만남: LlmClient와 mcp_client 통합하기에 이어 LlmServer와 mcp_server의 통합에 대해 심층적으로 알아봅니다. MCP 생태계에서 서버 측 통합을 통해 AI 기능을 서비스로 제공하는 방법을 배워봅시다.

Flutter와 AI 서버 통합

목차

LlmServer와 mcp_server의 역할

지난 글에서는 LlmClientmcp_client의 통합을 통해 클라이언트 측에서 AI 기능과 MCP 도구를 활용하는 방법을 살펴보았습니다. 이번에는 서버 측 통합인 LlmServermcp_server의 통합에 초점을 맞추겠습니다.

LlmServer란?

LlmServermcp_llm 패키지의 서버 측 핵심 컴포넌트로, AI 모델의 기능을 서비스로 제공하는 역할을 합니다. 주요 기능은 다음과 같습니다:

  • LLM 기반 쿼리 처리 및 응답 생성
  • 로컬 도구 등록 및 실행
  • 기본 플러그인 관리 및 활용
  • LLM 기반 도구 자동 생성
  • 요청 및 성능 모니터링

mcp_server란?

mcp_server는 Model Context Protocol(MCP)의 서버 구현체로, 다음과 같은 기능을 제공합니다:

  • 도구(Tools) 등록 및 제공
  • 리소스(Resources) 관리 및 제공
  • 프롬프트(Prompts) 템플릿 관리 및 제공
  • 클라이언트 연결 및 세션 관리
  • 요청 처리 및 응답 생성

통합의 의미

LlmServermcp_server의 통합은 AI 기능을 표준화된 방식으로 외부에 제공할 수 있게 합니다. 이 통합을 통해:

  1. LLM 기능을 MCP 프로토콜을 통해 제공할 수 있음
  2. LLM 기반 도구를 자동으로 생성하고 등록할 수 있음
  3. 코어 LLM 플러그인을 서비스로 제공할 수 있음
  4. 다양한 클라이언트에서 일관된 방식으로 AI 기능을 활용할 수 있음

이러한 통합은 확장 가능하고 표준화된 AI 서비스 구축의 기반이 됩니다.

서버 측 통합 아키텍처 이해하기

LlmServermcp_server의 통합 아키텍처를 이해하는 것이 중요합니다. 이 통합은 다음과 같은 구조로 이루어집니다:

통합 모델

┌────────────────┐       ┌────────────────┐
│   LlmServer    │◄─────►│   mcp_server   │
└───────┬────────┘       └───────┬────────┘
        │                        │
        ▼                        ▼
┌────────────────┐       ┌────────────────┐
│  LLM Provider  │       │  MCP Clients   │
│  (Claude, GPT) │       │                │
└────────────────┘       └────────────────┘

통신 흐름

  1. mcp_server가 클라이언트로부터 요청을 받습니다.
  2. 요청이 LLM 관련 도구에 대한 것이면 LlmServer로 전달됩니다.
  3. LlmServer는 요청을 처리하고 LLM 제공자와 통신합니다.
  4. LLM의 응답이 LlmServer로 반환됩니다.
  5. LlmServer는 응답을 처리하고 결과를 mcp_server로 전달합니다.
  6. mcp_server는 최종 결과를 클라이언트에게 반환합니다.

핵심 컴포넌트

서버 측 통합을 구성하는 핵심 컴포넌트들을 살펴보겠습니다:

  1. LlmServer: AI 모델 기능을 서비스로 제공합니다.
  2. mcp_server: MCP 프로토콜을 통해 클라이언트와 통신합니다.
  3. McpLlm: LLM 프로바이더 등록 및 서버 생성을 관리합니다.
  4. PluginManager: 등록된 플러그인을 관리합니다.
  5. BaseToolPlugin: 도구 플러그인의 기본 클래스를 제공합니다.
  6. Logger: 서버 로그를 구조적으로 기록하는 유틸리티입니다.

이들 컴포넌트가 함께 작동하여 확장 가능한 AI 서비스 환경을 구축합니다.

LlmServer와 mcp_server 설정하기

이제 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()),
    ]);
  }
}

LLM 기능을 MCP 도구로 등록하기

위 코드에서 볼 수 있듯이, 두 가지 방식으로 도구를 등록하고 있습니다:

  1. 플러그인 시스템: PluginManager를 통해 플러그인을 등록하고 관리합니다.
  2. 코어 LLM 플러그인: 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),
    ]);
  }
}

각 플러그인은 다음 요소를 정의해야 합니다:

  • name: 도구의 이름
  • version: 도구의 버전
  • description: 도구의 설명
  • inputSchema: 도구의 입력 스키마 (JSON Schema 형식)
  • onExecute: 도구 실행 시 호출되는 함수

코어 LLM 플러그인 등록 및 활용

코어 LLM 플러그인은 registerCoreLlmPlugins 메서드를 통해 등록됩니다:

// 코어 LLM 플러그인 등록 - 기본 AI 기능 활성화
await llmServer.registerCoreLlmPlugins(
  registerCompletionTool: true,  // 텍스트 생성 도구
  registerStreamingTool: true,   // 스트리밍 응답 도구
  registerEmbeddingTool: true,   // 임베딩 생성 도구
  registerRetrievalTools: true,  // 검색 관련 도구
  registerWithServer: true,      // MCP 서버에 자동 등록
);

이 메서드는 다음과 같은 플러그인들을 등록합니다:

  • CompletionTool: 텍스트 생성 도구
  • StreamingTool: 스트리밍 응답 도구
  • EmbeddingTool: 임베딩 생성 도구
  • RetrievalTools: 검색 관련 도구

이러한 플러그인들은 LLM의 핵심 기능을 MCP 도구로 노출하여 클라이언트가 사용할 수 있게 합니다.

LLM 기반 도구 자동 생성하기

코드의 가장 흥미로운 부분 중 하나는 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이 도구를 자동으로 설계하고 구현하도록 합니다. 이를 통해 개발자는 복잡한 도구를 간단하게 생성할 수 있습니다.

서버 상태 관리 및 모니터링

서버의 안정적인 운영을 위해 다음과 같은 기능들이 구현되어 있습니다:

  1. 로깅 시스템: Logger 클래스를 사용하여 다양한 레벨의 로그를 기록합니다.
  2. 오류 처리: 예외 처리를 통해 서버 안정성을 확보합니다.
  3. 종료 처리: 시그널 핸들링을 통해 안전한 종료를 구현합니다.
// 서버 중지 처리 - 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}');
}

다음 단계

이 글에서는 LlmServermcp_server의 통합에 대해 자세히 살펴보았습니다. 주요 내용을 요약하자면:

  • LlmServer와 mcp_server의 통합 아키텍처를 이해했습니다.
  • MCP 서버를 설정하고 LlmServer와 연결하는 방법을 배웠습니다.
  • LLM 기능을 MCP 도구로 등록하는 방법을 살펴보았습니다.
  • 코어 LLM 플러그인을 등록하고 활용하는 방법을 배웠습니다.
  • LLM 기반 도구 자동 생성 기능을 활용했습니다.
  • 서버 상태 관리 및 모니터링 방법을 구현했습니다.

다음 단계로 탐색할 수 있는 주제들은 다음과 같습니다:

  1. 다양한 LLM 제공자와 MCP 통합하기: Claude, OpenAI, Together AI 등 다양한 LLM 제공자를 MCP 생태계와 통합하는 방법
  2. MCP 플러그인 시스템 구축하기: 복잡한 도구와 리소스 플러그인 개발 및 MCP 서버와 통합하는 방법
  3. 멀티 MCP 환경 구축 및 관리하기: 여러 MCP 서버와 클라이언트로 구성된 분산 환경 설계 및 관리 방법
  4. MCP 기반 병렬 처리 및 태스크 실행: MCP 도구를 활용한 병렬 처리 및 복잡한 워크플로우 구현 방법
  5. MCP 기반 RAG 시스템 구현하기: 문서 검색과 AI 응답을 통합한 지식 기반 시스템 구축 방법

마무리

이 글에서는 AI 기능을 서버 측에서 제공하기 위한 LlmServermcp_server의 통합에 대해 살펴보았습니다. 두 컴포넌트의 통합을 통해 다음과 같은 주요 기능을 구현할 수 있게 되었습니다:

  • LLM 기능을 MCP 프로토콜을 통해 표준화된 방식으로 제공
  • LLM 기반 도구를 생성하고 클라이언트에 제공
  • 코어 LLM 플러그인 등록 및 확장
  • 서버 상태 관리 및 모니터링을 통한 안정성 확보

서버 측 통합은 확장 가능하고 관리하기 쉬운 AI 서비스 아키텍처를 구축하는 데 있어 핵심적인 부분입니다. 이전 글에서 살펴본 LlmClientmcp_client의 클라이언트 측 통합과 함께, 완전한 MCP 생태계를 구축하여 강력한 AI 애플리케이션을 개발할 수 있습니다.

다음 글에서는 다양한 LLM 제공자를 MCP 생태계와 통합하는 방법에 대해 자세히 알아보겠습니다. 각 제공자의 특성과 장단점, 그리고 Model Context Protocol을 활용한 통합 전략을 살펴볼 예정입니다.


참고 자료


개발자 후원하기

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

Support on Patreon

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

0개의 댓글