flutter_mcp_client_lib 1.0.3 copy "flutter_mcp_client_lib: ^1.0.3" to clipboard
flutter_mcp_client_lib: ^1.0.3 copied to clipboard

A Flutter implementation of the Model Context Protocol (MCP) for integrating with AI tools like Windsurf, Cursor, and Claude.

Flutter MCP Client Library #

pub package License: MIT

A Flutter implementation of the Model Context Protocol (MCP) for integrating with AI tools like Windsurf, Cursor, and Claude.

Latest Updates #

  • Enhanced ClientCapabilities with resources, tools, and prompts fields
  • Improved error handling and timeout management
  • Added comprehensive documentation and examples

Overview #

The Model Context Protocol (MCP) is an open protocol that enables seamless integration between LLM applications and external data sources and tools. This Flutter package implements the MCP specification, making it easy to:

  • Connect to MCP servers using WebSocket or HTTP
  • List and read resources (files, documents, etc.)
  • List and call tools (functions, commands, etc.)
  • List and retrieve prompts (templates for LLM interactions)
  • Render MCP responses in Flutter UI

Features #

  • Complete MCP Implementation: Full implementation of the MCP protocol version 2025-03-26
  • Multiple Transport Layers: Support for WebSocket and HTTP connections
  • Type-Safe API: Strongly typed models and interfaces for MCP components
  • UI Components: Ready-to-use widgets for displaying MCP responses
  • Error Handling: Comprehensive error handling and recovery strategies
  • Extensible Architecture: Easy to extend with custom functionality
  • Cross-Platform: Works on iOS, Android, web, and desktop platforms
  • Well-Documented: Comprehensive documentation and examples

Installation #

Add the following to your pubspec.yaml:

dependencies:
  flutter_mcp_client_lib: ^1.0.0

Then run:

flutter pub get

Quick Start #

Connecting to an MCP Server #

import 'package:flutter_mcp_client_lib/flutter_mcp_client_lib.dart';

Future<void> connectToServer() async {
  // Create a client
  final client = McpClient(
    name: 'My App',
    version: '1.0.0',
    capabilities: const ClientCapabilities(
      sampling: SamplingCapabilityConfig(sample: true),
      resources: ResourceCapabilityConfig(list: true, read: true),
      tools: ToolCapabilityConfig(list: true, call: true),
      prompts: PromptCapabilityConfig(list: true, get: true),
    ),
  );

  // Connect to a server using WebSocket
  final transport = McpWebSocketClientTransport(
    Uri.parse('ws://localhost:8080/mcp'),
  );

  try {
    await client.connect(transport);
    print('Connected to MCP server');
    print('Server capabilities: ${client.serverCapabilities}');
  } catch (e) {
    print('Failed to connect: $e');
  }
}

Working with Resources #

// List available resources
final resources = await client.listResources();
for (final resource in resources) {
  print('Resource: ${resource.name} (${resource.uriTemplate})');
}

// Read a resource
final contents = await client.readResource('file://path/to/file.txt');
for (final content in contents) {
  print('Content: ${content.text}');
}

Working with Tools #

// List available tools
final tools = await client.listTools();
for (final tool in tools) {
  print('Tool: ${tool.name} (${tool.description})');
}

// Call a tool
final result = await client.callTool('calculator.add', {
  'a': 5,
  'b': 7,
});
print('Result: ${result.content.first.text}'); // Output: 12

Working with Prompts #

// List available prompts
final prompts = await client.listPrompts();
for (final prompt in prompts) {
  print('Prompt: ${prompt.name} (${prompt.description})');
}

// Get a prompt
final result = await client.getPrompt('greeting', {
  'name': 'John',
});
for (final message in result.messages) {
  print('${message.role}: ${message.content.text}');
}

Rendering MCP Responses in UI #

import 'package:flutter/material.dart';
import 'package:flutter_mcp_client_lib/flutter_mcp_client_lib.dart';

class McpResponseView extends StatelessWidget {
  final McpResponse response;

