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
+}