mcp_playground_flutter 0.3.0 copy "mcp_playground_flutter: ^0.3.0" to clipboard
mcp_playground_flutter: ^0.3.0 copied to clipboard

Interactive AI Agent Playground widget for Flutter. Connect to any LLM provider, register Dart-native tools, remote HTTP MCP servers, and local Node.js or Python subprocesses.

example/lib/main.dart

import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:mcp_playground_flutter/mcp_playground_flutter.dart';
import 'env_loader.dart';
import 'example_local_tools.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await EnvLoader.load();
  runApp(const McpPlaygroundExampleApp());
}

class McpPlaygroundExampleApp extends StatelessWidget {
  const McpPlaygroundExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'MCP Playground Example',
      debugShowCheckedModeBanner: false,
      themeMode: ThemeMode.system,
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF0067C0),
          brightness: Brightness.light,
        ),
      ),
      darkTheme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF60CDFF),
          brightness: Brightness.dark,
        ),
      ),
      home: const McpPlaygroundScreen(),
    );
  }
}

class McpPlaygroundScreen extends StatelessWidget {
  const McpPlaygroundScreen({super.key});

  @override
  Widget build(BuildContext context) {
    // ── Register custom local (Dart-native) MCP tools ──────────────
    final sshDefaults = <String, dynamic>{
      'host': EnvLoader.get('LOCAL_MCP_SSH_HOST'),
      'port': int.tryParse(EnvLoader.get('LOCAL_MCP_SSH_PORT')) ?? 22,
      'username': EnvLoader.get('LOCAL_MCP_SSH_USER'),
      'password': EnvLoader.get('LOCAL_MCP_SSH_PASSW'),
    };

    final List<McpLocalTool> demoLocalTools = [
      // --- Weather tools (free Open-Meteo API, no key required) ---
      GetCurrentWeatherTool(),
      GetHourlyForecastTool(),
      GetDailyForecastTool(),
      GeocodeWeatherCityTool(),

      // --- SSH/SFTP tools (requires the dartssh2 package) ---
      SshListDirectoryTool(() => sshDefaults),
      SshReadFileTool(() => sshDefaults),
      SshDownloadFileTool(() => sshDefaults),
      SshUploadFileTool(() => sshDefaults),
      SshExecuteCommandTool(() => sshDefaults),
      SshMakeDirectoryTool(() => sshDefaults),
      SshRemoveDirectoryTool(() => sshDefaults),

      // --- Chart tools ---
      CreateChartPngTool(), // JSON config -> rendered via fl_chart in host
      Chart2PngTool(), // Canvas-based -> returns direct PNG image
    ];

    // Load initial LLM configuration if configured in .env
    final initialLlm = LlmConfig(
      provider: EnvLoader.getProvider(),
      model: EnvLoader.get('LLM_MODEL', defaultValue: 'gpt-4o'),
      apiKey: EnvLoader.get('LLM_API_KEY'),
      baseUrl: EnvLoader.get('LLM_URL'),
    );

    final isDesktop =
        !kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS);

    // Initial local MCP servers setup with git and filesystem configs (desktop only)
    final List<LocalMcpServerSetup>? initialLocalServers = isDesktop
        ? [
            LocalMcpServerSetup(
              name: 'Filesystem',
              type: 'node',
              method: 'npx',
              packageOrServerName: '@modelcontextprotocol/server-filesystem',
              launchArguments: Directory.current.path,
              reinstall: false,
            ),
            const LocalMcpServerSetup(
              name: 'Git',
              type: 'python',
              method: 'uvx',
              packageOrServerName: 'mcp-server-git',
              launchArguments: '',
              reinstall: false,
            ),
          ]
        : null;

