mcp_bridge 0.1.0 copy "mcp_bridge: ^0.1.0" to clipboard
mcp_bridge: ^0.1.0 copied to clipboard

Dart plugin for implementing Model Context Protocol (MCP) bridge. Provides tools to build MCP bridge that can expose data, functionality, and interaction patterns to LLM applications.

example/mcp_bridge_example.dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:args/args.dart';
import 'package:mcp_bridge/mcp_bridge.dart';

final Logger _logger = Logger.getLogger('test_bridge');

late IOSink logSink;
late File detailedLogFile;

/// Main entry point for the MCP Bridge test application
void main(List<String> arguments) async {
  // Parse command-line arguments
  final parser = ArgParser()
    ..addOption('server-type',
        abbr: 's',
        help: 'Server transport type (stdio, sse)',
        defaultsTo: 'stdio')
    ..addOption('client-type',
        abbr: 'c',
        help: 'Client transport type (stdio, sse)',
        defaultsTo: 'sse')
    ..addOption('server-action',
        help: 'Server shutdown action (shutdown, waitreconnect)',
        defaultsTo: 'shutdown')
    ..addOption('server-url', help: 'SSE server URL (required for sse client)',
        defaultsTo: 'http://localhost:8999/sse')
    ..addOption('auth-token',
        help: 'Authentication token for SSE client/server connections',
        defaultsTo: 'test_token')
    ..addOption('command',
        help: 'Command to execute (required for stdio client)')
    ..addOption('arguments', help: 'Command arguments as comma-separated list')
    ..addOption('working-dir', help: 'Working directory for stdio client')
    ..addOption('port',
        help: 'HTTP server port for SSE server', defaultsTo: '8999')
    ..addOption('config-file', help: 'Configuration file path')
  // Add logging related options
    ..addOption('log-to-file',
        help: 'Enable or disable logging to file',
        defaultsTo: 'false') // Enable/disable file logging
    ..addOption('log-dir',
        help: 'Directory to store log files',
        defaultsTo: null) // Log file storage directory
    ..addFlag('verbose',
        abbr: 'v', help: 'Enable verbose logging', negatable: false)
    ..addFlag('help', abbr: 'h', help: 'Show help', negatable: false);

  ArgResults results;

  try {
    results = parser.parse(arguments);
  } catch (e) {
    logError('Argument parsing error: $e');
    _printUsage(parser);
    exit(1);
  }

  if (results['help'] as bool) {
    _printUsage(parser);
    exit(0);
  }
  Logger.getLogger('mcp_bridge').setLevel(LogLevel.trace);
  // Setup logging with command line options
  final logToFileStr = results['log-to-file'] as String;
  final logToFile = logToFileStr.toLowerCase() == 'true';
  final String? logDirectory = results['log-dir'] as String?;
  setupLogging(logToFile: logToFile, logDirectory: logDirectory);

  // Set log level
  if (results['verbose'] as bool) {
    Logger.getLogger('mcp_bridge').setLevel(LogLevel.trace);
  }

  try {
    // Load from configuration file or create from command-line arguments
    McpBridgeConfig config;
    if (results['config-file'] != null) {
      final configFilePath = results['config-file'] as String;
      config = await _loadConfigFromFile(configFilePath);
    } else {
      config = _createConfigFromArgs(results);
    }

    // Create and initialize bridge
    await runBridge(config);
  } catch (e, stackTrace) {
    logError('Error occurred: $e');
    logDebug(stackTrace.toString());
    exit(1);
  }
}

Future<McpBridgeConfig> _loadConfigFromFile(String path) async {
  final configFile = File(path);
  if (!await configFile.exists()) {
    throw FileSystemException('Configuration file not found: $path');
  }

  logInfo('Loading from configuration file: $path');
  final jsonStr = await configFile.readAsString();
  final json = jsonDecode(jsonStr);
  return McpBridgeConfig.fromJson(json);
}

