main
dart 278 lines 8.61 KB
Raw
1 import 'dart:convert';
2 import 'package:flutter_riverpod/flutter_riverpod.dart';
3 import '../../../core/providers/app_providers.dart';
4 import '../data/models/mcp_integration.dart';
5 import '../../conversations/providers/conversation_providers.dart';
6 import 'tooling_providers.dart';
7
8 export '../data/models/mcp_integration.dart';
9
10 class ChatMcpConfigNotifier extends Notifier<ChatMcpConfig> {
11 @override
12 ChatMcpConfig build() {
13 final savedIntegrations = ref.read(settingsProvider).savedMcpIntegrations;
14 return ChatMcpConfig(enabled: false, integrations: savedIntegrations);
15 }
16
17 void setConfig(ChatMcpConfig config) {
18 state = config;
19 final manager = ref.read(mcpServerManagerProvider);
20 manager.clear();
21 for (final integration in config.integrations) {
22 if (integration.serverLabel != null && integration.serverUrl != null) {
23 manager
24 .addServer(
25 integration.serverLabel!,
26 integration.serverUrl!,
27 headers: integration.headers,
28 )
29 .catchError((_) {});
30 }
31 }
32 }
33
34 void setEnabled(bool enabled) {
35 state = state.copyWith(enabled: enabled);
36 }
37
38 void updateEnabled(WidgetRef ref, String conversationId, bool enabled) {
39 state = state.copyWith(enabled: enabled);
40 ref
41 .read(conversationsProvider.notifier)
42 .updateMcpEnabled(conversationId, enabled);
43 }
44
45 void addIntegration(McpIntegration integration) {
46 state = state.copyWith(
47 integrations: [...state.integrations, integration],
48 activeMcpServers:
49 integration.serverLabel != null && integration.serverUrl != null
50 ? {
51 ...state.activeMcpServers,
52 integration.serverLabel!: integration.serverUrl!,
53 }
54 : state.activeMcpServers,
55 );
56 if (integration.serverLabel != null && integration.serverUrl != null) {
57 ref
58 .read(mcpServerManagerProvider)
59 .addServer(
60 integration.serverLabel!,
61 integration.serverUrl!,
62 headers: integration.headers,
63 )
64 .catchError((_) {});
65 }
66 ref.read(settingsProvider.notifier).addSavedMcpIntegration(integration);
67 }
68
69 bool addIntegrationFromInput({required String label, required String url}) {
70 final trimmedLabel = label.trim();
71 final trimmedUrl = url.trim();
72 final uri = Uri.tryParse(trimmedUrl);
73 final isRemoteMcpUrl =
74 uri != null &&
75 uri.hasAuthority &&
76 (uri.scheme == 'http' || uri.scheme == 'https');
77
78 if (isRemoteMcpUrl) {
79 if (trimmedLabel.isEmpty) return false;
80 addIntegration(
81 McpIntegration(
82 type: McpIntegrationType.ephemeralMcp,
83 serverLabel: trimmedLabel,
84 serverUrl: trimmedUrl,
85 ),
86 );
87 return true;
88 }
89
90 final pluginId = trimmedLabel.isNotEmpty ? trimmedLabel : trimmedUrl;
91 final hasBothInputs = trimmedLabel.isNotEmpty && trimmedUrl.isNotEmpty;
92 if (hasBothInputs || !pluginId.contains('/')) {
93 return false;
94 }
95
96 addIntegration(
97 McpIntegration(
98 type: McpIntegrationType.plugin,
99 pluginId: pluginId,
100 serverLabel: trimmedLabel.isNotEmpty ? trimmedLabel : null,
101 ),
102 );
103 return true;
104 }
105
106 void removeIntegration(int index) {
107 final integration = state.integrations[index];
108 final newIntegrations = List<McpIntegration>.from(state.integrations)
109 ..removeAt(index);
110 final newServers = Map<String, String>.from(state.activeMcpServers);
111 if (integration.serverLabel != null) {
112 newServers.remove(integration.serverLabel);
113 }
114 state = state.copyWith(
115 integrations: newIntegrations,
116 activeMcpServers: newServers,
117 );
118 if (integration.serverLabel != null) {
119 ref.read(mcpServerManagerProvider).removeServer(integration.serverLabel!);
120 }
121 final saved = ref.read(settingsProvider).savedMcpIntegrations;
122 final savedIdx = saved.indexWhere(
123 (i) =>
124 (integration.pluginId != null &&
125 i.pluginId == integration.pluginId) ||
126 (integration.serverUrl != null &&
127 i.serverUrl == integration.serverUrl),
128 );
129 if (savedIdx != -1) {
130 ref.read(settingsProvider.notifier).removeSavedMcpIntegration(savedIdx);
131 }
132 }
133
134 void toggleIntegration(int index, bool enabled) {
135 if (index < 0 || index >= state.integrations.length) return;
136 final integration = state.integrations[index];
137 final newIntegrations = List<McpIntegration>.from(state.integrations);
138 newIntegrations[index] = newIntegrations[index].copyWith(enabled: enabled);
139 state = state.copyWith(integrations: newIntegrations);
140
141 final saved = ref.read(settingsProvider).savedMcpIntegrations;
142 final savedIdx = saved.indexWhere(
143 (i) =>
144 (integration.pluginId != null &&
145 i.pluginId == integration.pluginId) ||
146 (integration.serverUrl != null &&
147 i.serverUrl == integration.serverUrl),
148 );
149 if (savedIdx != -1) {
150 ref
151 .read(settingsProvider.notifier)
152 .toggleSavedMcpIntegration(savedIdx, enabled);
153 }
154 }
155
156 int importIntegrationsFromJson(String jsonString) {
157 dynamic decoded;
158 try {
159 decoded = jsonDecode(jsonString);
160 } catch (_) {
161 return 0;
162 }
163
164 final newIntegrations = <McpIntegration>[];
165
166 String normalizePluginId(String rawId) {
167 final trimmed = rawId.trim();
168 if (trimmed.contains('/')) return trimmed;
169 return 'mcp/$trimmed';
170 }
171
172 void processMcpServers(Map<String, dynamic> servers) {
173 servers.forEach((key, value) {
174 if (value is Map) {
175 final url = value['url'] as String? ?? value['server_url'] as String?;
176 if (url != null && url.isNotEmpty) {
177 newIntegrations.add(
178 McpIntegration(
179 type: McpIntegrationType.ephemeralMcp,
180 serverLabel: key,
181 serverUrl: url,
182 enabled: true,
183 ),
184 );
185 } else {
186 final pluginId = normalizePluginId(key);
187 newIntegrations.add(
188 McpIntegration(
189 type: McpIntegrationType.plugin,
190 pluginId: pluginId,
191 serverLabel: key,
192 enabled: true,
193 ),
194 );
195 }
196 }
197 });
198 }
199
200 if (decoded is Map<String, dynamic>) {
201 if (decoded.containsKey('mcpServers') && decoded['mcpServers'] is Map) {
202 processMcpServers(decoded['mcpServers'] as Map<String, dynamic>);
203 } else if (decoded.containsKey('integrations') &&
204 decoded['integrations'] is List) {
205 final list = decoded['integrations'] as List;
206 for (final item in list) {
207 if (item is String) {
208 final pluginId = normalizePluginId(item);
209 newIntegrations.add(
210 McpIntegration(
211 type: McpIntegrationType.plugin,
212 pluginId: pluginId,
213 enabled: true,
214 ),
215 );
216 } else if (item is Map<String, dynamic>) {
217 newIntegrations.add(McpIntegration.fromJson(item));
218 }
219 }
220 } else {
221 processMcpServers(decoded);
222 }
223 } else if (decoded is List) {
224 for (final item in decoded) {
225 if (item is String) {
226 final pluginId = normalizePluginId(item);
227 newIntegrations.add(
228 McpIntegration(
229 type: McpIntegrationType.plugin,
230 pluginId: pluginId,
231 enabled: true,
232 ),
233 );
234 } else if (item is Map<String, dynamic>) {
235 newIntegrations.add(McpIntegration.fromJson(item));
236 }
237 }
238 }
239
240 if (newIntegrations.isEmpty) return 0;
241
242 int addedCount = 0;
243 for (final integration in newIntegrations) {
244 final exists = state.integrations.any((existing) {
245 if (integration.type == McpIntegrationType.plugin) {
246 return existing.pluginId == integration.pluginId;
247 }
248 return existing.serverUrl == integration.serverUrl &&
249 existing.serverLabel == integration.serverLabel;
250 });
251
252 if (!exists) {
253 addIntegration(integration);
254 addedCount++;
255 }
256 }
257
258 return addedCount;
259 }
260
261 void toggleEnabled() {
262 state = state.copyWith(enabled: !state.enabled);
263 }
264
265 void clearAll() {
266 final settings = ref.read(settingsProvider);
267 state = ChatMcpConfig(
268 enabled: settings.mcpEnabled && settings.newChatMcpEnabled,
269 integrations: settings.savedMcpIntegrations,
270 );
271 ref.read(mcpServerManagerProvider).clear();
272 }
273 }
274
275 final chatMcpConfigProvider =
276 NotifierProvider<ChatMcpConfigNotifier, ChatMcpConfig>(() {
277 return ChatMcpConfigNotifier();
278 });