main
dart 188 lines 5.16 KB
Raw
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 }