McpBridgeConfig _createConfigFromArgs(ArgResults args) {
  final serverType = args['server-type'] as String;
  final clientType = args['client-type'] as String;
  final serverActionStr = args['server-action'] as String;
  final authToken =
  args['auth-token'] as String?; // Get authentication token value

  // Determine server action mode (existing code)
  ServerShutdownBehavior serverAction;
  if (serverType.toLowerCase() == 'stdio') {
    // STDIO server always uses shutdownBridge mode (because client controls it)
    serverAction = ServerShutdownBehavior.shutdownBridge;
    if (serverActionStr.toLowerCase() == 'waitreconnect') {
      logWarning(
          'STDIO server does not support wait for reconnection mode. Setting to shutdownBridge mode.');
    }
  } else {
    // SSE server determined by configuration
    serverAction = serverActionStr.toLowerCase() == 'waitreconnect'
        ? ServerShutdownBehavior.waitForReconnection
        : ServerShutdownBehavior.shutdownBridge;
  }

  // Server configuration
  final serverConfig = <String, dynamic>{};
  if (serverType.toLowerCase() == 'sse') {
    final port = int.tryParse(args['port'] as String) ?? 8080;
    serverConfig['port'] = port;
    serverConfig['endpoint'] = '/sse';
    serverConfig['messagesEndpoint'] =
    '/message'; // Changed '/messages' to '/message'

    // Add SSE server authentication token configuration
    if (authToken != null) {
      serverConfig['authToken'] = authToken;
      logInfo('Authentication token has been set for SSE server.');
    }
  }

  // Client configuration
  final clientConfig = <String, dynamic>{};
  if (clientType.toLowerCase() == 'stdio') {
    final command = args['command'] as String?;
    if (command == null) {
      throw ArgumentError('stdio client requires command argument');
    }
    clientConfig['command'] = command;

    // Process command arguments
    if (args['arguments'] != null) {
      final argsStr = args['arguments'] as String;
      final argsList = argsStr.split(',').map((e) => e.trim()).toList();
      clientConfig['arguments'] = argsList;
    }

    // Process working directory
    if (args['working-dir'] != null) {
      clientConfig['workingDirectory'] = args['working-dir'] as String;
    }
  } else if (clientType.toLowerCase() == 'sse') {
    final serverUrl = args['server-url'] as String?;
    if (serverUrl == null) {
      throw ArgumentError('sse client requires server-url argument');
    }
    clientConfig['serverUrl'] = serverUrl;

    // Add SSE client authentication header
    if (authToken != null) {
      clientConfig['headers'] = {'Authorization': 'Bearer $authToken'};
      logInfo('Authentication token header has been set for SSE client.');
    }
  }

  return McpBridgeConfig(
    serverTransportType: serverType,
    clientTransportType: clientType,
    serverConfig: serverConfig,
    clientConfig: clientConfig,
    serverShutdownBehavior: serverAction,
  );
}

Future<void> runBridge(McpBridgeConfig config) async {
  logInfo('Starting MCP Bridge');
  logInfo(
      'Server type: ${config.serverTransportType}, Client type: ${config.clientTransportType}');
  logInfo('Server shutdown behavior: ${config.serverShutdownBehavior}');

  // Check configuration
  if (config.clientTransportType.toLowerCase() == 'stdio') {
    logInfo('STDIO client command: ${config.clientConfig['command']}');
    if (config.clientConfig.containsKey('arguments')) {
      logInfo('STDIO client arguments: ${config.clientConfig['arguments']}');
    }
  } else if (config.clientTransportType.toLowerCase() == 'sse') {
    logInfo('SSE client server URL: ${config.clientConfig['serverUrl']}');
    // Log authentication header presence
    if (config.clientConfig.containsKey('headers')) {
      logInfo('SSE client authentication header has been set.');
    } else {
      logWarning('SSE client authentication header has not been set.');
    }
  }

  if (config.serverTransportType.toLowerCase() == 'sse') {
    logInfo('SSE server port: ${config.serverConfig['port']}');
    logInfo('SSE server endpoint: ${config.serverConfig['endpoint']}');
    logInfo(
        'SSE server message endpoint: ${config.serverConfig['messagesEndpoint']}');
    // Log authentication token presence
    if (config.serverConfig.containsKey('authToken')) {
      logInfo('SSE server authentication token has been set.');
    } else {
      logWarning('SSE server authentication token has not been set.');
    }
  }

  final bridge = McpBridge(config);

  // Configure client auto-reconnect
  bridge.setAutoReconnect(enabled: true);

  // Configure options for SSE server & wait for reconnection mode
  if (config.serverTransportType.toLowerCase() == 'sse' &&
      config.serverShutdownBehavior ==
          ServerShutdownBehavior.waitForReconnection) {
    bridge.setServerReconnectionOptions(
      maxAttempts: 0, // Unlimited retry
      checkInterval: Duration(seconds: 10),
    );

    // Server reconnection callback
    bridge.onServerReconnectRequested = () async {
      logInfo('Attempting server reconnection...');
      await Future.delayed(Duration(seconds: 3));
      return true;
    };
  }

  // Register event handlers
  bridge.onTransportError = (source, error, stackTrace) {
    logError('Error occurred in ${source.name}: $error');
  };

  bridge.onTransportClosed = (source) {
    logInfo('${source.name} connection closed');

    if (source == TransportSource.server) {
      if (config.serverTransportType.toLowerCase() == 'sse' &&
          config.serverShutdownBehavior ==
              ServerShutdownBehavior.waitForReconnection) {
        logInfo(
            'SSE server connection has been closed. Waiting for server reconnection.');
        // Handled internally (in McpBridge._handleServerDisconnection method)
      } else {
        logInfo('Server has terminated, so the bridge will also terminate.');
        // Shutdown is not necessary - handled automatically by internal logic
      }
    } else {
      logInfo(
          'Client connection has been lost. Will attempt reconnection while server is active.');
      // Client auto-reconnection is handled by internal logic
    }
  };

  bridge.onTransportReconnected = (source) {
    logInfo('${source.name} reconnection successful');
  };

  try {
    // Initialize bridge
    await bridge.initialize();
    logInfo('Bridge initialization complete');

    // Handle termination signals
    final completer = Completer<void>();

    ProcessSignal.sigint.watch().listen((_) async {
      logInfo('User termination signal received. Shutting down...');
      await bridge.shutdown();
      completer.complete();
    });

    logInfo('Bridge is running.');
    await completer.future;
  } catch (e) {
    logError('Failed to initialize bridge: $e');
    rethrow;
  } finally {
    if (bridge.isInitialized) {
      await bridge.shutdown();
    }
    logInfo('Bridge shutdown complete');
  }
}

