@tduncan / Cortana / commits / 6c0d9ad

feat: implement persistent MCP configuration storage with JSON import functionality and individual integration toggles.

abdulmominsakib committed Aug 1, 2026 at 15:04 UTC 6c0d9ad096dadb2c414ad329b0ee195a3d526558
24 files changed +1159 -34
lib/core/providers/app_providers.dart
+28
@@ -2,6 +2,7 @@ import 'package:flutter_gemma/flutter_gemma.dart';
2 import 'package:flutter_riverpod/flutter_riverpod.dart';
3 import 'package:package_info_plus/package_info_plus.dart';
4
5 +import '../../features/chat/data/models/mcp_integration.dart';
6 import '../../features/chat/utils/image_upload_utils.dart';
7 import '../../features/settings/data/models/app_settings.dart';
8 import '../models/enums.dart';
@@ -132,6 +133,33 @@ class SettingsNotifier extends Notifier<AppSettings> {
133 void setShowSystemMessagesInChat(bool value) =>
134 _update(state.copyWith(showSystemMessagesInChat: value));
135
136 + void addSavedMcpIntegration(McpIntegration integration) {
137 + final current = List<McpIntegration>.from(state.savedMcpIntegrations);
138 + final exists = current.any((i) =>
139 + (integration.pluginId != null && i.pluginId == integration.pluginId) ||
140 + (integration.serverUrl != null && i.serverUrl == integration.serverUrl));
141 + if (!exists) {
142 + current.add(integration);
143 + _update(state.copyWith(savedMcpIntegrations: current));
144 + }
145 + }
146 +
147 + void removeSavedMcpIntegration(int index) {
148 + if (index >= 0 && index < state.savedMcpIntegrations.length) {
149 + final current = List<McpIntegration>.from(state.savedMcpIntegrations)
150 + ..removeAt(index);
151 + _update(state.copyWith(savedMcpIntegrations: current));
152 + }
153 + }
154 +
155 + void toggleSavedMcpIntegration(int index, bool enabled) {
156 + if (index >= 0 && index < state.savedMcpIntegrations.length) {
157 + final current = List<McpIntegration>.from(state.savedMcpIntegrations);
158 + current[index] = current[index].copyWith(enabled: enabled);
159 + _update(state.copyWith(savedMcpIntegrations: current));
160 + }
161 + }
162 +
163 Future<void> _update(AppSettings updated) async {
164 state = updated;
165 final prefs = ref.read(sharedPreferencesProvider);
lib/features/chat/data/chat_service.dart
+2 -2
@@ -238,7 +238,7 @@ class LMStudioChatService implements ChatService {
238 // sending them would be silently ignored. The local tool registry is
239 // therefore only useful for non-LM-Studio server types on this build.
240 if (integrations != null && integrations.isNotEmpty) {
241 - body['integrations'] = integrations.map((i) => i.toJson()).toList();
241 + body['integrations'] = integrations.map((i) => i.toApiJson()).toList();
242 }
243
244 try {
@@ -659,7 +659,7 @@ class OpenAICompatibleChatService implements ChatService {
659 _applyReasoningControl(body, params);
660
661 if (integrations != null && integrations.isNotEmpty) {
662 - body['integrations'] = integrations.map((i) => i.toJson()).toList();
662 + body['integrations'] = integrations.map((i) => i.toApiJson()).toList();
663 }
664
665 if (tools != null && tools.isNotEmpty) {
lib/features/chat/data/models/mcp_integration.dart
+13 -1
@@ -7,6 +7,7 @@ class McpIntegration {
7 final String? pluginId;
8 final List<String>? allowedTools;
9 final Map<String, String>? headers;
10 + final bool enabled;
11
12 const McpIntegration({
13 required this.type,
@@ -15,9 +16,11 @@ class McpIntegration {
16 this.pluginId,
17 this.allowedTools,
18 this.headers,
19 + this.enabled = true,
20 });
21
20 - Map<String, dynamic> toJson() {
22 + /// Returns the clean API payload expected by LM Studio native endpoint.
23 + Map<String, dynamic> toApiJson() {
24 if (type == McpIntegrationType.plugin) {
25 return {
26 'type': 'plugin',
@@ -34,6 +37,12 @@ class McpIntegration {
37 };
38 }
39
40 + Map<String, dynamic> toJson() {
41 + final map = toApiJson();
42 + map['enabled'] = enabled;
43 + return map;
44 + }
45 +
46 factory McpIntegration.fromJson(Map<String, dynamic> json) {
47 final typeStr = json['type'] as String;
48 final type = typeStr == 'plugin'
@@ -47,6 +56,7 @@ class McpIntegration {
56 pluginId: json['id'] as String?,
57 allowedTools: (json['allowed_tools'] as List?)?.cast<String>(),
58 headers: (json['headers'] as Map?)?.cast<String, String>(),
59 + enabled: json['enabled'] as bool? ?? true,
60 );
61 }
62
@@ -57,6 +67,7 @@ class McpIntegration {
67 String? pluginId,
68 List<String>? allowedTools,
69 Map<String, String>? headers,
70 + bool? enabled,
71 }) {
72 return McpIntegration(
73 type: type ?? this.type,
@@ -65,6 +76,7 @@ class McpIntegration {
76 pluginId: pluginId ?? this.pluginId,
77 allowedTools: allowedTools ?? this.allowedTools,
78 headers: headers ?? this.headers,
79 + enabled: enabled ?? this.enabled,
80 );
81 }
82 }
lib/features/chat/providers/chat_mcp_providers.dart
+135 -4
@@ -1,3 +1,4 @@
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';
@@ -9,10 +10,11 @@ export '../data/models/mcp_integration.dart';
10 class ChatMcpConfigNotifier extends Notifier<ChatMcpConfig> {
11 @override
12 ChatMcpConfig build() {
12 - // We no longer watch settingsProvider here to avoid resetting per-chat state
13 - // when global settings change. Initial state is set via ChatNotifier.
14 - // Default to disabled so a fresh app launch does not silently enable MCP.
15 - return const ChatMcpConfig(enabled: false);
13 + final savedIntegrations = ref.read(settingsProvider).savedMcpIntegrations;
14 + return ChatMcpConfig(
15 + enabled: false,
16 + integrations: savedIntegrations,
17 + );
18 }
19
20 void setConfig(ChatMcpConfig config) {
@@ -59,6 +61,7 @@ class ChatMcpConfigNotifier extends Notifier<ChatMcpConfig> {
61 headers: integration.headers,
62 ).catchError((_) {});
63 }
64 + ref.read(settingsProvider.notifier).addSavedMcpIntegration(integration);
65 }
66
67 void removeIntegration(int index) {
@@ -76,6 +79,133 @@ class ChatMcpConfigNotifier extends Notifier<ChatMcpConfig> {
79 if (integration.serverLabel != null) {
80 ref.read(mcpServerManagerProvider).removeServer(integration.serverLabel!);
81 }
82 + final saved = ref.read(settingsProvider).savedMcpIntegrations;
83 + final savedIdx = saved.indexWhere((i) =>
84 + (integration.pluginId != null && i.pluginId == integration.pluginId) ||
85 + (integration.serverUrl != null && i.serverUrl == integration.serverUrl));
86 + if (savedIdx != -1) {
87 + ref.read(settingsProvider.notifier).removeSavedMcpIntegration(savedIdx);
88 + }
89 + }
90 +
91 + void toggleIntegration(int index, bool enabled) {
92 + if (index < 0 || index >= state.integrations.length) return;
93 + final integration = state.integrations[index];
94 + final newIntegrations = List<McpIntegration>.from(state.integrations);
95 + newIntegrations[index] = newIntegrations[index].copyWith(enabled: enabled);
96 + state = state.copyWith(integrations: newIntegrations);
97 +
98 + final saved = ref.read(settingsProvider).savedMcpIntegrations;
99 + final savedIdx = saved.indexWhere((i) =>
100 + (integration.pluginId != null && i.pluginId == integration.pluginId) ||
101 + (integration.serverUrl != null && i.serverUrl == integration.serverUrl));
102 + if (savedIdx != -1) {
103 + ref.read(settingsProvider.notifier).toggleSavedMcpIntegration(savedIdx, enabled);
104 + }
105 + }
106 +
107 + int importIntegrationsFromJson(String jsonString) {
108 + dynamic decoded;
109 + try {
110 + decoded = jsonDecode(jsonString);
111 + } catch (_) {
112 + return 0;
113 + }
114 +
115 + final newIntegrations = <McpIntegration>[];
116 +
117 + String normalizePluginId(String rawId) {
118 + final trimmed = rawId.trim();
119 + if (trimmed.contains('/')) return trimmed;
120 + return 'mcp/$trimmed';
121 + }
122 +
123 + void processMcpServers(Map<String, dynamic> servers) {
124 + servers.forEach((key, value) {
125 + if (value is Map) {
126 + final url = value['url'] as String? ?? value['server_url'] as String?;
127 + if (url != null && url.isNotEmpty) {
128 + newIntegrations.add(
129 + McpIntegration(
130 + type: McpIntegrationType.ephemeralMcp,
131 + serverLabel: key,
132 + serverUrl: url,
133 + enabled: true,
134 + ),
135 + );
136 + } else {
137 + final pluginId = normalizePluginId(key);
138 + newIntegrations.add(
139 + McpIntegration(
140 + type: McpIntegrationType.plugin,
141 + pluginId: pluginId,
142 + serverLabel: key,
143 + enabled: true,
144 + ),
145 + );
146 + }
147 + }
148 + });
149 + }
150 +
151 + if (decoded is Map<String, dynamic>) {
152 + if (decoded.containsKey('mcpServers') && decoded['mcpServers'] is Map) {
153 + processMcpServers(decoded['mcpServers'] as Map<String, dynamic>);
154 + } else if (decoded.containsKey('integrations') && decoded['integrations'] is List) {
155 + final list = decoded['integrations'] as List;
156 + for (final item in list) {
157 + if (item is String) {
158 + final pluginId = normalizePluginId(item);
159 + newIntegrations.add(
160 + McpIntegration(
161 + type: McpIntegrationType.plugin,
162 + pluginId: pluginId,
163 + enabled: true,
164 + ),
165 + );
166 + } else if (item is Map<String, dynamic>) {
167 + newIntegrations.add(McpIntegration.fromJson(item));
168 + }
169 + }
170 + } else {
171 + processMcpServers(decoded);
172 + }
173 + } else if (decoded is List) {
174 + for (final item in decoded) {
175 + if (item is String) {
176 + final pluginId = normalizePluginId(item);
177 + newIntegrations.add(
178 + McpIntegration(
179 + type: McpIntegrationType.plugin,
180 + pluginId: pluginId,
181 + enabled: true,
182 + ),
183 + );
184 + } else if (item is Map<String, dynamic>) {
185 + newIntegrations.add(McpIntegration.fromJson(item));
186 + }
187 + }
188 + }
189 +
190 + if (newIntegrations.isEmpty) return 0;
191 +
192 + int addedCount = 0;
193 + for (final integration in newIntegrations) {
194 + final exists = state.integrations.any((existing) {
195 + if (integration.type == McpIntegrationType.plugin) {
196 + return existing.pluginId == integration.pluginId;
197 + }
198 + return existing.serverUrl == integration.serverUrl &&
199 + existing.serverLabel == integration.serverLabel;
200 + });
201 +
202 + if (!exists) {
203 + addIntegration(integration);
204 + addedCount++;
205 + }
206 + }
207 +
208 + return addedCount;
209 }
210
211
@@ -87,6 +217,7 @@ class ChatMcpConfigNotifier extends Notifier<ChatMcpConfig> {
217 final settings = ref.read(settingsProvider);
218 state = ChatMcpConfig(
219 enabled: settings.mcpEnabled && settings.newChatMcpEnabled,
220 + integrations: settings.savedMcpIntegrations,
221 );
222 ref.read(mcpServerManagerProvider).clear();
223 }
lib/features/chat/providers/chat_notifier.dart
+8 -4
@@ -874,9 +874,11 @@ class ChatNotifier extends Notifier<ChatState> {
874
875 if (chatService != null) {
876 final mcpConfig = ref.read(chatMcpConfigProvider);
877 + final activeIntegrations =
878 + mcpConfig.integrations.where((i) => i.enabled).toList();
879 final integrations =
878 - mcpConfig.enabled && mcpConfig.integrations.isNotEmpty
879 - ? mcpConfig.integrations
880 + mcpConfig.enabled && activeIntegrations.isNotEmpty
881 + ? activeIntegrations
882 : null;
883
884 final registry = ref.read(toolRegistryProvider);
@@ -1934,9 +1936,11 @@ class ChatNotifier extends Notifier<ChatState> {
1936 var isFirstContinueChunk = continueGeneration;
1937
1938 final mcpConfig = ref.read(chatMcpConfigProvider);
1939 + final activeIntegrations =
1940 + mcpConfig.integrations.where((i) => i.enabled).toList();
1941 final integrations =
1938 - mcpConfig.enabled && mcpConfig.integrations.isNotEmpty
1939 - ? mcpConfig.integrations
1942 + mcpConfig.enabled && activeIntegrations.isNotEmpty
1943 + ? activeIntegrations
1944 : null;
1945
1946 final registry = ref.read(toolRegistryProvider);
lib/features/chat/views/components/chat_settings_sheet.dart
+175 -15
@@ -277,7 +277,32 @@ class _ChatSettingsSheetState extends ConsumerState<ChatSettingsSheet> {
277 ),
278 const SizedBox(height: 16),
279 if (mcpConfig.enabled && isGloballyEnabled) ...[
280 - Text(l10n.add_ephemeral_mcp, style: theme.textTheme.list),
280 + Row(
281 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
282 + children: [
283 + Expanded(
284 + child: Text(
285 + l10n.add_ephemeral_mcp,
286 + style: theme.textTheme.list,
287 + ),
288 + ),
289 + ShadButton.outline(
290 + size: ShadButtonSize.sm,
291 + onPressed: () => _showImportJsonDialog(context),
292 + child: Row(
293 + mainAxisSize: MainAxisSize.min,
294 + children: [
295 + const HugeIcon(
296 + icon: HugeIcons.strokeRoundedFileImport,
297 + size: 16,
298 + ),
299 + const SizedBox(width: 4),
300 + Text(l10n.import_mcp_json),
301 + ],
302 + ),
303 + ),
304 + ],
305 + ),
306 const SizedBox(height: 12),
307 Row(
308 children: [
@@ -329,27 +354,54 @@ class _ChatSettingsSheetState extends ConsumerState<ChatSettingsSheet> {
354 ),
355 child: ListTile(
356 dense: true,
332 - leading: const HugeIcon(
357 + leading: HugeIcon(
358 icon: HugeIcons.strokeRoundedPuzzle,
359 size: 18,
360 + color: integration.enabled
361 + ? null
362 + : Colors.grey,
363 ),
364 title: Text(
365 integration.serverLabel ?? integration.pluginId ?? '',
338 - style: const TextStyle(fontWeight: FontWeight.w600),
366 + style: TextStyle(
367 + fontWeight: FontWeight.w600,
368 + decoration: integration.enabled
369 + ? TextDecoration.none
370 + : TextDecoration.lineThrough,
371 + color: integration.enabled
372 + ? null
373 + : (isDark
374 + ? AppColors.darkMutedText
375 + : AppColors.lightMutedText),
376 + ),
377 ),
378 subtitle: Text(
341 - integration.serverUrl ?? '',
379 + integration.type == McpIntegrationType.plugin
380 + ? 'Plugin (${integration.pluginId})'
381 + : (integration.serverUrl ?? ''),
382 style: const TextStyle(fontSize: 11),
383 ),
344 - trailing: ShadIconButton.ghost(
345 - icon: const HugeIcon(icon:
346 - HugeIcons.strokeRoundedDelete01,
347 - size: 18,
348 - color: Colors.red,
349 - ),
350 - onPressed: () => ref
351 - .read(chatMcpConfigProvider.notifier)
352 - .removeIntegration(index),
384 + trailing: Row(
385 + mainAxisSize: MainAxisSize.min,
386 + children: [
387 + ShadSwitch(
388 + value: integration.enabled,
389 + onChanged: (v) => ref
390 + .read(chatMcpConfigProvider.notifier)
391 + .toggleIntegration(index, v),
392 + ),
393 + const SizedBox(width: 4),
394 + ShadIconButton.ghost(
395 + icon: const HugeIcon(
396 + icon: HugeIcons.strokeRoundedDelete01,
397 + size: 18,
398 + color: Colors.red,
399 + ),
400 + onPressed: () => ref
401 + .read(chatMcpConfigProvider.notifier)
402 + .removeIntegration(index),
403 + ),
404 + ],
405 ),
406 ),
407 );
@@ -363,17 +415,125 @@ class _ChatSettingsSheetState extends ConsumerState<ChatSettingsSheet> {
415 );
416 }
417
418 + void _showImportJsonDialog(BuildContext context) {
419 + final l10n = AppLocalizations.of(context)!;
420 + final theme = Theme.of(context);
421 + final isDark = theme.brightness == Brightness.dark;
422 + final jsonController = TextEditingController();
423 +
424 + showDialog<void>(
425 + context: context,
426 + builder: (dialogContext) {
427 + return AlertDialog(
428 + shape: RoundedRectangleBorder(
429 + borderRadius: BorderRadius.circular(16),
430 + ),
431 + title: Row(
432 + children: [
433 + const HugeIcon(
434 + icon: HugeIcons.strokeRoundedFileImport,
435 + size: 22,
436 + ),
437 + const SizedBox(width: 8),
438 + Expanded(
439 + child: Text(
440 + l10n.import_mcp_json_dialog_title,
441 + style: const TextStyle(
442 + fontSize: 16,
443 + fontWeight: FontWeight.bold,
444 + ),
445 + ),
446 + ),
447 + ],
448 + ),
449 + content: SingleChildScrollView(
450 + child: Column(
451 + mainAxisSize: MainAxisSize.min,
452 + crossAxisAlignment: CrossAxisAlignment.start,
453 + children: [
454 + Container(
455 + padding: const EdgeInsets.all(12),
456 + decoration: BoxDecoration(
457 + color: isDark
458 + ? AppColors.darkBackground
459 + : AppColors.lightSurface,
460 + borderRadius: BorderRadius.circular(8),
461 + border: Border.all(
462 + color: isDark
463 + ? AppColors.darkSurfaceCard
464 + : AppColors.lightBorder,
465 + ),
466 + ),
467 + child: Text(
468 + l10n.import_mcp_json_instructions,
469 + style: TextStyle(
470 + fontSize: 12,
471 + color: isDark
472 + ? AppColors.darkMutedText
473 + : AppColors.lightMutedText,
474 + ),
475 + ),
476 + ),
477 + const SizedBox(height: 12),
478 + ShadInput(
479 + controller: jsonController,
480 + maxLines: 8,
481 + placeholder: Text(
482 + l10n.import_mcp_json_placeholder,
483 + style: const TextStyle(fontSize: 11),
484 + ),
485 + ),
486 + ],
487 + ),
488 + ),
489 + actions: [
490 + ShadButton.ghost(
491 + onPressed: () => Navigator.of(dialogContext).pop(),
492 + child: Text(l10n.cancel),
493 + ),
494 + ShadButton(
495 + onPressed: () {
496 + final jsonStr = jsonController.text.trim();
497 + if (jsonStr.isNotEmpty) {
498 + final importedCount = ref
499 + .read(chatMcpConfigProvider.notifier)
500 + .importIntegrationsFromJson(jsonStr);
501 + Navigator.of(dialogContext).pop();
502 +
503 + ScaffoldMessenger.of(context).showSnackBar(
504 + SnackBar(
505 + content: Text(
506 + importedCount > 0
507 + ? l10n.mcp_import_success(importedCount)
508 + : l10n.mcp_import_failed,
509 + ),
510 + backgroundColor:
511 + importedCount > 0 ? Colors.green : Colors.red,
512 + ),
513 + );
514 + }
515 + },
516 + child: Text(l10n.import_mcp_json),
517 + ),
518 + ],
519 + );
520 + },
521 + );
522 + }
523 +
524 void _addServer() {
525 final label = _serverLabelController.text.trim();
526 final url = _serverUrlController.text.trim();
527 if (label.isEmpty && url.isEmpty) return;
528
529 final McpIntegration integration;
372 - if (label.startsWith('mcp/') || url.startsWith('mcp/')) {
373 - final pluginId = label.startsWith('mcp/') ? label : url;
530 + if (label.contains('/') || url.contains('/')) {
531 + final raw = label.isNotEmpty ? label : url;
532 + final pluginId = raw.contains('/') ? raw : 'mcp/$raw';
533 integration = McpIntegration(
534 type: McpIntegrationType.plugin,
535 pluginId: pluginId,
536 + serverLabel: label.isNotEmpty ? label : null,
537 );
538 } else {
539 if (label.isEmpty || url.isEmpty) return;
lib/features/mcp/views/mcp_tools_screen.dart
+313 -7
@@ -6,8 +6,10 @@ import 'package:shadcn_ui/shadcn_ui.dart';
6
7 import '../../chat/data/mcp_server_manager.dart';
8 import '../../chat/data/tools/tool_definition.dart';
9 +import '../../chat/providers/chat_mcp_providers.dart';
10 import '../../chat/providers/tooling_providers.dart';
11 import '../../../core/providers/app_providers.dart';
12 +import '../../../core/theme/colors.dart';
13 import '../../../core/utils/system_insets.dart';
14
15 class McpToolsScreen extends ConsumerStatefulWidget {
@@ -156,6 +158,12 @@ class _McpToolsScreenState extends ConsumerState<McpToolsScreen> {
158 crossAxisAlignment: CrossAxisAlignment.stretch,
159 children: [
160 mcpCard,
161 + if (settings.mcpEnabled) ...[
162 + const SizedBox(height: 16),
163 + _ConfiguredMcpServersCard(
164 + onServersChanged: _refreshTools,
165 + ),
166 + ],
167 const SizedBox(height: 16),
168 toolsCard,
169 ],
@@ -231,12 +239,14 @@ class _McpSectionCard extends StatelessWidget {
239 required this.children,
240 this.icon = HugeIcons.strokeRoundedMcpServer,
241 this.accent = const Color(0xFF8B5CF6),
242 + this.trailing,
243 });
244
245 final String title;
246 final List<List<dynamic>> icon;
247 final Color accent;
248 final List<Widget> children;
249 + final Widget? trailing;
250
251 @override
252 Widget build(BuildContext context) {
@@ -269,10 +279,11 @@ class _McpSectionCard extends StatelessWidget {
279 child: Text(
280 title,
281 style: Theme.of(context).textTheme.titleMedium?.copyWith(
272 - fontWeight: FontWeight.w700,
273 - ),
282 + fontWeight: FontWeight.w700,
283 + ),
284 ),
285 ),
286 + if (trailing != null) ...[const SizedBox(width: 8), trailing!],
287 ],
288 ),
289 const SizedBox(height: 12),
@@ -524,10 +535,10 @@ class _FeatureBadge extends StatelessWidget {
535 child: Text(
536 label.toUpperCase(),
537 style: Theme.of(context).textTheme.labelSmall?.copyWith(
527 - color: const Color(0xFFB45309),
528 - fontWeight: FontWeight.w800,
529 - letterSpacing: 0.45,
530 - ),
538 + color: const Color(0xFFB45309),
539 + fontWeight: FontWeight.w800,
540 + letterSpacing: 0.45,
541 + ),
542 ),
543 );
544 }
@@ -555,4 +566,299 @@ Color _surfaceColor(BuildContext context) {
566
567 Color _outlineColor(BuildContext context, {double alpha = 0.6}) {
568 return ShadTheme.of(context).colorScheme.border.withValues(alpha: alpha);
558 -}
\ No newline at end of file
569 +}
570 +
571 +class _ConfiguredMcpServersCard extends ConsumerStatefulWidget {
572 + const _ConfiguredMcpServersCard({required this.onServersChanged});
573 +
574 + final VoidCallback onServersChanged;
575 +
576 + @override
577 + ConsumerState<_ConfiguredMcpServersCard> createState() =>
578 + __ConfiguredMcpServersCardState();
579 +}
580 +
581 +class __ConfiguredMcpServersCardState
582 + extends ConsumerState<_ConfiguredMcpServersCard> {
583 + final _serverLabelController = TextEditingController();
584 + final _serverUrlController = TextEditingController();
585 +
586 + @override
587 + void dispose() {
588 + _serverLabelController.dispose();
589 + _serverUrlController.dispose();
590 + super.dispose();
591 + }
592 +
593 + void _addServer() {
594 + final label = _serverLabelController.text.trim();
595 + final url = _serverUrlController.text.trim();
596 + if (label.isEmpty && url.isEmpty) return;
597 +
598 + final McpIntegration integration;
599 + if (label.contains('/') || url.contains('/')) {
600 + final raw = label.isNotEmpty ? label : url;
601 + final pluginId = raw.contains('/') ? raw : 'mcp/$raw';
602 + integration = McpIntegration(
603 + type: McpIntegrationType.plugin,
604 + pluginId: pluginId,
605 + serverLabel: label.isNotEmpty ? label : null,
606 + );
607 + } else {
608 + if (label.isEmpty || url.isEmpty) return;
609 + integration = McpIntegration(
610 + type: McpIntegrationType.ephemeralMcp,
611 + serverLabel: label,
612 + serverUrl: url,
613 + );
614 + }
615 +
616 + ref.read(settingsProvider.notifier).addSavedMcpIntegration(integration);
617 + ref.read(chatMcpConfigProvider.notifier).addIntegration(integration);
618 + _serverLabelController.clear();
619 + _serverUrlController.clear();
620 + widget.onServersChanged();
621 + }
622 +
623 + void _showImportJsonDialog(BuildContext context) {
624 + final l10n = AppLocalizations.of(context)!;
625 + final theme = Theme.of(context);
626 + final isDark = theme.brightness == Brightness.dark;
627 + final jsonController = TextEditingController();
628 +
629 + showDialog<void>(
630 + context: context,
631 + builder: (dialogContext) {
632 + return AlertDialog(
633 + shape: RoundedRectangleBorder(
634 + borderRadius: BorderRadius.circular(16),
635 + ),
636 + title: Row(
637 + children: [
638 + const HugeIcon(icon: HugeIcons.strokeRoundedFileImport, size: 22),
639 + const SizedBox(width: 8),
640 + Expanded(
641 + child: Text(
642 + l10n.import_mcp_json_dialog_title,
643 + style: const TextStyle(
644 + fontSize: 16,
645 + fontWeight: FontWeight.bold,
646 + ),
647 + ),
648 + ),
649 + ],
650 + ),
651 + content: SingleChildScrollView(
652 + padding: EdgeInsets.zero,
653 + child: Column(
654 + mainAxisSize: MainAxisSize.min,
655 + crossAxisAlignment: CrossAxisAlignment.start,
656 + children: [
657 + Container(
658 + padding: const EdgeInsets.all(12),
659 + decoration: BoxDecoration(
660 + color: isDark
661 + ? AppColors.darkBackground
662 + : AppColors.lightSurface,
663 + borderRadius: BorderRadius.circular(8),
664 + border: Border.all(
665 + color: isDark
666 + ? AppColors.darkSurfaceCard
667 + : AppColors.lightBorder,
668 + ),
669 + ),
670 + child: Text(
671 + l10n.import_mcp_json_instructions,
672 + style: TextStyle(
673 + fontSize: 12,
674 + color: isDark
675 + ? AppColors.darkMutedText
676 + : AppColors.lightMutedText,
677 + ),
678 + ),
679 + ),
680 + const SizedBox(height: 12),
681 + ShadInput(
682 + controller: jsonController,
683 + maxLines: 8,
684 + placeholder: Text(
685 + l10n.import_mcp_json_placeholder,
686 + style: const TextStyle(fontSize: 11),
687 + ),
688 + ),
689 + ],
690 + ),
691 + ),
692 + actions: [
693 + ShadButton.ghost(
694 + onPressed: () => Navigator.of(dialogContext).pop(),
695 + child: Text(l10n.cancel),
696 + ),
697 + ShadButton(
698 + onPressed: () {
699 + final jsonStr = jsonController.text.trim();
700 + if (jsonStr.isNotEmpty) {
701 + final count = ref
702 + .read(chatMcpConfigProvider.notifier)
703 + .importIntegrationsFromJson(jsonStr);
704 + Navigator.of(dialogContext).pop();
705 + widget.onServersChanged();
706 +
707 + ScaffoldMessenger.of(context).showSnackBar(
708 + SnackBar(
709 + content: Text(
710 + count > 0
711 + ? l10n.mcp_import_success(count)
712 + : l10n.mcp_import_failed,
713 + ),
714 + backgroundColor: count > 0 ? Colors.green : Colors.red,
715 + ),
716 + );
717 + }
718 + },
719 + child: Text(l10n.import_mcp_json),
720 + ),
721 + ],
722 + );
723 + },
724 + );
725 + }
726 +
727 + @override
728 + Widget build(BuildContext context) {
729 + final l10n = AppLocalizations.of(context)!;
730 + final settings = ref.watch(settingsProvider);
731 + final savedIntegrations = settings.savedMcpIntegrations;
732 + final isDark = Theme.of(context).brightness == Brightness.dark;
733 +
734 + return _McpSectionCard(
735 + title: l10n.active_integrations,
736 + accent: const Color(0xFF3B82F6),
737 + icon: HugeIcons.strokeRoundedMcpServer,
738 + trailing: ShadButton.outline(
739 + size: ShadButtonSize.sm,
740 + onPressed: () => _showImportJsonDialog(context),
741 + child: Row(
742 + mainAxisSize: MainAxisSize.min,
743 + children: [
744 + const HugeIcon(icon: HugeIcons.strokeRoundedFileImport, size: 16),
745 + const SizedBox(width: 4),
746 + Text(l10n.import_mcp_json),
747 + ],
748 + ),
749 + ),
750 + children: [
751 + if (savedIntegrations.isNotEmpty) ...[
752 + ListView.builder(
753 + shrinkWrap: true,
754 + padding: EdgeInsets.zero,
755 + physics: const NeverScrollableScrollPhysics(),
756 + itemCount: savedIntegrations.length,
757 + itemBuilder: (context, index) {
758 + final integration = savedIntegrations[index];
759 + return Container(
760 + margin: const EdgeInsets.only(bottom: 8),
761 + decoration: BoxDecoration(
762 + color: isDark
763 + ? AppColors.darkBackground
764 + : AppColors.lightSurface,
765 + borderRadius: BorderRadius.circular(12),
766 + border: Border.all(
767 + color: isDark
768 + ? AppColors.darkSurfaceCard
769 + : AppColors.lightBorder,
770 + ),
771 + ),
772 + child: ListTile(
773 + dense: true,
774 + leading: HugeIcon(
775 + icon: HugeIcons.strokeRoundedPuzzle,
776 + size: 18,
777 + color: integration.enabled ? null : Colors.grey,
778 + ),
779 + title: Text(
780 + integration.serverLabel ?? integration.pluginId ?? '',
781 + style: TextStyle(
782 + fontWeight: FontWeight.w600,
783 + decoration: integration.enabled
784 + ? TextDecoration.none
785 + : TextDecoration.lineThrough,
786 + color: integration.enabled
787 + ? null
788 + : (isDark
789 + ? AppColors.darkMutedText
790 + : AppColors.lightMutedText),
791 + ),
792 + ),
793 + subtitle: Text(
794 + integration.type == McpIntegrationType.plugin
795 + ? 'Plugin (${integration.pluginId})'
796 + : (integration.serverUrl ?? ''),
797 + style: const TextStyle(fontSize: 11),
798 + ),
799 + trailing: Row(
800 + mainAxisSize: MainAxisSize.min,
801 + children: [
802 + ShadSwitch(
803 + value: integration.enabled,
804 + onChanged: (v) {
805 + ref
806 + .read(settingsProvider.notifier)
807 + .toggleSavedMcpIntegration(index, v);
808 + ref
809 + .read(chatMcpConfigProvider.notifier)
810 + .toggleIntegration(index, v);
811 + },
812 + ),
813 + const SizedBox(width: 4),
814 + ShadIconButton.ghost(
815 + icon: const HugeIcon(
816 + icon: HugeIcons.strokeRoundedDelete01,
817 + size: 18,
818 + color: Colors.red,
819 + ),
820 + onPressed: () {
821 + ref
822 + .read(settingsProvider.notifier)
823 + .removeSavedMcpIntegration(index);
824 + ref
825 + .read(chatMcpConfigProvider.notifier)
826 + .removeIntegration(index);
827 + widget.onServersChanged();
828 + },
829 + ),
830 + ],
831 + ),
832 + ),
833 + );
834 + },
835 + ),
836 + ],
837 + Row(
838 + children: [
839 + Expanded(
840 + child: ShadInput(
841 + controller: _serverLabelController,
842 + placeholder: Text(l10n.mcp_label_placeholder),
843 + ),
844 + ),
845 + const SizedBox(width: 8),
846 + Expanded(
847 + flex: 2,
848 + child: ShadInput(
849 + controller: _serverUrlController,
850 + placeholder: Text(l10n.mcp_url_placeholder),
851 + keyboardType: TextInputType.url,
852 + ),
853 + ),
854 + const SizedBox(width: 8),
855 + ShadIconButton(
856 + icon: const HugeIcon(icon: HugeIcons.strokeRoundedAdd01),
857 + onPressed: _addServer,
858 + ),
859 + ],
860 + ),
861 + ],
862 + );
863 + }
864 +}
lib/features/settings/data/models/app_settings.dart
+20
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
4 import 'package:flutter_gemma/flutter_gemma.dart';
5
6 import '../../../../core/models/enums.dart';
7 +import '../../../../features/chat/data/models/mcp_integration.dart';
8 import '../../../../features/chat/utils/image_upload_utils.dart';
9 import '../../../../features/tts/data/kitten_tts_model.dart';
10
@@ -54,6 +55,7 @@ class AppSettings {
55 final bool keepPersonaOnNewChat;
56 final bool roleSwapButtonEnabled;
57 final bool showSystemMessagesInChat;
58 + final List<McpIntegration> savedMcpIntegrations;
59
60 AppSettings({
61 this.temperature = 0.7,
@@ -98,6 +100,7 @@ class AppSettings {
100 this.keepPersonaOnNewChat = false,
101 this.roleSwapButtonEnabled = false,
102 this.showSystemMessagesInChat = true,
103 + this.savedMcpIntegrations = const [],
104 });
105
106 AppSettings copyWith({
@@ -143,6 +146,7 @@ class AppSettings {
146 bool? keepPersonaOnNewChat,
147 bool? roleSwapButtonEnabled,
148 bool? showSystemMessagesInChat,
149 + List<McpIntegration>? savedMcpIntegrations,
150 }) {
151 return AppSettings(
152 temperature: temperature ?? this.temperature,
@@ -206,6 +210,8 @@ class AppSettings {
210 roleSwapButtonEnabled ?? this.roleSwapButtonEnabled,
211 showSystemMessagesInChat:
212 showSystemMessagesInChat ?? this.showSystemMessagesInChat,
213 + savedMcpIntegrations:
214 + savedMcpIntegrations ?? this.savedMcpIntegrations,
215 );
216 }
217
@@ -253,6 +259,8 @@ class AppSettings {
259 'keepPersonaOnNewChat': keepPersonaOnNewChat,
260 'roleSwapButtonEnabled': roleSwapButtonEnabled,
261 'showSystemMessagesInChat': showSystemMessagesInChat,
262 + 'savedMcpIntegrations':
263 + savedMcpIntegrations.map((i) => i.toJson()).toList(),
264 };
265 }
266
@@ -302,9 +310,21 @@ class AppSettings {
310 keepPersonaOnNewChat: map['keepPersonaOnNewChat'] ?? false,
311 roleSwapButtonEnabled: map['roleSwapButtonEnabled'] ?? false,
312 showSystemMessagesInChat: map['showSystemMessagesInChat'] ?? true,
313 + savedMcpIntegrations:
314 + _parseSavedMcpIntegrations(map['savedMcpIntegrations']),
315 );
316 }
317
318 + static List<McpIntegration> _parseSavedMcpIntegrations(dynamic value) {
319 + if (value is List) {
320 + return value
321 + .whereType<Map<String, dynamic>>()
322 + .map((m) => McpIntegration.fromJson(m))
323 + .toList();
324 + }
325 + return const [];
326 + }
327 +
328 static ImageCompressionLevel _parseImageCompressionLevel(dynamic value) {
329 if (value is String) {
330 try {
lib/l10n/app_en.arb
+29
@@ -1209,6 +1209,35 @@
1209 "@active_integrations": {
1210 "description": "Active MCP integrations section"
1211 },
1212 + "import_mcp_json": "Import JSON",
1213 + "@import_mcp_json": {
1214 + "description": "Button label to import MCP servers from JSON"
1215 + },
1216 + "import_mcp_json_dialog_title": "Import MCP Config JSON",
1217 + "@import_mcp_json_dialog_title": {
1218 + "description": "Dialog title for importing MCP JSON"
1219 + },
1220 + "import_mcp_json_instructions": "Copy your mcpServers JSON directly from LM Studio (mcp.json) or paste a plugin array below:",
1221 + "@import_mcp_json_instructions": {
1222 + "description": "Instructions for importing MCP JSON"
1223 + },
1224 + "import_mcp_json_placeholder": "Paste mcpServers JSON or plugin list here...",
1225 + "@import_mcp_json_placeholder": {
1226 + "description": "Placeholder for MCP JSON import field"
1227 + },
1228 + "mcp_import_success": "Successfully imported {count} integration(s)",
1229 + "@mcp_import_success": {
1230 + "description": "Toast message on successful MCP import",
1231 + "placeholders": {
1232 + "count": {
1233 + "type": "int"
1234 + }
1235 + }
1236 + },
1237 + "mcp_import_failed": "No valid MCP integrations found in JSON",
1238 + "@mcp_import_failed": {
1239 + "description": "Toast message on failed MCP import"
1240 + },
1241 "enable_notifications": "Enable Notifications",
1242 "@enable_notifications": {
1243 "description": "Notification banner title"
lib/l10n/app_localizations.dart
+36
@@ -1767,6 +1767,42 @@ abstract class AppLocalizations {
1767 /// **'Active Integrations'**
1768 String get active_integrations;
1769
1770 + /// Button label to import MCP servers from JSON
1771 + ///
1772 + /// In en, this message translates to:
1773 + /// **'Import JSON'**
1774 + String get import_mcp_json;
1775 +
1776 + /// Dialog title for importing MCP JSON
1777 + ///
1778 + /// In en, this message translates to:
1779 + /// **'Import MCP Config JSON'**
1780 + String get import_mcp_json_dialog_title;
1781 +
1782 + /// Instructions for importing MCP JSON
1783 + ///
1784 + /// In en, this message translates to:
1785 + /// **'Copy your mcpServers JSON directly from LM Studio (mcp.json) or paste a plugin array below:'**
1786 + String get import_mcp_json_instructions;
1787 +
1788 + /// Placeholder for MCP JSON import field
1789 + ///
1790 + /// In en, this message translates to:
1791 + /// **'Paste mcpServers JSON or plugin list here...'**
1792 + String get import_mcp_json_placeholder;
1793 +
1794 + /// Toast message on successful MCP import
1795 + ///
1796 + /// In en, this message translates to:
1797 + /// **'Successfully imported {count} integration(s)'**
1798 + String mcp_import_success(int count);
1799 +
1800 + /// Toast message on failed MCP import
1801 + ///
1802 + /// In en, this message translates to:
1803 + /// **'No valid MCP integrations found in JSON'**
1804 + String get mcp_import_failed;
1805 +
1806 /// Notification banner title
1807 ///
1808 /// In en, this message translates to:
lib/l10n/app_localizations_ar.dart
+22
@@ -893,6 +893,28 @@ class AppLocalizationsAr extends AppLocalizations {
893 @override
894 String get active_integrations => 'التكاملات النشطة';
895
896 + @override
897 + String get import_mcp_json => 'Import JSON';
898 +
899 + @override
900 + String get import_mcp_json_dialog_title => 'Import MCP Config JSON';
901 +
902 + @override
903 + String get import_mcp_json_instructions =>
904 + 'Copy your mcpServers JSON directly from LM Studio (mcp.json) or paste a plugin array below:';
905 +
906 + @override
907 + String get import_mcp_json_placeholder =>
908 + 'Paste mcpServers JSON or plugin list here...';
909 +
910 + @override
911 + String mcp_import_success(int count) {
912 + return 'Successfully imported $count integration(s)';
913 + }
914 +
915 + @override
916 + String get mcp_import_failed => 'No valid MCP integrations found in JSON';
917 +
918 @override
919 String get enable_notifications => 'تفعيل الإشعارات';
920
lib/l10n/app_localizations_bn.dart
+22
@@ -898,6 +898,28 @@ class AppLocalizationsBn extends AppLocalizations {
898 @override
899 String get active_integrations => 'সক্রিয় ইন্টিগ্রেশনসমূহ';
900
901 + @override
902 + String get import_mcp_json => 'Import JSON';
903 +
904 + @override
905 + String get import_mcp_json_dialog_title => 'Import MCP Config JSON';
906 +
907 + @override
908 + String get import_mcp_json_instructions =>
909 + 'Copy your mcpServers JSON directly from LM Studio (mcp.json) or paste a plugin array below:';
910 +
911 + @override
912 + String get import_mcp_json_placeholder =>
913 + 'Paste mcpServers JSON or plugin list here...';
914 +
915 + @override
916 + String mcp_import_success(int count) {
917 + return 'Successfully imported $count integration(s)';
918 + }
919 +
920 + @override
921 + String get mcp_import_failed => 'No valid MCP integrations found in JSON';
922 +
923 @override
924 String get enable_notifications => 'নোটিফিকেশন সক্ষম করুন';
925
lib/l10n/app_localizations_en.dart
+22
@@ -891,6 +891,28 @@ class AppLocalizationsEn extends AppLocalizations {
891 @override
892 String get active_integrations => 'Active Integrations';
893
894 + @override
895 + String get import_mcp_json => 'Import JSON';
896 +
897 + @override
898 + String get import_mcp_json_dialog_title => 'Import MCP Config JSON';
899 +
900 + @override
901 + String get import_mcp_json_instructions =>
902 + 'Copy your mcpServers JSON directly from LM Studio (mcp.json) or paste a plugin array below:';
903 +
904 + @override
905 + String get import_mcp_json_placeholder =>
906 + 'Paste mcpServers JSON or plugin list here...';
907 +
908 + @override
909 + String mcp_import_success(int count) {
910 + return 'Successfully imported $count integration(s)';
911 + }
912 +
913 + @override
914 + String get mcp_import_failed => 'No valid MCP integrations found in JSON';
915 +
916 @override
917 String get enable_notifications => 'Enable Notifications';
918
lib/l10n/app_localizations_es.dart
+22
@@ -899,6 +899,28 @@ class AppLocalizationsEs extends AppLocalizations {
899 @override
900 String get active_integrations => 'Integraciones activas';
901
902 + @override
903 + String get import_mcp_json => 'Import JSON';
904 +
905 + @override
906 + String get import_mcp_json_dialog_title => 'Import MCP Config JSON';
907 +
908 + @override
909 + String get import_mcp_json_instructions =>
910 + 'Copy your mcpServers JSON directly from LM Studio (mcp.json) or paste a plugin array below:';
911 +
912 + @override
913 + String get import_mcp_json_placeholder =>
914 + 'Paste mcpServers JSON or plugin list here...';
915 +
916 + @override
917 + String mcp_import_success(int count) {
918 + return 'Successfully imported $count integration(s)';
919 + }
920 +
921 + @override
922 + String get mcp_import_failed => 'No valid MCP integrations found in JSON';
923 +
924 @override
925 String get enable_notifications => 'Habilitar notificaciones';
926
lib/l10n/app_localizations_fr.dart
+22
@@ -893,6 +893,28 @@ class AppLocalizationsFr extends AppLocalizations {
893 @override
894 String get active_integrations => 'Active Integrations';
895
896 + @override
897 + String get import_mcp_json => 'Import JSON';
898 +
899 + @override
900 + String get import_mcp_json_dialog_title => 'Import MCP Config JSON';
901 +
902 + @override
903 + String get import_mcp_json_instructions =>
904 + 'Copy your mcpServers JSON directly from LM Studio (mcp.json) or paste a plugin array below:';
905 +
906 + @override
907 + String get import_mcp_json_placeholder =>
908 + 'Paste mcpServers JSON or plugin list here...';
909 +
910 + @override
911 + String mcp_import_success(int count) {
912 + return 'Successfully imported $count integration(s)';
913 + }
914 +
915 + @override
916 + String get mcp_import_failed => 'No valid MCP integrations found in JSON';
917 +
918 @override
919 String get enable_notifications => 'Enable Nontifications';
920
lib/l10n/app_localizations_hi.dart
+22
@@ -898,6 +898,28 @@ class AppLocalizationsHi extends AppLocalizations {
898 @override
899 String get active_integrations => 'सक्रिय एकीकरण';
900
901 + @override
902 + String get import_mcp_json => 'Import JSON';
903 +
904 + @override
905 + String get import_mcp_json_dialog_title => 'Import MCP Config JSON';
906 +
907 + @override
908 + String get import_mcp_json_instructions =>
909 + 'Copy your mcpServers JSON directly from LM Studio (mcp.json) or paste a plugin array below:';
910 +
911 + @override
912 + String get import_mcp_json_placeholder =>
913 + 'Paste mcpServers JSON or plugin list here...';
914 +
915 + @override
916 + String mcp_import_success(int count) {
917 + return 'Successfully imported $count integration(s)';
918 + }
919 +
920 + @override
921 + String get mcp_import_failed => 'No valid MCP integrations found in JSON';
922 +
923 @override
924 String get enable_notifications => 'सूचनाएं सक्षम करें';
925
lib/l10n/app_localizations_it.dart
+22
@@ -900,6 +900,28 @@ class AppLocalizationsIt extends AppLocalizations {
900 @override
901 String get active_integrations => 'Integrazioni attive';
902
903 + @override
904 + String get import_mcp_json => 'Import JSON';
905 +
906 + @override
907 + String get import_mcp_json_dialog_title => 'Import MCP Config JSON';
908 +
909 + @override
910 + String get import_mcp_json_instructions =>
911 + 'Copy your mcpServers JSON directly from LM Studio (mcp.json) or paste a plugin array below:';
912 +
913 + @override
914 + String get import_mcp_json_placeholder =>
915 + 'Paste mcpServers JSON or plugin list here...';
916 +
917 + @override
918 + String mcp_import_success(int count) {
919 + return 'Successfully imported $count integration(s)';
920 + }
921 +
922 + @override
923 + String get mcp_import_failed => 'No valid MCP integrations found in JSON';
924 +
925 @override
926 String get enable_notifications => 'Abilita notifiche';
927
lib/l10n/app_localizations_ja.dart
+22
@@ -879,6 +879,28 @@ class AppLocalizationsJa extends AppLocalizations {
879 @override
880 String get active_integrations => '有効な統合';
881
882 + @override
883 + String get import_mcp_json => 'Import JSON';
884 +
885 + @override
886 + String get import_mcp_json_dialog_title => 'Import MCP Config JSON';
887 +
888 + @override
889 + String get import_mcp_json_instructions =>
890 + 'Copy your mcpServers JSON directly from LM Studio (mcp.json) or paste a plugin array below:';
891 +
892 + @override
893 + String get import_mcp_json_placeholder =>
894 + 'Paste mcpServers JSON or plugin list here...';
895 +
896 + @override
897 + String mcp_import_success(int count) {
898 + return 'Successfully imported $count integration(s)';
899 + }
900 +
901 + @override
902 + String get mcp_import_failed => 'No valid MCP integrations found in JSON';
903 +
904 @override
905 String get enable_notifications => '通知を有効にする';
906
lib/l10n/app_localizations_ru.dart
+22
@@ -899,6 +899,28 @@ class AppLocalizationsRu extends AppLocalizations {
899 @override
900 String get active_integrations => 'Активная интеграция';
901
902 + @override
903 + String get import_mcp_json => 'Import JSON';
904 +
905 + @override
906 + String get import_mcp_json_dialog_title => 'Import MCP Config JSON';
907 +
908 + @override
909 + String get import_mcp_json_instructions =>
910 + 'Copy your mcpServers JSON directly from LM Studio (mcp.json) or paste a plugin array below:';
911 +
912 + @override
913 + String get import_mcp_json_placeholder =>
914 + 'Paste mcpServers JSON or plugin list here...';
915 +
916 + @override
917 + String mcp_import_success(int count) {
918 + return 'Successfully imported $count integration(s)';
919 + }
920 +
921 + @override
922 + String get mcp_import_failed => 'No valid MCP integrations found in JSON';
923 +
924 @override
925 String get enable_notifications => 'Включить уведомления';
926
lib/l10n/app_localizations_tr.dart
+22
@@ -899,6 +899,28 @@ class AppLocalizationsTr extends AppLocalizations {
899 @override
900 String get active_integrations => 'Aktif Entegrasyonlar';
901
902 + @override
903 + String get import_mcp_json => 'Import JSON';
904 +
905 + @override
906 + String get import_mcp_json_dialog_title => 'Import MCP Config JSON';
907 +
908 + @override
909 + String get import_mcp_json_instructions =>
910 + 'Copy your mcpServers JSON directly from LM Studio (mcp.json) or paste a plugin array below:';
911 +
912 + @override
913 + String get import_mcp_json_placeholder =>
914 + 'Paste mcpServers JSON or plugin list here...';
915 +
916 + @override
917 + String mcp_import_success(int count) {
918 + return 'Successfully imported $count integration(s)';
919 + }
920 +
921 + @override
922 + String get mcp_import_failed => 'No valid MCP integrations found in JSON';
923 +
924 @override
925 String get enable_notifications => 'Bildirimleri Etkinleştir';
926
lib/l10n/app_localizations_zh.dart
+22
@@ -873,6 +873,28 @@ class AppLocalizationsZh extends AppLocalizations {
873 @override
874 String get active_integrations => '活动集成';
875
876 + @override
877 + String get import_mcp_json => 'Import JSON';
878 +
879 + @override
880 + String get import_mcp_json_dialog_title => 'Import MCP Config JSON';
881 +
882 + @override
883 + String get import_mcp_json_instructions =>
884 + 'Copy your mcpServers JSON directly from LM Studio (mcp.json) or paste a plugin array below:';
885 +
886 + @override
887 + String get import_mcp_json_placeholder =>
888 + 'Paste mcpServers JSON or plugin list here...';
889 +
890 + @override
891 + String mcp_import_success(int count) {
892 + return 'Successfully imported $count integration(s)';
893 + }
894 +
895 + @override
896 + String get mcp_import_failed => 'No valid MCP integrations found in JSON';
897 +
898 @override
899 String get enable_notifications => '启用通知';
900
lib/l10n/untranslated.txt
+72
@@ -1,5 +1,11 @@
1 {
2 "ar": [
3 + "import_mcp_json",
4 + "import_mcp_json_dialog_title",
5 + "import_mcp_json_instructions",
6 + "import_mcp_json_placeholder",
7 + "mcp_import_success",
8 + "mcp_import_failed",
9 "server_type_ollama_cloud",
10 "server_type_ollama_cloud_sub",
11 "server_type_ollama_cloud_display",
@@ -13,6 +19,12 @@
19 ],
20
21 "bn": [
22 + "import_mcp_json",
23 + "import_mcp_json_dialog_title",
24 + "import_mcp_json_instructions",
25 + "import_mcp_json_placeholder",
26 + "mcp_import_success",
27 + "mcp_import_failed",
28 "server_type_ollama_cloud",
29 "server_type_ollama_cloud_sub",
30 "server_type_ollama_cloud_display",
@@ -26,6 +38,12 @@
38 ],
39
40 "es": [
41 + "import_mcp_json",
42 + "import_mcp_json_dialog_title",
43 + "import_mcp_json_instructions",
44 + "import_mcp_json_placeholder",
45 + "mcp_import_success",
46 + "mcp_import_failed",
47 "server_type_ollama_cloud",
48 "server_type_ollama_cloud_sub",
49 "server_type_ollama_cloud_display",
@@ -38,7 +56,22 @@
56 "add_server_endpoint_subtitle"
57 ],
58
59 + "fr": [
60 + "import_mcp_json",
61 + "import_mcp_json_dialog_title",
62 + "import_mcp_json_instructions",
63 + "import_mcp_json_placeholder",
64 + "mcp_import_success",
65 + "mcp_import_failed"
66 + ],
67 +
68 "hi": [
69 + "import_mcp_json",
70 + "import_mcp_json_dialog_title",
71 + "import_mcp_json_instructions",
72 + "import_mcp_json_placeholder",
73 + "mcp_import_success",
74 + "mcp_import_failed",
75 "server_type_ollama_cloud",
76 "server_type_ollama_cloud_sub",
77 "server_type_ollama_cloud_display",
@@ -52,6 +85,12 @@
85 ],
86
87 "it": [
88 + "import_mcp_json",
89 + "import_mcp_json_dialog_title",
90 + "import_mcp_json_instructions",
91 + "import_mcp_json_placeholder",
92 + "mcp_import_success",
93 + "mcp_import_failed",
94 "server_type_ollama_cloud",
95 "server_type_ollama_cloud_sub",
96 "server_type_ollama_cloud_display",
@@ -65,6 +104,12 @@
104 ],
105
106 "ja": [
107 + "import_mcp_json",
108 + "import_mcp_json_dialog_title",
109 + "import_mcp_json_instructions",
110 + "import_mcp_json_placeholder",
111 + "mcp_import_success",
112 + "mcp_import_failed",
113 "server_type_ollama_cloud",
114 "server_type_ollama_cloud_sub",
115 "server_type_ollama_cloud_display",
@@ -78,6 +123,12 @@
123 ],
124
125 "ru": [
126 + "import_mcp_json",
127 + "import_mcp_json_dialog_title",
128 + "import_mcp_json_instructions",
129 + "import_mcp_json_placeholder",
130 + "mcp_import_success",
131 + "mcp_import_failed",
132 "server_type_ollama_cloud",
133 "server_type_ollama_cloud_sub",
134 "server_type_ollama_cloud_display",
@@ -90,7 +141,22 @@
141 "add_server_endpoint_subtitle"
142 ],
143
144 + "tr": [
145 + "import_mcp_json",
146 + "import_mcp_json_dialog_title",
147 + "import_mcp_json_instructions",
148 + "import_mcp_json_placeholder",
149 + "mcp_import_success",
150 + "mcp_import_failed"
151 + ],
152 +
153 "zh": [
154 + "import_mcp_json",
155 + "import_mcp_json_dialog_title",
156 + "import_mcp_json_instructions",
157 + "import_mcp_json_placeholder",
158 + "mcp_import_success",
159 + "mcp_import_failed",
160 "server_type_ollama_cloud",
161 "server_type_ollama_cloud_sub",
162 "server_type_ollama_cloud_display",
@@ -104,6 +170,12 @@
170 ],
171
172 "zh_TW": [
173 + "import_mcp_json",
174 + "import_mcp_json_dialog_title",
175 + "import_mcp_json_instructions",
176 + "import_mcp_json_placeholder",
177 + "mcp_import_success",
178 + "mcp_import_failed",
179 "server_type_ollama_cloud",
180 "server_type_ollama_cloud_sub",
181 "server_type_ollama_cloud_display",
pubspec.yaml
+1 -1
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
16 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
17 # In Windows, build-name is used as the major, minor, and patch parts
18 # of the product and file versions while build-number is used as the build suffix.
19 -version: 1.5.7+62
19 +version: 1.5.8+63
20
21 environment:
22 sdk: ">=3.12.0 <4.0.0"
test/features/chat/chat_mcp_config_notifier_test.dart new
+85
@@ -0,0 +1,85 @@
1 +import 'package:flutter_test/flutter_test.dart';
2 +import 'package:flutter_riverpod/flutter_riverpod.dart';
3 +import 'package:shared_preferences/shared_preferences.dart';
4 +import 'package:localmind/core/providers/storage_providers.dart';
5 +import 'package:localmind/features/chat/providers/chat_mcp_providers.dart';
6 +
7 +void main() {
8 + TestWidgetsFlutterBinding.ensureInitialized();
9 +
10 + group('ChatMcpConfigNotifier Tests', () {
11 + late ProviderContainer container;
12 +
13 + setUp(() async {
14 + SharedPreferences.setMockInitialValues({});
15 + final prefs = await SharedPreferences.getInstance();
16 + container = ProviderContainer(
17 + overrides: [
18 + sharedPreferencesProvider.overrideWithValue(prefs),
19 + ],
20 + );
21 + });
22 +
23 + tearDown(() {
24 + container.dispose();
25 + });
26 +
27 + test('importIntegrationsFromJson parses mcpServers JSON correctly', () {
28 + const jsonStr = '''
29 +{
30 + "mcpServers": {
31 + "playwright": {
32 + "command": "npx",
33 + "args": ["-y", "@playwright/mcp"]
34 + },
35 + "danielsig/duckduckgo": {
36 + "command": "npx",
37 + "args": ["-y", "@danielsig/duckduckgo"]
38 + },
39 + "duckduckgo": {
40 + "url": "http://localhost:8000/sse"
41 + }
42 + }
43 +}
44 +''';
45 +
46 + final notifier = container.read(chatMcpConfigProvider.notifier);
47 + final count = notifier.importIntegrationsFromJson(jsonStr);
48 +
49 + expect(count, equals(3));
50 + final state = container.read(chatMcpConfigProvider);
51 + expect(state.integrations.length, equals(3));
52 +
53 + final playwright = state.integrations.firstWhere((i) => i.serverLabel == 'playwright');
54 + expect(playwright.type, equals(McpIntegrationType.plugin));
55 + expect(playwright.pluginId, equals('mcp/playwright'));
56 +
57 + final ddgPlugin = state.integrations.firstWhere((i) => i.serverLabel == 'danielsig/duckduckgo');
58 + expect(ddgPlugin.type, equals(McpIntegrationType.plugin));
59 + expect(ddgPlugin.pluginId, equals('danielsig/duckduckgo'));
60 +
61 + final ddg = state.integrations.firstWhere((i) => i.serverLabel == 'duckduckgo');
62 + expect(ddg.type, equals(McpIntegrationType.ephemeralMcp));
63 + expect(ddg.serverUrl, equals('http://localhost:8000/sse'));
64 + });
65 +
66 + test('toggleIntegration toggles integration enabled state', () {
67 + final notifier = container.read(chatMcpConfigProvider.notifier);
68 + notifier.addIntegration(
69 + const McpIntegration(
70 + type: McpIntegrationType.plugin,
71 + pluginId: 'mcp/playwright',
72 + enabled: true,
73 + ),
74 + );
75 +
76 + expect(container.read(chatMcpConfigProvider).integrations[0].enabled, isTrue);
77 +
78 + notifier.toggleIntegration(0, false);
79 + expect(container.read(chatMcpConfigProvider).integrations[0].enabled, isFalse);
80 +
81 + notifier.toggleIntegration(0, true);
82 + expect(container.read(chatMcpConfigProvider).integrations[0].enabled, isTrue);
83 + });
84 + });
85 +}