@tduncan / Cortana / commits / 708b4fb

fix(mcp): register remote server URLs correctly

Refs #23

abdulmominsakib committed Aug 19, 2026 at 22:32 UTC 708b4fbe0cf7c9d86d954412539ce06ef46426ea
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 }