void _printUsage(ArgParser parser) {
  logInfo('MCP Bridge Test Application');
  logInfo('');
  logInfo('Usage:');
  logInfo(
      '  dart mcp_test_app.dart --server-type=sse --client-type=stdio --command="python" --arguments="client.py,arg1,arg2" [options]');
  logInfo('  dart mcp_test_app.dart --config-file=config.json');
  logInfo('');
  logInfo('Options:');
  logInfo(parser.usage);
  logInfo('');
  logInfo('Logging Options:');
  logInfo('  --log-to-file=[true|false]   Enable or disable logging to file (default: false)');
  logInfo('  --log-dir=PATH               Directory to store log files (default: current directory)');
  logInfo('');
  logInfo('Examples:');
  logInfo(
      '  dart mcp_test_app.dart --server-type=stdio --client-type=sse --server-url="http://localhost:8080/sse"');
  logInfo(
      '  dart mcp_test_app.dart --server-type=sse --client-type=stdio --command="python" --arguments="mcp_client.py" --server-action=waitreconnect');
  logInfo(
      '  dart mcp_test_app.dart --server-type=sse --client-type=stdio --command="python" --arguments="mcp_client.py" --auth-token="test_token"');
  logInfo(
      '  dart mcp_test_app.dart --server-type=stdio --client-type=sse --server-url="http://localhost:8080/sse" --auth-token="test_token"');
  logInfo('  dart mcp_test_app.dart --config-file=config.json');
  logInfo('');
  logInfo('Logging Examples:');
  logInfo('  dart mcp_test_app.dart --log-to-file=false --server-type=stdio --client-type=sse --server-url="http://localhost:8080/sse"');
  logInfo('  dart mcp_test_app.dart --log-dir=/tmp --server-type=sse --client-type=stdio --command="python"');
}

// Logging setup function
void setupLogging({bool logToFile = true, String? logDirectory}) {
  Logger.getLogger('mcp_bridge').setLevel(LogLevel.debug);
  // Basic logging configuration
  _logger.configure(
      level: LogLevel.debug, includeTimestamp: true, useColor: true);

  // Skip log file creation if file logging is disabled
  if (!logToFile) {
    logInfo("File logging disabled");
    return;
  }

  // Set log directory - use default or specified directory
  final logDir = logDirectory ?? Directory.current.path;

  try {
    // Check if log directory exists and create if necessary
    final logDirObj = Directory(logDir);
    if (!logDirObj.existsSync()) {
      logDirObj.createSync(recursive: true);
    }

    // Create log file
    final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-');
    detailedLogFile = File('$logDir/mcp_bridge_$timestamp.log');
    logSink = detailedLogFile.openWrite(mode: FileMode.append);

    logInfo("Log file created: ${detailedLogFile.path}");
  } catch (e) {
    _logger.error("Failed to create log file: $e. File logging disabled.");
    // Use only console logging if file logging fails
    logToFile = false;
  }
}

void logInfo(String message) {
  // Console logging
  _logger.info(message);

  try {
    final timestamp = DateTime.now().toIso8601String();
    logSink.writeln("$timestamp INFO: $message");
  } catch (e) {
    // Ignore log file write errors
  }
}

void logDebug(String message) {
  // Console logging
  _logger.debug(message);

  try {
    final timestamp = DateTime.now().toIso8601String();
    logSink.writeln("$timestamp DEBUG: $message");
  } catch (e) {
    // Ignore log file write errors
  }
}

void logWarning(String message) {
  // Console logging
  _logger.warning(message);

  try {
    final timestamp = DateTime.now().toIso8601String();
    logSink.writeln("$timestamp WARNING: $message");
  } catch (e) {
    // Ignore log file write errors
  }
}

void logError(String message, [Object? error, StackTrace? stackTrace]) {
  // Console logging
  _logger.error(message);

  try {
    final timestamp = DateTime.now().toIso8601String();
    logSink.writeln("$timestamp ERROR: $message");

    if (error != null) {
      logSink.writeln("$timestamp ERROR DETAIL: $error");
    }

    if (stackTrace != null) {
      logSink.writeln("$timestamp STACK TRACE: $stackTrace");
    }
  } catch (e) {
    // Ignore log file write errors
  }
}
2
likes
140
points
82
downloads

Documentation

API reference

Publisher

verified publishermakemind.dev

Weekly Downloads

Dart plugin for implementing Model Context Protocol (MCP) bridge. Provides tools to build MCP bridge that can expose data, functionality, and interaction patterns to LLM applications.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

args, mcp_client, mcp_server

More

Packages that depend on mcp_bridge