| 1 | // ignore_for_file: prefer_initializing_formals |
| 2 | |
| 3 | import 'mcp_client.dart'; |
| 4 | |
| 5 | const exampleMcpServerLabel = 'Example MCP'; |
| 6 | const exampleMcpServerUrl = 'local://example-mcp'; |
| 7 | |
| 8 | class McpServerManager { |
| 9 | final String _appVersion; |
| 10 | final Map<String, McpClient> _clients = {}; |
| 11 | final Map<String, McpCapabilities> _capabilities = {}; |
| 12 | final Map<String, List<McpTool>> _tools = {}; |
| 13 | final Map<String, String> _serverUrls = {}; |
| 14 | final Set<String> _localExampleServers = {}; |
| 15 | final Set<String> _pendingLabels = {}; |
| 16 | |
| 17 | McpServerManager({String appVersion = '1.0.0'}) : _appVersion = appVersion; |
| 18 | |
| 19 | Future<void> addServer( |
| 20 | String label, |
| 21 | String url, { |
| 22 | Map<String, String>? headers, |
| 23 | }) async { |
| 24 | // Guard against concurrent addServer calls for the same label. |
| 25 | if (_pendingLabels.contains(label)) return; |
| 26 | _pendingLabels.add(label); |
| 27 | |
| 28 | try { |
| 29 | if (_clients.containsKey(label)) { |
| 30 | final oldClient = _clients.remove(label); |
| 31 | await oldClient?.close(); |
| 32 | _capabilities.remove(label); |
| 33 | _tools.remove(label); |
| 34 | _serverUrls.remove(label); |
| 35 | } |
| 36 | |
| 37 | final client = McpClient( |
| 38 | serverUrl: url, |
| 39 | headers: headers, |
| 40 | version: _appVersion, |
| 41 | ); |
| 42 | |
| 43 | final capabilities = await client.initialize(); |
| 44 | final tools = await client.listTools(); |
| 45 | |
| 46 | _clients[label] = client; |
| 47 | _capabilities[label] = capabilities; |
| 48 | _tools[label] = tools; |
| 49 | _serverUrls[label] = url; |
| 50 | } finally { |
| 51 | _pendingLabels.remove(label); |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | Future<void> removeServer(String label) async { |
| 56 | final client = _clients.remove(label); |
| 57 | await client?.close(); |
| 58 | _capabilities.remove(label); |
| 59 | _tools.remove(label); |
| 60 | _serverUrls.remove(label); |
| 61 | _localExampleServers.remove(label); |
| 62 | } |
| 63 | |
| 64 | Future<void> addExampleServer() async { |
| 65 | await removeServer(exampleMcpServerLabel); |
| 66 | |
| 67 | _capabilities[exampleMcpServerLabel] = const McpCapabilities(tools: true); |
| 68 | _tools[exampleMcpServerLabel] = const [ |
| 69 | McpTool( |
| 70 | name: 'example.echo', |
| 71 | description: 'Echo a message back from the example MCP server.', |
| 72 | inputSchema: { |
| 73 | 'type': 'object', |
| 74 | 'properties': { |
| 75 | 'message': { |
| 76 | 'type': 'string', |
| 77 | 'description': 'The message to echo.', |
| 78 | }, |
| 79 | }, |
| 80 | 'required': ['message'], |
| 81 | }, |
| 82 | ), |
| 83 | McpTool( |
| 84 | name: 'example.word_count', |
| 85 | description: 'Count the words in a text string.', |
| 86 | inputSchema: { |
| 87 | 'type': 'object', |
| 88 | 'properties': { |
| 89 | 'text': { |
| 90 | 'type': 'string', |
| 91 | 'description': 'The text to count words in.', |
| 92 | }, |
| 93 | }, |
| 94 | 'required': ['text'], |
| 95 | }, |
| 96 | ), |
| 97 | ]; |
| 98 | _serverUrls[exampleMcpServerLabel] = exampleMcpServerUrl; |
| 99 | _localExampleServers.add(exampleMcpServerLabel); |
| 100 | } |
| 101 | |
| 102 | bool hasServer(String label) => |
| 103 | _clients.containsKey(label) || _localExampleServers.contains(label); |
| 104 | |
| 105 | bool hasExampleServer() => |
| 106 | _localExampleServers.contains(exampleMcpServerLabel); |
| 107 | |
| 108 | List<McpTool> getTools(String label) => _tools[label] ?? []; |
| 109 | |
| 110 | Map<String, List<McpTool>> get allTools => Map.unmodifiable(_tools); |
| 111 | |
| 112 | McpCapabilities? getCapabilities(String label) => _capabilities[label]; |
| 113 | |
| 114 | String? getServerUrl(String label) => _serverUrls[label]; |
| 115 | |
| 116 | Future<String> callTool( |
| 117 | String serverLabel, |
| 118 | String toolName, |
| 119 | Map<String, dynamic> args, |
| 120 | ) async { |
| 121 | if (_localExampleServers.contains(serverLabel)) { |
| 122 | return _callExampleTool(toolName, args); |
| 123 | } |
| 124 | |
| 125 | final client = _clients[serverLabel]; |
| 126 | if (client == null) { |
| 127 | throw McpException('MCP server not connected: $serverLabel'); |
| 128 | } |
| 129 | |
| 130 | if (!client.isInitialized) { |
| 131 | await client.initialize(); |
| 132 | } |
| 133 | |
| 134 | return client.callTool(toolName, args); |
| 135 | } |
| 136 | |
| 137 | Future<String> readResource(String serverLabel, String uri) async { |
| 138 | final client = _clients[serverLabel]; |
| 139 | if (client == null) { |
| 140 | throw McpException('MCP server not connected: $serverLabel'); |
| 141 | } |
| 142 | |
| 143 | if (!client.isInitialized) { |
| 144 | await client.initialize(); |
| 145 | } |
| 146 | |
| 147 | return client.readResource(uri); |
| 148 | } |
| 149 | |
| 150 | Future<void> clear() async { |
| 151 | for (final client in _clients.values) { |
| 152 | await client.close(); |
| 153 | } |
| 154 | _clients.clear(); |
| 155 | _capabilities.clear(); |
| 156 | _tools.clear(); |
| 157 | _serverUrls.clear(); |
| 158 | _localExampleServers.clear(); |
| 159 | } |
| 160 | |
| 161 | String _callExampleTool(String toolName, Map<String, dynamic> args) { |
| 162 | switch (toolName) { |
| 163 | case 'example.echo': |
| 164 | final message = args['message']; |
| 165 | if (message is! String) { |
| 166 | throw McpException('example.echo requires a string message'); |
| 167 | } |
| 168 | return message; |
| 169 | case 'example.word_count': |
| 170 | final text = args['text']; |
| 171 | if (text is! String) { |
| 172 | throw McpException('example.word_count requires a string text value'); |
| 173 | } |
| 174 | final words = text |
| 175 | .trim() |
| 176 | .split(RegExp(r'\s+')) |
| 177 | .where((word) => word.isNotEmpty) |
| 178 | .length; |
| 179 | return words.toString(); |
| 180 | default: |
| 181 | throw McpException('Example MCP tool not found: $toolName'); |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | int get serverCount => _tools.length; |
| 186 | |
| 187 | List<String> get serverLabels => _tools.keys.toList(); |
| 188 | } |