  const McpResponseView({Key? key, required this.response}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Builder(
      builder: (context) {
        final theme = McpTheme.fromTheme(context);
        final renderer = McpResponseRenderer(theme: theme);
        return renderer.render(context, response);
      },
    );
  }
}

Architecture #

The Flutter MCP package is organized into several components:

  • Client: Handles communication with MCP servers
  • Transports: Implements different transport protocols (WebSocket, HTTP)
  • Models: Defines data structures for MCP messages and objects
  • UI: Provides widgets for rendering MCP responses
  • Utils: Offers utility functions and helpers

Examples #

The package includes several examples:

  • Basic Client: Simple example of connecting to an MCP server
  • Resource Browser: Example of browsing and viewing resources
  • Tool Explorer: Example of discovering and calling tools
  • Prompt Manager: Example of working with prompts
  • UI Components: Example of rendering MCP responses in UI

To run the examples:

cd example
flutter run -t mcp_client_example.dart  # For client example
flutter run -t mcp_ui_example.dart      # For UI example

Documentation #

For more information about the Model Context Protocol, see:

Platform Support #

Platform Support
Android
iOS
Web
Windows
macOS
Linux

Contributing #

Contributions are welcome! Please see CONTRIBUTING.md for details.

License #

This project is licensed under the MIT License - see the LICENSE file for details.

Usage #

MCP Client #

import 'package:flutter_mcp_client_lib/flutter_mcp_client_lib.dart';
import 'package:logging/logging.dart';

void main() async {
  // Set up logging
  Logger.root.level = Level.INFO;
  Logger.root.onRecord.listen((record) {
    print('${record.level.name}: ${record.time}: ${record.message}');
  });

  // Create a client
  final client = McpClient(
    name: 'Example Client',
    version: '1.0.0',
    capabilities: const ClientCapabilities(
      sampling: SamplingCapabilityConfig(sample: true),
      resources: ResourceCapabilityConfig(list: true, read: true),
      tools: ToolCapabilityConfig(list: true, call: true),
      prompts: PromptCapabilityConfig(list: true, get: true),
    ),
  );

  try {
    // Connect to a server using WebSocket
    final transport = McpWebSocketClientTransport(
      Uri.parse('ws://localhost:8080/mcp'),
    );
    await client.connect(transport);

    // After connection, the client will automatically initialize
    // and negotiate capabilities with the server

    // List resources
    final resources = await client.listResources();
    print('Resources:');
    for (final resource in resources) {
      print('- ${resource.name}: ${resource.description}');
      print('  URI Template: ${resource.uriTemplate}');
    }

    // Read a resource
    // For template URIs, replace parameters with actual values
    final greetingUri = 'greeting://John';
    final contents = await client.readResource(greetingUri);
    print('Resource contents:');
    for (final content in contents) {
      print('- ${content.uri}: ${content.text}');
    }

    // List tools
    final tools = await client.listTools();
    print('Tools:');
    for (final tool in tools) {
      print('- ${tool.name}: ${tool.description}');
      print('  Arguments:');
      for (final arg in tool.arguments) {
        print('    - ${arg.name}: ${arg.description} (required: ${arg.required})');
      }
    }

    // Call a tool
    final addResult = await client.callTool(
      'add',
      {'a': '5', 'b': '7'},
    );
    print('Add tool result:');
    for (final content in addResult.content) {
      print('- ${content.type}: ${content.text}');
    }

    // Call another tool
    final echoResult = await client.callTool(
      'echo',
      {'message': 'Hello from MCP client!'},
    );
    print('Echo tool result:');
    for (final content in echoResult.content) {
      print('- ${content.type}: ${content.text}');
    }

    // List prompts
    final prompts = await client.listPrompts();
    print('Prompts:');
    for (final prompt in prompts) {
      print('- ${prompt.name}: ${prompt.description}');
      print('  Arguments:');
      for (final arg in prompt.arguments) {
        print('    - ${arg.name}: ${arg.description} (required: ${arg.required})');
      }
    }

    // Get a prompt
    final promptResult = await client.getPrompt(
      'greeting',
      {'name': 'John'},
    );
    print('Greeting prompt:');
    print('Description: ${promptResult.description}');
    print('Messages:');
    for (final message in promptResult.messages) {
      print('- ${message.role}: ${message.content.text}');
    }
  } catch (e, stackTrace) {
    print('Error: $e');
    print('Stack trace: $stackTrace');
  } finally {
    // Disconnect from the server
    await client.disconnect();
  }
}

