3 files changed
+120
-23
lib/features/chat/providers/chat_mcp_providers.dart
+37
@@ -64,6 +64,43 @@ class ChatMcpConfigNotifier extends Notifier<ChatMcpConfig> {
64
ref.read(settingsProvider.notifier).addSavedMcpIntegration(integration);
65
}
66
67
+ bool addIntegrationFromInput({required String label, required String url}) {
68
+ final trimmedLabel = label.trim();
69
+ final trimmedUrl = url.trim();
70
+ final uri = Uri.tryParse(trimmedUrl);
71
+ final isRemoteMcpUrl =
72
+ uri != null &&
73
+ uri.hasAuthority &&
74
+ (uri.scheme == 'http' || uri.scheme == 'https');
75
+
76
+ if (isRemoteMcpUrl) {
77
+ if (trimmedLabel.isEmpty) return false;
78
+ addIntegration(
79
+ McpIntegration(
80
+ type: McpIntegrationType.ephemeralMcp,
81
+ serverLabel: trimmedLabel,
82
+ serverUrl: trimmedUrl,
83
+ ),
84
+ );
85
+ return true;
86
+ }
87
+
88
+ final pluginId = trimmedLabel.isNotEmpty ? trimmedLabel : trimmedUrl;
89
+ final hasBothInputs = trimmedLabel.isNotEmpty && trimmedUrl.isNotEmpty;
90
+ if (hasBothInputs || !pluginId.contains('/')) {
91
+ return false;
92
+ }
93
+
94
+ addIntegration(
95
+ McpIntegration(
96
+ type: McpIntegrationType.plugin,
97
+ pluginId: pluginId,
98
+ serverLabel: trimmedLabel.isNotEmpty ? trimmedLabel : null,
99
+ ),
100
+ );
101
+ return true;
102
+ }
103
+
104
void removeIntegration(int index) {
105
final integration = state.integrations[index];
106
final newIntegrations = List<McpIntegration>.from(state.integrations)
lib/features/chat/views/components/chat_settings_sheet.dart
+7
-23
@@ -522,29 +522,13 @@ class _ChatSettingsSheetState extends ConsumerState<ChatSettingsSheet> {
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;
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;
540
- integration = McpIntegration(
541
- type: McpIntegrationType.ephemeralMcp,
542
- serverLabel: label,
543
- serverUrl: url,
544
- );
545
- }
546
-
547
- ref.read(chatMcpConfigProvider.notifier).addIntegration(integration);
525
+ final added = ref
526
+ .read(chatMcpConfigProvider.notifier)
527
+ .addIntegrationFromInput(
528
+ label: _serverLabelController.text,
529
+ url: _serverUrlController.text,
530
+ );
531
+ if (!added) return;
532
533
_serverLabelController.clear();
534
_serverUrlController.clear();
test/features/chat/chat_mcp_config_notifier_test.dart
+76
@@ -2,20 +2,39 @@ 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/data/mcp_server_manager.dart';
6
import 'package:localmind/features/chat/providers/chat_mcp_providers.dart';
7
+import 'package:localmind/features/chat/providers/tooling_providers.dart';
8
+
9
+class RecordingMcpServerManager extends McpServerManager {
10
+ final List<({String label, String url, Map<String, String>? headers})>
11
+ addedServers = [];
12
+
13
+ @override
14
+ Future<void> addServer(
15
+ String label,
16
+ String url, {
17
+ Map<String, String>? headers,
18
+ }) async {
19
+ addedServers.add((label: label, url: url, headers: headers));
20
+ }
21
+}
22
23
void main() {
24
TestWidgetsFlutterBinding.ensureInitialized();
25
26
group('ChatMcpConfigNotifier Tests', () {
27
late ProviderContainer container;
28
+ late RecordingMcpServerManager mcpServerManager;
29
30
setUp(() async {
31
SharedPreferences.setMockInitialValues({});
32
final prefs = await SharedPreferences.getInstance();
33
+ mcpServerManager = RecordingMcpServerManager();
34
container = ProviderContainer(
35
overrides: [
36
sharedPreferencesProvider.overrideWithValue(prefs),
37
+ mcpServerManagerProvider.overrideWithValue(mcpServerManager),
38
],
39
);
40
});
@@ -81,5 +100,62 @@ void main() {
100
notifier.toggleIntegration(0, true);
101
expect(container.read(chatMcpConfigProvider).integrations[0].enabled, isTrue);
102
});
103
+
104
+ test('addIntegrationFromInput connects an HTTPS MCP server', () {
105
+ final notifier = container.read(chatMcpConfigProvider.notifier);
106
+
107
+ final added = notifier.addIntegrationFromInput(
108
+ label: 'Remote tools',
109
+ url: 'https://example.com/mcp/sse',
110
+ );
111
+
112
+ expect(added, isTrue);
113
+ final integration = container
114
+ .read(chatMcpConfigProvider)
115
+ .integrations
116
+ .single;
117
+ expect(integration.type, McpIntegrationType.ephemeralMcp);
118
+ expect(integration.serverLabel, 'Remote tools');
119
+ expect(integration.serverUrl, 'https://example.com/mcp/sse');
120
+ expect(integration.pluginId, isNull);
121
+ expect(mcpServerManager.addedServers, hasLength(1));
122
+ expect(mcpServerManager.addedServers.single.label, 'Remote tools');
123
+ expect(
124
+ mcpServerManager.addedServers.single.url,
125
+ 'https://example.com/mcp/sse',
126
+ );
127
+ });
128
+
129
+ test('addIntegrationFromInput still accepts a plugin id', () {
130
+ final notifier = container.read(chatMcpConfigProvider.notifier);
131
+
132
+ final added = notifier.addIntegrationFromInput(
133
+ label: 'mcp/playwright',
134
+ url: '',
135
+ );
136
+
137
+ expect(added, isTrue);
138
+ final integration = container
139
+ .read(chatMcpConfigProvider)
140
+ .integrations
141
+ .single;
142
+ expect(integration.type, McpIntegrationType.plugin);
143
+ expect(integration.pluginId, 'mcp/playwright');
144
+ expect(integration.serverUrl, isNull);
145
+ expect(mcpServerManager.addedServers, isEmpty);
146
+ });
147
+
148
+ test('addIntegrationFromInput rejects an unlabeled MCP URL', () {
149
+ final notifier = container.read(chatMcpConfigProvider.notifier);
150
+
151
+ final added = notifier.addIntegrationFromInput(
152
+ label: '',
153
+ url: 'https://example.com/mcp/sse',
154
+ );
155
+
156
+ expect(added, isFalse);
157
+ expect(container.read(chatMcpConfigProvider).integrations, isEmpty);
158
+ expect(mcpServerManager.addedServers, isEmpty);
159
+ });
160
});
161
}