Integrating AI with Flutter: Creating AI Services with LlmServer and mcp_server について
Dart の `LlmServer` と `mcp_server` を組み合わせ、複数のクライアントアプリに AI サービスを提供する方法を解説する記事です。MCP ツールの登録、LLM プラグイン、自然言語の説明からのツール生成を扱います。
主な機能
- 01LLM 機能を MCP ツールとして公開
- 02カスタムツールプラグインの登録
- 03テキスト生成、ストリーミング、埋め込み、検索
- 04自然言語の説明からツールを生成
- 05サーバー動作のログ記録と監視
活用シーン
- 01複数のクライアントアプリに共通の AI サービスを提供
- 02MCP 経由で LLM 機能を利用可能にする
- 03自然言語の仕様からツールを作成
MCP 対応
`mcp_server` コンポーネントは MCP クライアントにツール、リソース、プロンプトテンプレートを公開します。`LlmServer` と接続し、LLM 機能を MCP ツールとして登録する方法を解説しています。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
このサーバーのパッケージやリモートエンドポイントはまだ取得できていません。プロジェクトの README の手順に従ってください。
ツール
ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。
詳細
Flutter と AI の統合:LlmServer と mcp_server による AI サービスの作成
はじめに
Flutter アプリケーションへの AI の統合に関するシリーズでは、これまで LlmClient と mcp_client を使ったクライアント側の統合について解説しました。今回は、LlmServer と mcp_server を使ったサーバー側の統合について詳しく見ていきます。この強力な組み合わせにより、複数のクライアントから利用できるサービスとして AI 機能を提供できます。
この記事では、Model Context Protocol(MCP)シリーズの第 5 回、そして mcp_llm に焦点を当てた第 3 回として、標準化された MCP プロトコルを活用する堅牢な AI サービスの構築方法を解説します。これにより、幅広いクライアントアプリケーションから AI 機能を利用できるようになります。
目次
- LlmServer と mcp_server について
- サーバー側統合アーキテクチャ
- 統合のセットアップ
- LLM 関数の MCP ツールとしての登録
- コア LLM プラグイン
- AI ベースのツール生成
- サーバーの監視と管理
- 次のステップ
LlmServer と mcp_server について
統合の詳細に入る前に、これら 2 つの主要コンポーネントの役割を確認しましょう。
LlmServer の役割
LlmServer は mcp_llm パッケージのコアコンポーネントで、AI 機能のサーバー側実装を担います。主な役割は次のとおりです。
- Claude、GPT などの大規模言語モデル(LLM)との通信
- AI を活用したツールの登録と管理
- クエリの処理と応答の生成
- プラグインと拡張機能の管理
- パフォーマンスとリクエストの監視
AI 機能をクライアントアプリケーションに直接組み込むのではなく、サービスとして提供します。
mcp_server の役割
mcp_server はサーバー側で Model Context Protocol(MCP)を実装し、次の機能を提供します。
- クライアントへのツールの登録と公開
- リソース(データベース、API、ファイル)の管理
- 標準化されたプロンプトテンプレートの提供
- クライアント接続とセッションの管理
- プロトコルに基づく通信
統合の価値
これらのコンポーネントを統合すると、次のことが可能になります。
- LLM の機能を標準化された MCP ツールとして公開する
- 複数のクライアント向けに AI 機能を一元管理する
- API キーと認証情報を 1 か所で安全に管理する
- 自然言語による説明に基づいて新しいツールを動的に生成する
- クライアントアプリケーションとは独立して AI 機能をスケーリングする
この統合により、クライアントインターフェースとバックエンドの AI 処理が明確に分離され、スケーラビリティ、セキュリティ、リソース管理が向上します。
サーバー側統合アーキテクチャ
この統合を実装するには、アーキテクチャを理解することが重要です。
統合モデル
[Client Apps] ← HTTP/SSE → [mcp_server] ↔ [LlmServer] ← HTTP → [LLM API]
↓ ↓
[Tools/Resources] [Plugin Management]
通信フロー
コンポーネント間の通信は、次の流れで行われます。
mcp_serverがクライアント(Flutter アプリ、Web クライアントなど)からリクエストを受け取ります- リクエストに LLM ツールが関係する場合、
LlmServerに転送します LlmServerがリクエストを処理し、LLM プロバイダーと通信します- LLM の応答が
LlmServerに返されます LlmServerが応答を処理し、結果をmcp_serverに返しますmcp_serverが最終結果をクライアントに送信します
この階層型アーキテクチャによって、関心事が明確に分離され、効率的なスケーリングが可能になります。
インテグレーションのセットアップ
LlmServer と mcp_server を使用して、統合 AI サーバーをセットアップする手順を見ていきましょう。
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 {
// Logger setup - utility for structured server logs
final logger = Logger.getLogger('mcp_llm.server');
try {
// Load environment variables - server configuration settings
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';
// Set log level - various levels of detail for logging
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);
// Verify API key - required setting for server startup
if (apiKey.isEmpty) {
logger.error('OPENAI_API_KEY is not set.');
exit(1);
}
logger.info('Starting AI service server...');
// Create McpLlm instance - entry point for all LLM functionality
final mcpLlm = McpLlm();
// Register LLM provider - support for various AI models
mcpLlm.registerProvider('openai', OpenAiProviderFactory());
logger.debug('OpenAI provider registered.');
// Create MCP server - Model Context Protocol server configuration
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,
),
);
// Register custom tools - add specific functionality tools
final pluginManager = PluginManager();
await pluginManager.registerPlugin(EchoToolPlugin());
await pluginManager.registerPlugin(CalculatorToolPlugin());
// Create LlmServer - server for providing AI functionality as a service
final llmServer = await mcpLlm.createServer(
providerName: 'openai', // LLM provider to use
config: LlmConfiguration(
apiKey: apiKey,
model: 'gpt-4o', // Model to use
options: {
'temperature': 0.3, // Control response randomness/creativity (0-1)
'max_tokens': 2000, // Maximum output token limit
},
),
storageManager: MemoryStorage(),
pluginManager: pluginManager,
mcpServer: mcpServer, // MCP server integration
);
logger.info('LlmServer has been created.');
// Register core LLM plugins - enable basic AI functionality
await llmServer.registerCoreLlmPlugins(
registerCompletionTool: true, // Text generation tool
registerStreamingTool: true, // Streaming response tool
registerEmbeddingTool: true, // Embedding generation tool
registerRetrievalTools: true, // Retrieval-related tools
registerWithServer: true, // Auto-register with MCP server
);
logger.info('Core LLM plugins registered.');
// Add auto-generated tool - LLM designs/implements tool automatically
// Executing asynchronously to avoid delaying server startup
_generateAutomaticTool(llmServer, logger);
// Create SSE transport - communication channel between server and clients
final transport = mcp.McpServer.createSseTransport(
endpoint: '/sse',
messagesEndpoint: '/message',
port: serverPort,
authToken: authToken, // Token for client authentication
);
// Connect MCP server with transport
mcpServer.connect(transport);
logger.info('MCP server connected to transport.');
// Output server information
logger.info('AI service running on port $serverPort.');
logger.info('Server URL: http://localhost:$serverPort/sse');
// Verify auth token setting
if (authToken.isNotEmpty) {
logger.debug('Auth token set: $authToken');
}
// Output available tools
final tools = mcpServer.getTools();
logger.info('');
logger.info('Available tools:');
for (final tool in tools) {
logger.info('- ${tool.name}: ${tool.description}');
}
// Handle server shutdown - for Ctrl+C or other termination
ProcessSignal.sigint.watch().listen((_) async {
logger.info('Shutting down server...');
await mcpLlm.shutdown();
exit(0);
});
// Maintain event loop - keep server running
try {
logger.info('Server ready to process requests. Press Ctrl+C to shut down.');
await Future.delayed(Duration(days: 365));
} catch (e) {
logger.error('Error during server execution: $e');
}
} catch (e, stack) {
logger.error('Error during server startup: $e');
logger.debug('Stack trace: $stack');
exit(1);
}
}
// AI-based tool generation function - LLM designs and implements tools automatically
void _generateAutomaticTool(LlmServer server, Logger logger) async {
try {
logger.info('Generating sentiment analysis tool...');
// Tool description - LLM uses this description to generate the tool
final toolDescription = """
Create a tool that analyzes the sentiment of text. This tool should provide:
1. Sentiment analysis of input text (positive, negative, neutral)
2. Sentiment score (-1.0 to 1.0, where -1 is most negative, 1 is most positive)
3. Extraction of key sentiment words
4. Analysis confidence (0.0 to 1.0)
It should support text in multiple languages and provide results in either text or JSON format.
The tool name must be sentiment_analyzer.
""";
// Attempt LLM-based tool generation
try {
logger.info('Attempting LLM-based tool generation...');
// Generate tool
final success = await server.generateAndRegisterTool(
toolDescription,
registerWithServer: true, // Auto-register with server
);
logger.info('Automatic tool generation result: ${success ? "Success" : "Failed"}');
if (!success) {
logger.info('Automatic generation failed...');
}
} catch (e) {
logger.error('Error during tool generation: $e');
// Handle error if direct registration is needed
logger.info('Error...');
}
} catch (e) {
logger.error('Error during sentiment analyzer tool setup: $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']
},
);
@override
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']
},
);
@override
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()),
]);
}
}
上記のコードでは、いくつかの主要なコンポーネントを使用して統合 AI サーバーをセットアップしています。
- 環境設定:環境変数から API キー、ポート設定、認証トークンを読み込みます
- McpLlm とプロバイダーのセットアップ:メインインスタンスを作成し、LLM プロバイダーを登録します
- MCP サーバーの作成:必要な機能を備えた MCP サーバーをセットアップします
- ツールの登録:Echo や Calculator などの基本的なツールを登録します
- LlmServer の作成:LLM サーバーを作成し、MCP サーバーに接続します
- コアプラグインの登録:基本的な AI 機能をツールとして登録します
- AI ベースのツール生成:LLM を使用して、感情分析ツールを自動的に生成します
- トランスポートのセットアップ:クライアントとの通信に SSE トランスポートをセットアップします
LLM 関数を MCP ツールとして登録する
このインテグレーションの重要な要素の 1 つは、クライアントが利用できる MCP ツールとして LLM の機能を公開することです。提供されているコードでは、ツールを登録する 2 つの方法を示しています。
1. プラグイン登録
サンプルコードでは、BaseToolPlugin クラスを拡張してツールプラグインを作成し、PluginManager を介して登録する方法を示しています。
// Register custom tools through PluginManager
final pluginManager = PluginManager();
await pluginManager.registerPlugin(EchoToolPlugin());
await pluginManager.registerPlugin(CalculatorToolPlugin());
EchoToolPlugin クラスと CalculatorToolPlugin クラスでは、次のようにしてカスタムツールを実装する方法を示しています。
- ツール名、バージョン、説明を定義する
- 入力スキーマを JSON Schema として指定する
- ツール呼び出しを処理する
onExecuteメソッドを実装する
2. コア LLM プラグイン
registerCoreLlmPlugins メソッドは、コア LLM 機能のセットをツールとして自動的に登録します。
// Register core LLM plugins
await llmServer.registerCoreLlmPlugins(
registerCompletionTool: true, // Text generation
registerStreamingTool: true, // Streaming responses
registerEmbeddingTool: true, // Embedding generation
registerRetrievalTools: true, // Retrieval-related tools
registerWithServer: true, // Auto-register with MCP server
);
これらのコアプラグインは、LLM の基本機能(テキスト生成、埋め込みなど)を標準化されたツールとして公開し、MCP プロトコルを介してクライアントが利用できるようにします。
コア LLM プラグイン
registerCoreLlmPlugins を介して登録されるコア LLM プラグインでは、次の機能を利用できます。
- CompletionTool:プロンプトからテキストを生成できます
- StreamingTool:リアルタイムの更新に対応するストリーミング応答を提供します
- EmbeddingTool:テキストのベクトル埋め込みを生成します
- RetrievalTools:検索拡張生成機能を提供します
これらのツールは MCP サーバーに自動的に登録され、接続されているすべてのクライアントが利用できます。
AI ベースのツール生成
コードで示されている最も革新的な機能の 1 つは、LLM 自体を使用して新しいツールを自動的に生成できることです。これは _generateAutomaticTool 関数で実装されています。
void _generateAutomaticTool(LlmServer server, Logger logger) async {
try {
logger.info('Generating sentiment analysis tool...');
// Tool description - LLM uses this description to generate the tool
final toolDescription = """
Create a tool that analyzes the sentiment of text. This tool should provide:
1. Sentiment analysis of input text (positive, negative, neutral)
2. Sentiment score (-1.0 to 1.0, where -1 is most negative, 1 is most positive)
3. Extraction of key sentiment words
4. Analysis confidence (0.0 to 1.0)
It should support text in multiple languages and provide results in either text or JSON format.
The tool name must be sentiment_analyzer.
""";
// Attempt LLM-based tool generation
try {
logger.info('Attempting LLM-based tool generation...');
// Generate tool
final success = await server.generateAndRegisterTool(
toolDescription,
registerWithServer: true, // Auto-register with server
);
logger.info('Automatic tool generation result: ${success ? "Success" : "Failed"}');
if (!success) {
logger.info('Automatic generation failed...');
}
} catch (e) {
logger.error('Error during tool generation: $e');
logger.info('Error...');
}
} catch (e) {
logger.error('Error during sentiment analyzer tool setup: $e');
}
}
この関数では、generateAndRegisterTool メソッドを使用して、次の処理を行います。
- 自然言語によるツールの説明を LLM に送信します
- LLM にツールの入力スキーマと実装を設計させます
- ツールの機能を実現する実行可能なコードを生成します
- 生成されたツールをサーバーに登録します
この強力な機能により、開発者は自然言語で説明するだけで複雑なツールを作成でき、開発を大幅に加速できます。
サーバーの監視と管理
サンプルコードには、サーバーを効果的に監視、管理するための機能がいくつか含まれています。
1. ロギングシステム
コードでは、複数のレベルに対応する包括的なロギングシステムをセットアップしています。
final logger = Logger.getLogger('mcp_llm.server');
// Set log level based on environment
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);
このロギングシステムは、サーバーの処理、エラー、重要なイベントの追跡に役立ちます。
2. ツール一覧
コードでは、登録後に利用可能なすべてのツールを一覧表示します。
// Output available tools
final tools = mcpServer.getTools();
logger.info('');
logger.info('Available tools:');
for (final tool in tools) {
logger.info('- ${tool.name}: ${tool.description}');
}
これにより、クライアントが利用できる機能を明確に把握できます。
3. グレースフルシャットダウン
コードでは、リソースを適切にクリーンアップするためのグレースフルシャットダウン機構を実装しています。
// Handle server shutdown - for Ctrl+C or other termination
ProcessSignal.sigint.watch().listen((_) async {
logger.info('Shutting down server...');
await mcpLlm.shutdown();
exit(0);
});
これにより、サーバーの終了時にすべてのリソースが適切に解放されます。
次のステップ
LlmServer と mcp_server の基本的な統合を実装した後は、次のような高度なトピックを検討できます。
- 複数の LLM プロバイダーとの統合:さまざまな AI モデル(Claude、GPT など)を MCP エコシステムに統合します
- MCP プラグインシステムの開発:カスタムのツールやリソースをプラグインとして構築します
- 分散型 MCP 環境:複数の MCP クライアントとサーバーを使用して、複雑なシステムを構築します
- MCP ツールによる並列処理:MCP ツールを使った並列タスク実行を実装します
- MCP ベースの RAG システムの構築:ドキュメント検索を使ったナレッジベースのシステムを実装します
LlmServer と mcp_server を統合すると、幅広いクライアントアプリケーションから利用できる AI 搭載サービスを構築するための可能性が大きく広がります。
まとめ
この記事では、強力な AI サービスを作成するための LlmServer と mcp_server の統合について説明しました。このサーバーサイド統合には、次のような多くの利点があります。
- 標準化された API:標準化されたプロトコルを通じて AI 機能を公開できます
- リソースの最適化:負荷の高い AI ワークロードをサーバー上で処理し、クライアントのリソースを節約できます
- 一元管理:すべての AI 機能とツールを一元的に管理できます
- スケーラビリティ:クライアント側を変更することなく、新しい AI 機能やツールを簡単に追加できます
- 複数クライアントのサポート:単一のサーバーでさまざまなクライアントプラットフォームをサポートできます
- セキュリティの強化:機密性の高い API キーや認証情報をサーバー上に保持できます
LLM 自体を使って新しいツールを自動生成できることは、特に注目すべき点です。コードを書く代わりに自然言語で説明することで、複雑な AI 搭載ツールを作成できます。
これまでの記事で取り上げたクライアントサイド統合と、この記事で紹介したサーバーサイド統合を組み合わせることで、Model Context Protocol の力を活用した、完全でスケーラブルな AI エコシステムを構築できます。
リソース
- mcp_llm GitHub リポジトリ
- mcp_server GitHub リポジトリ
- Model Context Protocol
- OpenAI API ドキュメント
- Flutter ドキュメント
開発者を支援する
この記事が役に立った場合は、Patreon を通じて、今後も無料コンテンツを開発できるようご支援をご検討ください。皆さまのご支援は大きな力になります!
タグ:#Flutter #AI #MCP #LLM #Dart #OpenAI #ModelContextProtocol #AIIntegration #mcp_server #LlmServer
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。