Flutter App Example #

import 'package:flutter/material.dart';
import 'package:flutter_mcp_client_lib/flutter_mcp_client_lib.dart';
import 'package:logging/logging.dart';

void main() {
  // Set up logging
  Logger.root.level = Level.INFO;
  Logger.root.onRecord.listen((record) {
    debugPrint('${record.level.name}: ${record.time}: ${record.message}');
  });

  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter MCP Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      home: const McpDemoPage(title: 'Flutter MCP Demo'),
    );
  }
}

class McpDemoPage extends StatefulWidget {
  const McpDemoPage({super.key, required this.title});

  final String title;

  @override
  State<McpDemoPage> createState() => _McpDemoPageState();
}

class _McpDemoPageState extends State<McpDemoPage> {
  final TextEditingController _serverUrlController = TextEditingController(
    text: 'ws://localhost:8080/mcp',
  );

  McpClient? _client;
  bool _isConnected = false;
  bool _isLoading = false;
  List<ResourceInfo> _resources = [];
  List<ToolInfo> _tools = [];
  List<PromptInfo> _prompts = [];
  String _statusMessage = '';

  Future<void> _connect() async {
    setState(() {
      _isLoading = true;
      _statusMessage = 'Connecting...';
    });

    try {
      final serverUrl = _serverUrlController.text;

      _client = McpClient(
        name: 'Flutter MCP Demo',
        version: '1.0.0',
        capabilities: const ClientCapabilities(
          sampling: SamplingCapabilityConfig(sample: true),
          resources: ResourceCapabilityConfig(list: true, read: true),
          tools: ToolCapabilityConfig(list: true, call: true),
          prompts: PromptCapabilityConfig(list: true, get: true),
        ),
      );

      final transport = McpWebSocketClientTransport(Uri.parse(serverUrl));
      await _client!.connect(transport);

      // Load resources
      final resources = await _client!.listResources();

      // Load tools
      final tools = await _client!.listTools();

      // Load prompts
      final prompts = await _client!.listPrompts();

      setState(() {
        _isConnected = true;
        _resources = resources;
        _tools = tools;
        _prompts = prompts;
        _statusMessage = 'Connected to ${serverUrl}';
      });
    } catch (e) {
      setState(() {
        _statusMessage = 'Connection failed: $e';
      });
      _client = null;
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _disconnect() async {
    setState(() {
      _isLoading = true;
      _statusMessage = 'Disconnecting...';
    });

    try {
      await _client!.disconnect();

      setState(() {
        _isConnected = false;
        _resources = [];
        _tools = [];
        _prompts = [];
        _statusMessage = 'Disconnected';
      });

      _client = null;
    } catch (e) {
      setState(() {
        _statusMessage = 'Disconnection failed: $e';
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _readResource(ResourceInfo resource) async {
    setState(() {
      _isLoading = true;
      _statusMessage = 'Reading resource ${resource.name}...';
    });

    try {
      // For template URIs, we need to replace parameters with actual values
      // This is a simple example that replaces {name} with 'Flutter'
      final uri = resource.uriTemplate.replaceAll('{name}', 'Flutter');

      final contents = await _client!.readResource(uri);

      // Show the resource contents in a dialog
      if (!mounted) return;

      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: Text('Resource: ${resource.name}'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('URI: $uri'),
              const SizedBox(height: 8),
              const Text('Contents:'),
              ...contents.map((content) => Text('- ${content.text}')),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: const Text('Close'),
            ),
          ],
        ),
      );

      setState(() {
        _statusMessage = 'Resource read successfully';
      });
    } catch (e) {
      setState(() {
        _statusMessage = 'Failed to read resource: $e';
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _callTool(ToolInfo tool) async {
    // This is a simple example that calls the 'add' tool with fixed arguments
    // In a real app, you would collect the arguments from the user

    setState(() {
      _isLoading = true;
      _statusMessage = 'Calling tool ${tool.name}...';
    });

    try {
      Map<String, dynamic> args = {};

      // Simple example for the 'add' tool
      if (tool.name == 'add') {
        args = {'a': '5', 'b': '7'};
      }
      // Simple example for the 'echo' tool
      else if (tool.name == 'echo') {
        args = {'message': 'Hello from Flutter MCP Demo!'};
      }

      final result = await _client!.callTool(tool.name, args);

      // Show the tool result in a dialog
      if (!mounted) return;

      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: Text('Tool: ${tool.name}'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('Arguments: $args'),
              const SizedBox(height: 8),
              const Text('Result:'),
              ...result.content.map((content) => Text('- ${content.text}')),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: const Text('Close'),
            ),
          ],
        ),
      );

      setState(() {
        _statusMessage = 'Tool called successfully';
      });
    } catch (e) {
      setState(() {
        _statusMessage = 'Failed to call tool: $e';
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _getPrompt(PromptInfo prompt) async {
    setState(() {
      _isLoading = true;
      _statusMessage = 'Getting prompt ${prompt.name}...';
    });

    try {
      // Simple example for the 'greeting' prompt
      final args = {'name': 'Flutter Developer'};

      final result = await _client!.getPrompt(prompt.name, args);

      // Show the prompt result in a dialog
      if (!mounted) return;

      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: Text('Prompt: ${prompt.name}'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('Arguments: $args'),
              const SizedBox(height: 8),
              Text('Description: ${result.description}'),
              const SizedBox(height: 8),
              const Text('Messages:'),
              ...result.messages.map((message) =>
                Text('- ${message.role}: ${message.content.text}')),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: const Text('Close'),
            ),
          ],
        ),
      );

      setState(() {
        _statusMessage = 'Prompt retrieved successfully';
      });
    } catch (e) {
      setState(() {
        _statusMessage = 'Failed to get prompt: $e';
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            TextField(
              controller: _serverUrlController,
              decoration: const InputDecoration(
                labelText: 'Server URL',
                border: OutlineInputBorder(),
              ),
              enabled: !_isConnected && !_isLoading,
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: _isLoading
                ? null
                : (_isConnected ? _disconnect : _connect),
              child: Text(_isConnected ? 'Disconnect' : 'Connect'),
            ),
            const SizedBox(height: 16),
            Text(
              _statusMessage,
              style: Theme.of(context).textTheme.bodyMedium,
            ),
            const SizedBox(height: 16),
            if (_isLoading)
              const Center(child: CircularProgressIndicator())
            else if (_isConnected)
              Expanded(
                child: DefaultTabController(
                  length: 3,
                  child: Column(
                    children: [
                      const TabBar(
                        tabs: [
                          Tab(text: 'Resources'),
                          Tab(text: 'Tools'),
                          Tab(text: 'Prompts'),
                        ],
                      ),
                      Expanded(
                        child: TabBarView(
                          children: [
                            // Resources tab
                            _resources.isEmpty
                              ? const Center(child: Text('No resources available'))
                              : ListView.builder(
                                  itemCount: _resources.length,
                                  itemBuilder: (context, index) {
                                    final resource = _resources[index];
                                    return ListTile(
                                      title: Text(resource.name),
                                      subtitle: Text(resource.description),
                                      trailing: const Icon(Icons.arrow_forward),
                                      onTap: () => _readResource(resource),
                                    );
                                  },
                                ),

                            // Tools tab
                            _tools.isEmpty
                              ? const Center(child: Text('No tools available'))
                              : ListView.builder(
                                  itemCount: _tools.length,
                                  itemBuilder: (context, index) {
                                    final tool = _tools[index];
                                    return ListTile(
                                      title: Text(tool.name),
                                      subtitle: Text(tool.description),
                                      trailing: const Icon(Icons.arrow_forward),
                                      onTap: () => _callTool(tool),
                                    );
                                  },
                                ),

                            // Prompts tab
                            _prompts.isEmpty
                              ? const Center(child: Text('No prompts available'))
                              : ListView.builder(
                                  itemCount: _prompts.length,
                                  itemBuilder: (context, index) {
                                    final prompt = _prompts[index];
                                    return ListTile(
                                      title: Text(prompt.name),
                                      subtitle: Text(prompt.description),
                                      trailing: const Icon(Icons.arrow_forward),
                                      onTap: () => _getPrompt(prompt),
                                    );
                                  },
                                ),
                          ],
                        ),
                      ),
                    ],
                  ),
                ),
              ),
          ],
        ),
      ),
    );
  }
}

Configuration Reference #

McpClient Configuration #

Parameter Type Default Description
name String required The name of the client
version String required The version of the client
capabilities ClientCapabilities required The capabilities of the client
timeout Duration 30 seconds The timeout for requests

ClientCapabilities Configuration #

Parameter Type Default Description
sampling SamplingCapabilityConfig null Configuration for sampling capabilities
resources ResourceCapabilityConfig null Configuration for resource capabilities
tools ToolCapabilityConfig null Configuration for tool capabilities
prompts PromptCapabilityConfig null Configuration for prompt capabilities

SamplingCapabilityConfig #

Parameter Type Default Description
sample bool false Whether the client can handle sample requests

ResourceCapabilityConfig #

Parameter Type Default Description
list bool false Whether the client can list resources
read bool false Whether the client can read resources

ToolCapabilityConfig #

Parameter Type Default Description
list bool false Whether the client can list tools
call bool false Whether the client can call tools

PromptCapabilityConfig #

Parameter Type Default Description
list bool false Whether the client can list prompts
get bool false Whether the client can get prompts

Troubleshooting Guide #

Connection Issues #

  • WebSocket connection failed:

    • Check that the server is running and the URI is correct
    • Verify that the port is not blocked by a firewall
    • Ensure the server is configured to accept WebSocket connections
    • Check for SSL/TLS certificate issues if using wss://
  • Connection timeout:

    • Increase the timeout value in the client configuration
    • Check network latency between client and server
    • Verify server is not overloaded
  • Connection refused:

    • Check firewall settings and server port configuration
    • Verify the server is running and listening on the specified port
    • Check if the server has reached its connection limit

Protocol Errors #

  • Invalid request:

    • Check that the request parameters are correct
    • Verify that the request format follows the JSON-RPC 2.0 specification
    • Ensure all required fields are provided
  • Method not found:

    • Verify that the server supports the requested method
    • Check capability negotiation to ensure the method is supported
    • Verify method name spelling and case sensitivity
  • Invalid params:

    • Ensure that the parameters match the expected format
    • Check parameter types and values
    • Verify that all required parameters are provided

Performance Optimization #

  • Use WebSocket transport for long-lived connections
  • Implement caching for frequently used resources
  • Use connection pooling for HTTP transport
  • Batch requests when possible to reduce network overhead
  • Implement retry logic with exponential backoff for transient errors

Documentation #

For more information about the Model Context Protocol, see:

For detailed API documentation, see the API Reference.

License #

This project is licensed under the MIT License - see the LICENSE file for details.

1
likes
160
points
32
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Flutter implementation of the Model Context Protocol (MCP) for integrating with AI tools like Windsurf, Cursor, and Claude.

Repository (GitHub)
View/report issues
Contributing

License

MIT (license)

Dependencies

cupertino_icons, equatable, flutter, http, json_annotation, logging, meta, stream_channel, uuid, web_socket_channel

More

Packages that depend on flutter_mcp_client_lib