    return McpPlayground(
      initialLlmConfig: initialLlm.provider != LlmProvider.none
          ? initialLlm
          : null,
      customLocalTools: demoLocalTools,
      initialLocalMcpServers: initialLocalServers,
      initialEnabledTools: const [
        'get_weather_forecast',
        'get_current_weather',
        'get_hourly_forecast',
        'get_daily_forecast',
        'geocode_weather_city',
      ],
      messageContentBuilder: (context, message) {
        if (message == null) return null;
        if (message.type != MessageType.toolResponse) return null;
        if (message.toolName != 'create_chart_png') return null;
        final contentText = message.content.trim();
        if (!((contentText.startsWith('{') && contentText.endsWith('}')) ||
            (contentText.startsWith('[') && contentText.endsWith(']')))) {
          return null;
        }

        try {
          final decoded = jsonDecode(contentText);
          if (decoded is! Map<String, dynamic>) return null;

          final chartType =
              ((decoded['chart_type'] ?? decoded['chartType']) as String? ??
                      'line')
                  .trim()
                  .toLowerCase();
          final labels =
              ((decoded['labels'] ?? decoded['xAxis']) as List?)
                  ?.cast<String>() ??
              [];
          final dataList =
              ((decoded['data'] ?? decoded['yAxis']) as List?)
                  ?.map((d) => (d as num).toDouble())
                  .toList() ??
              [];

          if (labels.isEmpty || dataList.isEmpty) return null;

          final theme = Theme.of(context);
          final title = (decoded['title'] ?? decoded['chartTitle'] ?? 'Chart')
              .toString();

          return Container(
            margin: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: theme.colorScheme.surfaceContainerLow,
              borderRadius: BorderRadius.circular(16),
              boxShadow: [
                BoxShadow(
                  color: Colors.black.withValues(alpha: 0.05),
                  blurRadius: 10,
                  offset: const Offset(0, 4),
                ),
              ],
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(
                  title,
                  style: theme.textTheme.titleMedium?.copyWith(
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 24),
                SizedBox(
                  height: 220,
                  child: _renderFlChart(chartType, labels, dataList, theme),
                ),
              ],
            ),
          );
        } catch (_) {
          return null;
        }
      },
    );
  }

  Widget _renderFlChart(
    String type,
    List<String> labels,
    List<double> data,
    ThemeData theme,
  ) {
    if (type == 'pie') {
      final List<PieChartSectionData> sections = [];
      for (int i = 0; i < data.length; i++) {
        sections.add(
          PieChartSectionData(
            color: Colors.primaries[i % Colors.primaries.length],
            value: data[i],
            title: '${labels.length > i ? labels[i] : ""}\n${data[i]}',
            radius: 60,
            titleStyle: const TextStyle(
              fontSize: 11,
              fontWeight: FontWeight.bold,
              color: Colors.white,
            ),
          ),
        );
      }
      return PieChart(PieChartData(sections: sections, centerSpaceRadius: 40));
    }

    if (type == 'bar') {
      final List<BarChartGroupData> groups = [];
      for (int i = 0; i < data.length; i++) {
        groups.add(
          BarChartGroupData(
            x: i,
            barRods: [
              BarChartRodData(
                toY: data[i],
                color: theme.colorScheme.primary,
                width: 16,
                borderRadius: BorderRadius.circular(4),
              ),
            ],
          ),
        );
      }

      return BarChart(
        BarChartData(
          barGroups: groups,
          gridData: const FlGridData(show: false),
          titlesData: FlTitlesData(
            leftTitles: const AxisTitles(
              sideTitles: SideTitles(showTitles: true, reservedSize: 32),
            ),
            bottomTitles: AxisTitles(
              sideTitles: SideTitles(
                showTitles: true,
                getTitlesWidget: (val, meta) {
                  final idx = val.toInt();
                  if (idx >= 0 && idx < labels.length) {
                    return Padding(
                      padding: const EdgeInsets.only(top: 6.0),
                      child: Text(
                        labels[idx],
                        style: const TextStyle(fontSize: 10),
                      ),
                    );
                  }
                  return const Text('');
                },
              ),
            ),
            rightTitles: const AxisTitles(
              sideTitles: SideTitles(showTitles: false),
            ),
            topTitles: const AxisTitles(
              sideTitles: SideTitles(showTitles: false),
            ),
          ),
        ),
      );
    }

    // Default: Line chart
    final List<FlSpot> spots = [];
    for (int i = 0; i < data.length; i++) {
      spots.add(FlSpot(i.toDouble(), data[i]));
    }

    return LineChart(
      LineChartData(
        lineBarsData: [
          LineChartBarData(
            spots: spots,
            isCurved: true,
            color: theme.colorScheme.primary,
            barWidth: 3,
            dotData: const FlDotData(show: true),
            belowBarData: BarAreaData(
              show: true,
              color: theme.colorScheme.primary.withValues(alpha: 0.15),
            ),
          ),
        ],
        gridData: const FlGridData(show: false),
        titlesData: FlTitlesData(
          leftTitles: const AxisTitles(
            sideTitles: SideTitles(showTitles: true, reservedSize: 32),
          ),
          bottomTitles: AxisTitles(
            sideTitles: SideTitles(
              showTitles: true,
              getTitlesWidget: (val, meta) {
                final idx = val.toInt();
                if (idx >= 0 && idx < labels.length) {
                  return Padding(
                    padding: const EdgeInsets.only(top: 6.0),
                    child: Text(
                      labels[idx],
                      style: const TextStyle(fontSize: 10),
                    ),
                  );
                }
                return const Text('');
              },
            ),
          ),
          rightTitles: const AxisTitles(
            sideTitles: SideTitles(showTitles: false),
          ),
          topTitles: const AxisTitles(
            sideTitles: SideTitles(showTitles: false),
          ),
        ),
      ),
    );
  }
}
1
likes
150
points
384
downloads

Documentation

API reference

Publisher

verified publishertealkit.dev

Weekly Downloads

Interactive AI Agent Playground widget for Flutter. Connect to any LLM provider, register Dart-native tools, remote HTTP MCP servers, and local Node.js or Python subprocesses.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

anthropic_sdk_dart, archive, file_picker, flutter, flutter_markdown_plus, flutter_widget_from_html, googleai_dart, http, llamadart, mcp_playground_dart, mcp_playground_ui, ollama_dart, openai_dart, path, path_provider, shared_preferences, universal_io, url_launcher, uuid

More

Packages that depend on mcp_playground_flutter