shreyask commited on
Commit
aad94d8
·
verified ·
1 Parent(s): d89db9c

add mcp support

Browse files
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
src/components/MCPServerManager.tsx ADDED
@@ -0,0 +1,434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from "react";
2
+ import { Plus, Server, Wifi, WifiOff, Trash2, TestTube } from "lucide-react";
3
+ import { useMCP } from "../hooks/useMCP";
4
+ import type { MCPServerConfig } from "../types/mcp";
5
+
6
+ interface MCPServerManagerProps {
7
+ isOpen: boolean;
8
+ onClose: () => void;
9
+ }
10
+
11
+ export const MCPServerManager: React.FC<MCPServerManagerProps> = ({
12
+ isOpen,
13
+ onClose,
14
+ }) => {
15
+ const {
16
+ mcpState,
17
+ addServer,
18
+ removeServer,
19
+ connectToServer,
20
+ disconnectFromServer,
21
+ testConnection,
22
+ } = useMCP();
23
+ const [showAddForm, setShowAddForm] = useState(false);
24
+ const [testingConnection, setTestingConnection] = useState<string | null>(
25
+ null
26
+ );
27
+
28
+ const [newServer, setNewServer] = useState<Omit<MCPServerConfig, "id">>({
29
+ name: "",
30
+ url: "",
31
+ enabled: true,
32
+ transport: "streamable-http",
33
+ auth: {
34
+ type: "bearer",
35
+ },
36
+ });
37
+
38
+ if (!isOpen) return null;
39
+
40
+ const handleAddServer = async () => {
41
+ if (!newServer.name || !newServer.url) return;
42
+
43
+ const serverConfig: MCPServerConfig = {
44
+ ...newServer,
45
+ id: `server_${Date.now()}`,
46
+ };
47
+
48
+ try {
49
+ await addServer(serverConfig);
50
+ setNewServer({
51
+ name: "",
52
+ url: "",
53
+ enabled: true,
54
+ transport: "streamable-http",
55
+ auth: {
56
+ type: "bearer",
57
+ },
58
+ });
59
+ setShowAddForm(false);
60
+ } catch (error) {
61
+ console.error("Failed to add server:", error);
62
+ }
63
+ };
64
+
65
+ const handleTestConnection = async (config: MCPServerConfig) => {
66
+ setTestingConnection(config.id);
67
+ try {
68
+ const success = await testConnection(config);
69
+ if (success) {
70
+ alert("Connection test successful!");
71
+ } else {
72
+ alert("Connection test failed. Please check your configuration.");
73
+ }
74
+ } catch (error) {
75
+ alert(`Connection test failed: ${error}`);
76
+ } finally {
77
+ setTestingConnection(null);
78
+ }
79
+ };
80
+
81
+ const handleToggleConnection = async (
82
+ serverId: string,
83
+ isConnected: boolean
84
+ ) => {
85
+ try {
86
+ if (isConnected) {
87
+ await disconnectFromServer(serverId);
88
+ } else {
89
+ await connectToServer(serverId);
90
+ }
91
+ } catch (error) {
92
+ console.error("Failed to toggle connection:", error);
93
+ }
94
+ };
95
+
96
+ return (
97
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
98
+ <div className="bg-gray-800 rounded-lg p-6 w-full max-w-4xl max-h-[80vh] overflow-y-auto">
99
+ <div className="flex justify-between items-center mb-6">
100
+ <h2 className="text-2xl font-bold text-white flex items-center gap-2">
101
+ <Server className="text-blue-400" />
102
+ MCP Server Manager
103
+ </h2>
104
+ <button onClick={onClose} className="text-gray-400 hover:text-white">
105
+
106
+ </button>
107
+ </div>
108
+
109
+ {/* Add Server Button */}
110
+ <div className="mb-6">
111
+ <button
112
+ onClick={() => setShowAddForm(!showAddForm)}
113
+ className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2"
114
+ >
115
+ <Plus size={16} />
116
+ Add MCP Server
117
+ </button>
118
+ </div>
119
+
120
+ {/* Add Server Form */}
121
+ {showAddForm && (
122
+ <div className="bg-gray-700 rounded-lg p-4 mb-6">
123
+ <h3 className="text-lg font-semibold text-white mb-4">
124
+ Add New MCP Server
125
+ </h3>
126
+ <div className="space-y-4">
127
+ <div>
128
+ <label className="block text-sm font-medium text-gray-300 mb-1">
129
+ Server Name
130
+ </label>
131
+ <input
132
+ type="text"
133
+ value={newServer.name}
134
+ onChange={(e) =>
135
+ setNewServer({ ...newServer, name: e.target.value })
136
+ }
137
+ className="w-full bg-gray-600 text-white rounded px-3 py-2"
138
+ placeholder="My MCP Server"
139
+ />
140
+ </div>
141
+
142
+ <div>
143
+ <label className="block text-sm font-medium text-gray-300 mb-1">
144
+ Server URL
145
+ </label>
146
+ <input
147
+ type="url"
148
+ value={newServer.url}
149
+ onChange={(e) =>
150
+ setNewServer({ ...newServer, url: e.target.value })
151
+ }
152
+ className="w-full bg-gray-600 text-white rounded px-3 py-2"
153
+ placeholder="http://localhost:3000/mcp"
154
+ />
155
+ </div>
156
+
157
+ <div>
158
+ <label className="block text-sm font-medium text-gray-300 mb-1">
159
+ Transport
160
+ </label>
161
+ <select
162
+ value={newServer.transport}
163
+ onChange={(e) =>
164
+ setNewServer({
165
+ ...newServer,
166
+ transport: e.target.value as MCPServerConfig["transport"],
167
+ })
168
+ }
169
+ className="w-full bg-gray-600 text-white rounded px-3 py-2"
170
+ >
171
+ <option value="streamable-http">Streamable HTTP</option>
172
+ <option value="sse">Server-Sent Events</option>
173
+ <option value="websocket">WebSocket</option>
174
+ </select>
175
+ </div>
176
+
177
+ <div>
178
+ <label className="block text-sm font-medium text-gray-300 mb-1">
179
+ Authentication
180
+ </label>
181
+ <select
182
+ value={newServer.auth?.type || "none"}
183
+ onChange={(e) => {
184
+ const authType = e.target.value;
185
+ if (authType === "none") {
186
+ setNewServer({ ...newServer, auth: undefined });
187
+ } else {
188
+ setNewServer({
189
+ ...newServer,
190
+ auth: {
191
+ type: authType as "bearer" | "basic" | "oauth",
192
+ ...(authType === "bearer" ? { token: "" } : {}),
193
+ ...(authType === "basic"
194
+ ? { username: "", password: "" }
195
+ : {}),
196
+ ...(authType === "oauth" ? { token: "" } : {}),
197
+ },
198
+ });
199
+ }
200
+ }}
201
+ className="w-full bg-gray-600 text-white rounded px-3 py-2"
202
+ >
203
+ <option value="none">No Authentication</option>
204
+ <option value="bearer">Bearer Token</option>
205
+ <option value="basic">Basic Auth</option>
206
+ <option value="oauth">OAuth Token</option>
207
+ </select>
208
+ </div>
209
+
210
+ {/* Auth-specific fields */}
211
+ {newServer.auth?.type === "bearer" && (
212
+ <div>
213
+ <label className="block text-sm font-medium text-gray-300 mb-1">
214
+ Bearer Token
215
+ </label>
216
+ <input
217
+ type="password"
218
+ value={newServer.auth.token || ""}
219
+ onChange={(e) =>
220
+ setNewServer({
221
+ ...newServer,
222
+ auth: { ...newServer.auth!, token: e.target.value },
223
+ })
224
+ }
225
+ className="w-full bg-gray-600 text-white rounded px-3 py-2"
226
+ placeholder="your-bearer-token"
227
+ />
228
+ </div>
229
+ )}
230
+
231
+ {newServer.auth?.type === "basic" && (
232
+ <>
233
+ <div>
234
+ <label className="block text-sm font-medium text-gray-300 mb-1">
235
+ Username
236
+ </label>
237
+ <input
238
+ type="text"
239
+ value={newServer.auth.username || ""}
240
+ onChange={(e) =>
241
+ setNewServer({
242
+ ...newServer,
243
+ auth: {
244
+ ...newServer.auth!,
245
+ username: e.target.value,
246
+ },
247
+ })
248
+ }
249
+ className="w-full bg-gray-600 text-white rounded px-3 py-2"
250
+ placeholder="username"
251
+ />
252
+ </div>
253
+ <div>
254
+ <label className="block text-sm font-medium text-gray-300 mb-1">
255
+ Password
256
+ </label>
257
+ <input
258
+ type="password"
259
+ value={newServer.auth.password || ""}
260
+ onChange={(e) =>
261
+ setNewServer({
262
+ ...newServer,
263
+ auth: {
264
+ ...newServer.auth!,
265
+ password: e.target.value,
266
+ },
267
+ })
268
+ }
269
+ className="w-full bg-gray-600 text-white rounded px-3 py-2"
270
+ placeholder="password"
271
+ />
272
+ </div>
273
+ </>
274
+ )}
275
+
276
+ {newServer.auth?.type === "oauth" && (
277
+ <div>
278
+ <label className="block text-sm font-medium text-gray-300 mb-1">
279
+ OAuth Token
280
+ </label>
281
+ <input
282
+ type="password"
283
+ value={newServer.auth.token || ""}
284
+ onChange={(e) =>
285
+ setNewServer({
286
+ ...newServer,
287
+ auth: { ...newServer.auth!, token: e.target.value },
288
+ })
289
+ }
290
+ className="w-full bg-gray-600 text-white rounded px-3 py-2"
291
+ placeholder="your-oauth-token"
292
+ />
293
+ </div>
294
+ )}
295
+
296
+ <div className="flex items-center gap-2">
297
+ <input
298
+ type="checkbox"
299
+ id="enabled"
300
+ checked={newServer.enabled}
301
+ onChange={(e) =>
302
+ setNewServer({ ...newServer, enabled: e.target.checked })
303
+ }
304
+ className="rounded"
305
+ />
306
+ <label htmlFor="enabled" className="text-sm text-gray-300">
307
+ Auto-connect when added
308
+ </label>
309
+ </div>
310
+
311
+ <div className="flex gap-2">
312
+ <button
313
+ onClick={handleAddServer}
314
+ className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded"
315
+ >
316
+ Add Server
317
+ </button>
318
+ <button
319
+ onClick={() => setShowAddForm(false)}
320
+ className="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded"
321
+ >
322
+ Cancel
323
+ </button>
324
+ </div>
325
+ </div>
326
+ </div>
327
+ )}
328
+
329
+ {/* Server List */}
330
+ <div className="space-y-4">
331
+ <h3 className="text-lg font-semibold text-white">
332
+ Configured Servers
333
+ </h3>
334
+
335
+ {Object.values(mcpState.servers).length === 0 ? (
336
+ <div className="text-gray-400 text-center py-8">
337
+ No MCP servers configured. Add one to get started!
338
+ </div>
339
+ ) : (
340
+ Object.values(mcpState.servers).map((config) => (
341
+ <div
342
+ key={config.config.id}
343
+ className="mb-4 p-2 border rounded-lg bg-gray-800"
344
+ >
345
+ <div className="flex items-center justify-between">
346
+ <div>
347
+ <span className="font-bold text-lg">
348
+ {config.config.name}
349
+ </span>
350
+ <span className="ml-2 text-xs text-gray-400">
351
+ {config.config.url}
352
+ </span>
353
+ </div>
354
+ <div className="flex gap-2">
355
+ {/* Test Connection */}
356
+ <button
357
+ onClick={() => handleTestConnection(config.config)}
358
+ disabled={testingConnection === config.config.id}
359
+ className="p-2 text-yellow-400 hover:text-yellow-300 disabled:opacity-50"
360
+ title="Test Connection"
361
+ >
362
+ <TestTube size={16} />
363
+ </button>
364
+
365
+ {/* Connect/Disconnect */}
366
+ <button
367
+ onClick={() =>
368
+ handleToggleConnection(
369
+ config.config.id,
370
+ config.isConnected
371
+ )
372
+ }
373
+ className={`p-2 ${
374
+ config.isConnected
375
+ ? "text-green-400 hover:text-green-300"
376
+ : "text-gray-400 hover:text-gray-300"
377
+ }`}
378
+ title={config.isConnected ? "Disconnect" : "Connect"}
379
+ >
380
+ {config.isConnected ? (
381
+ <Wifi size={16} />
382
+ ) : (
383
+ <WifiOff size={16} />
384
+ )}
385
+ </button>
386
+
387
+ {/* Remove Server */}
388
+ <button
389
+ onClick={() => removeServer(config.config.id)}
390
+ className="p-2 text-red-400 hover:text-red-300"
391
+ title="Remove Server"
392
+ >
393
+ <Trash2 size={16} />
394
+ </button>
395
+ </div>
396
+ </div>
397
+
398
+ {config.lastError && (
399
+ <div className="mt-2 text-red-400 text-sm">
400
+ Error: {config.lastError}
401
+ </div>
402
+ )}
403
+
404
+ {config.isConnected && config.tools.length > 0 && (
405
+ <div className="mt-3">
406
+ <details className="text-sm">
407
+ <summary className="text-gray-300 cursor-pointer">
408
+ Available Tools ({config.tools.length})
409
+ </summary>
410
+ <div className="mt-2 space-y-1">
411
+ {config.tools.map((tool) => (
412
+ <div key={tool.name} className="text-gray-400 pl-4">
413
+ • {tool.name} -{" "}
414
+ {tool.description || "No description"}
415
+ </div>
416
+ ))}
417
+ </div>
418
+ </details>
419
+ </div>
420
+ )}
421
+ </div>
422
+ ))
423
+ )}
424
+ </div>
425
+
426
+ {mcpState.error && (
427
+ <div className="mt-4 p-4 bg-red-900 border border-red-700 rounded-lg text-red-200">
428
+ <strong>Error:</strong> {mcpState.error}
429
+ </div>
430
+ )}
431
+ </div>
432
+ </div>
433
+ );
434
+ };
src/hooks/useMCP.ts ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { MCPClientService } from "../services/mcpClient";
3
+ import type {
4
+ MCPServerConfig,
5
+ MCPClientState,
6
+ ExtendedTool,
7
+ } from "../types/mcp";
8
+ import type { Tool as OriginalTool } from "../components/ToolItem";
9
+
10
+ // Singleton instance
11
+ let mcpClientInstance: MCPClientService | null = null;
12
+
13
+ const getMCPClient = (): MCPClientService => {
14
+ if (!mcpClientInstance) {
15
+ mcpClientInstance = new MCPClientService();
16
+ }
17
+ return mcpClientInstance;
18
+ };
19
+
20
+ export const useMCP = () => {
21
+ const [mcpState, setMCPState] = useState<MCPClientState>({
22
+ servers: {},
23
+ isLoading: false,
24
+ error: undefined,
25
+ });
26
+
27
+ const mcpClient = getMCPClient();
28
+
29
+ // Subscribe to MCP state changes
30
+ useEffect(() => {
31
+ const handleStateChange = (state: MCPClientState) => {
32
+ console.log("MCP state change triggered:", Object.keys(state.servers));
33
+ setMCPState(state);
34
+ };
35
+
36
+ mcpClient.addStateListener(handleStateChange);
37
+
38
+ // Get initial state
39
+ setMCPState(mcpClient.getState());
40
+
41
+ return () => {
42
+ mcpClient.removeStateListener(handleStateChange);
43
+ // Don't cleanup the service itself as it's a singleton used across the app
44
+ };
45
+ }, [mcpClient]);
46
+
47
+ // Add a new MCP server
48
+ const addServer = useCallback(
49
+ async (config: MCPServerConfig): Promise<void> => {
50
+ return mcpClient.addServer(config);
51
+ },
52
+ [mcpClient]
53
+ );
54
+
55
+ // Remove an MCP server
56
+ const removeServer = useCallback(
57
+ async (serverId: string): Promise<void> => {
58
+ return mcpClient.removeServer(serverId);
59
+ },
60
+ [mcpClient]
61
+ );
62
+
63
+ // Connect to a server
64
+ const connectToServer = useCallback(
65
+ async (serverId: string): Promise<void> => {
66
+ return mcpClient.connectToServer(serverId);
67
+ },
68
+ [mcpClient]
69
+ );
70
+
71
+ // Disconnect from a server
72
+ const disconnectFromServer = useCallback(
73
+ async (serverId: string): Promise<void> => {
74
+ return mcpClient.disconnectFromServer(serverId);
75
+ },
76
+ [mcpClient]
77
+ );
78
+
79
+ // Test connection to a server
80
+ const testConnection = useCallback(
81
+ async (config: MCPServerConfig): Promise<boolean> => {
82
+ return mcpClient.testConnection(config);
83
+ },
84
+ [mcpClient]
85
+ );
86
+
87
+ // Call a tool on an MCP server
88
+ const callMCPTool = useCallback(
89
+ async (
90
+ serverId: string,
91
+ toolName: string,
92
+ args: Record<string, unknown>
93
+ ) => {
94
+ console.log("MCP tool called:", {
95
+ serverId,
96
+ toolName,
97
+ args,
98
+ timestamp: new Date().toISOString(),
99
+ });
100
+ console.trace("MCP tool call stack trace:");
101
+ return mcpClient.callTool(serverId, toolName, args);
102
+ },
103
+ [mcpClient]
104
+ );
105
+
106
+ // Get all available MCP tools
107
+ const getMCPTools = useCallback((): ExtendedTool[] => {
108
+ console.log(
109
+ "getMCPTools called - checking if this is being called frequently"
110
+ );
111
+ const mcpTools: ExtendedTool[] = [];
112
+
113
+ Object.entries(mcpState.servers).forEach(([serverId, connection]) => {
114
+ if (connection.isConnected && connection.config.enabled) {
115
+ connection.tools.forEach((mcpTool) => {
116
+ mcpTools.push({
117
+ id: `${serverId}:${mcpTool.name}`,
118
+ name: mcpTool.name,
119
+ enabled: true,
120
+ isCollapsed: false,
121
+ mcpServerId: serverId,
122
+ mcpTool: mcpTool,
123
+ isRemote: true,
124
+ });
125
+ });
126
+ }
127
+ });
128
+
129
+ return mcpTools;
130
+ }, [mcpState.servers]);
131
+
132
+ // Convert MCP tools to the format expected by the existing tool system
133
+ const getMCPToolsAsOriginalTools = useCallback((): OriginalTool[] => {
134
+ const mcpTools: OriginalTool[] = [];
135
+ let globalId = 10000; // Start with high numbers to avoid conflicts
136
+
137
+ Object.entries(mcpState.servers).forEach(([serverId, connection]) => {
138
+ if (connection.isConnected && connection.config.enabled) {
139
+ connection.tools.forEach((mcpTool) => {
140
+ // Convert tool name to valid JavaScript identifier and include server name for uniqueness
141
+ const serverName = connection.config.name
142
+ .replace(/[-\s]/g, "_")
143
+ .replace(/[^a-zA-Z0-9_]/g, "");
144
+ const toolName = mcpTool.name
145
+ .replace(/[-\s]/g, "_")
146
+ .replace(/[^a-zA-Z0-9_]/g, "");
147
+ const jsToolName = `${serverName}_${toolName}`;
148
+
149
+ // Create a JavaScript function that calls the MCP tool
150
+ const code = `/**
151
+ * ${mcpTool.description || `MCP tool from ${connection.config.name}`}
152
+ * Server: ${connection.config.name}
153
+ * ${Object.entries(mcpTool.inputSchema.properties || {})
154
+ .map(([name, prop]) => {
155
+ const p = prop as { type?: string; description?: string };
156
+ return `@param {${p.type || "any"}} ${name} - ${p.description || ""}`;
157
+ })
158
+ .join("\n * ")}
159
+ * @returns {Promise<any>} Tool execution result
160
+ */
161
+ export async function ${jsToolName}(${Object.keys(
162
+ mcpTool.inputSchema.properties || {}
163
+ ).join(", ")}) {
164
+ // This is an MCP tool - execution is handled by the MCP client
165
+ return { mcpServerId: "${serverId}", toolName: "${
166
+ mcpTool.name
167
+ }", arguments: arguments };
168
+ }
169
+
170
+ export default (input, output) =>
171
+ React.createElement(
172
+ "div",
173
+ { className: "bg-blue-50 border border-blue-200 rounded-lg p-4" },
174
+ React.createElement(
175
+ "div",
176
+ { className: "flex items-center mb-3" },
177
+ React.createElement(
178
+ "div",
179
+ {
180
+ className:
181
+ "w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-3",
182
+ },
183
+ "🌐",
184
+ ),
185
+ React.createElement(
186
+ "h3",
187
+ { className: "text-blue-900 font-semibold" },
188
+ "${mcpTool.name} (${connection.config.name})"
189
+ ),
190
+ ),
191
+ React.createElement(
192
+ "div",
193
+ { className: "text-sm mb-2" },
194
+ React.createElement(
195
+ "p",
196
+ { className: "text-blue-700 font-medium mb-1" },
197
+ \`Server: ${connection.config.name}\`
198
+ ),
199
+ ),
200
+ );`;
201
+
202
+ mcpTools.push({
203
+ id: globalId++,
204
+ name: jsToolName, // Use JavaScript-safe name for function calls
205
+ code: code,
206
+ enabled: true,
207
+ isCollapsed: false,
208
+ });
209
+ });
210
+ }
211
+ });
212
+
213
+ return mcpTools;
214
+ }, [mcpState.servers]);
215
+
216
+ // Connect to all enabled servers
217
+ const connectAll = useCallback(async (): Promise<void> => {
218
+ return mcpClient.connectAll();
219
+ }, [mcpClient]);
220
+
221
+ // Disconnect from all servers
222
+ const disconnectAll = useCallback(async (): Promise<void> => {
223
+ return mcpClient.disconnectAll();
224
+ }, [mcpClient]);
225
+
226
+ return {
227
+ mcpState,
228
+ addServer,
229
+ removeServer,
230
+ connectToServer,
231
+ disconnectFromServer,
232
+ testConnection,
233
+ callMCPTool,
234
+ getMCPTools,
235
+ getMCPToolsAsOriginalTools,
236
+ connectAll,
237
+ disconnectAll,
238
+ };
239
+ };
src/services/mcpClient.ts ADDED
@@ -0,0 +1,444 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { WebSocketClientTransport } from "@modelcontextprotocol/sdk/client/websocket.js";
3
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
5
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
6
+
7
+ import type {
8
+ MCPServerConfig,
9
+ MCPServerConnection,
10
+ MCPClientState,
11
+ MCPToolResult,
12
+ } from "../types/mcp.js";
13
+
14
+ export class MCPClientService {
15
+ private clients: Map<string, Client> = new Map();
16
+ private connections: Map<string, MCPServerConnection> = new Map();
17
+ private listeners: Array<(state: MCPClientState) => void> = [];
18
+ private healthCheckInterval: NodeJS.Timeout | null = null;
19
+
20
+ constructor() {
21
+ // Load saved server configurations from localStorage
22
+ this.loadServerConfigs();
23
+
24
+ // Start health check every 5 minutes
25
+ this.startHealthCheck();
26
+ }
27
+
28
+ // Start periodic health check (every 5 minutes)
29
+ private startHealthCheck() {
30
+ // Clear any existing interval
31
+ if (this.healthCheckInterval) {
32
+ clearInterval(this.healthCheckInterval);
33
+ }
34
+
35
+ // Set up new interval for 5 minutes (300,000 ms)
36
+ this.healthCheckInterval = setInterval(() => {
37
+ this.performHealthCheck();
38
+ }, 5 * 60 * 1000);
39
+ }
40
+
41
+ // Perform health check on all connected servers
42
+ private async performHealthCheck() {
43
+ console.log("Performing MCP health check...");
44
+
45
+ for (const [serverId, connection] of this.connections) {
46
+ if (connection.config.enabled && connection.isConnected) {
47
+ try {
48
+ const client = this.clients.get(serverId);
49
+ if (client) {
50
+ // Try to list tools as a health check
51
+ await client.listTools();
52
+ console.log(`Health check passed for ${serverId}`);
53
+ }
54
+ } catch (error) {
55
+ console.warn(`Health check failed for ${serverId}:`, error);
56
+ // Mark as disconnected and try to reconnect
57
+ connection.isConnected = false;
58
+ connection.lastError = "Health check failed";
59
+
60
+ // Try to reconnect
61
+ try {
62
+ await this.connectToServer(serverId);
63
+ console.log(`Reconnected to ${serverId}`);
64
+ } catch (reconnectError) {
65
+ console.error(
66
+ `Failed to reconnect to ${serverId}:`,
67
+ reconnectError
68
+ );
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ this.notifyStateChange();
75
+ }
76
+
77
+ // Clean up health check on destruction
78
+ public cleanup() {
79
+ if (this.healthCheckInterval) {
80
+ clearInterval(this.healthCheckInterval);
81
+ this.healthCheckInterval = null;
82
+ }
83
+ }
84
+
85
+ // Add state change listener
86
+ addStateListener(listener: (state: MCPClientState) => void) {
87
+ this.listeners.push(listener);
88
+ }
89
+
90
+ // Remove state change listener
91
+ removeStateListener(listener: (state: MCPClientState) => void) {
92
+ const index = this.listeners.indexOf(listener);
93
+ if (index > -1) {
94
+ this.listeners.splice(index, 1);
95
+ }
96
+ }
97
+
98
+ // Notify all listeners of state changes
99
+ private notifyStateChange() {
100
+ const state = this.getState();
101
+ this.listeners.forEach((listener) => listener(state));
102
+ }
103
+
104
+ // Get current MCP client state
105
+ getState(): MCPClientState {
106
+ const servers: Record<string, MCPServerConnection> = {};
107
+ for (const [id, connection] of this.connections) {
108
+ servers[id] = connection;
109
+ }
110
+
111
+ return {
112
+ servers,
113
+ isLoading: false,
114
+ error: undefined,
115
+ };
116
+ }
117
+
118
+ // Load server configurations from localStorage
119
+ private loadServerConfigs() {
120
+ try {
121
+ const stored = localStorage.getItem("mcp-servers");
122
+ if (stored) {
123
+ const configs: MCPServerConfig[] = JSON.parse(stored);
124
+ configs.forEach((config) => {
125
+ const connection: MCPServerConnection = {
126
+ config,
127
+ isConnected: false,
128
+ tools: [],
129
+ lastError: undefined,
130
+ lastConnected: undefined,
131
+ };
132
+ this.connections.set(config.id, connection);
133
+ });
134
+ }
135
+ } catch (error) {
136
+ console.error("Failed to load MCP server configs:", error);
137
+ }
138
+ }
139
+
140
+ // Save server configurations to localStorage
141
+ private saveServerConfigs() {
142
+ try {
143
+ const configs = Array.from(this.connections.values()).map(
144
+ (conn) => conn.config
145
+ );
146
+ localStorage.setItem("mcp-servers", JSON.stringify(configs));
147
+ } catch (error) {
148
+ console.error("Failed to save MCP server configs:", error);
149
+ }
150
+ }
151
+
152
+ // Add a new MCP server
153
+ async addServer(config: MCPServerConfig): Promise<void> {
154
+ const connection: MCPServerConnection = {
155
+ config,
156
+ isConnected: false,
157
+ tools: [],
158
+ lastError: undefined,
159
+ lastConnected: undefined,
160
+ };
161
+
162
+ this.connections.set(config.id, connection);
163
+ this.saveServerConfigs();
164
+ this.notifyStateChange();
165
+
166
+ // Auto-connect if enabled
167
+ if (config.enabled) {
168
+ await this.connectToServer(config.id);
169
+ }
170
+ }
171
+
172
+ // Remove an MCP server
173
+ async removeServer(serverId: string): Promise<void> {
174
+ // Disconnect first if connected
175
+ await this.disconnectFromServer(serverId);
176
+
177
+ // Remove from our maps
178
+ this.connections.delete(serverId);
179
+ this.clients.delete(serverId);
180
+
181
+ this.saveServerConfigs();
182
+ this.notifyStateChange();
183
+ }
184
+
185
+ // Connect to an MCP server
186
+ async connectToServer(serverId: string): Promise<void> {
187
+ const connection = this.connections.get(serverId);
188
+ if (!connection) {
189
+ throw new Error(`Server ${serverId} not found`);
190
+ }
191
+
192
+ if (connection.isConnected) {
193
+ return; // Already connected
194
+ }
195
+
196
+ try {
197
+ // Create client
198
+ const client = new Client(
199
+ {
200
+ name: "LFM2-WebGPU",
201
+ version: "1.0.0",
202
+ },
203
+ {
204
+ capabilities: {
205
+ tools: {},
206
+ },
207
+ }
208
+ );
209
+
210
+ // Create transport based on config
211
+ let transport;
212
+ const url = new URL(connection.config.url);
213
+
214
+ // Prepare headers for authentication
215
+ const headers: Record<string, string> = {};
216
+ if (connection.config.auth) {
217
+ switch (connection.config.auth.type) {
218
+ case "bearer":
219
+ if (connection.config.auth.token) {
220
+ headers[
221
+ "Authorization"
222
+ ] = `Bearer ${connection.config.auth.token}`;
223
+ }
224
+ break;
225
+ case "basic":
226
+ if (
227
+ connection.config.auth.username &&
228
+ connection.config.auth.password
229
+ ) {
230
+ const credentials = btoa(
231
+ `${connection.config.auth.username}:${connection.config.auth.password}`
232
+ );
233
+ headers["Authorization"] = `Basic ${credentials}`;
234
+ }
235
+ break;
236
+ case "oauth":
237
+ if (connection.config.auth.token) {
238
+ headers[
239
+ "Authorization"
240
+ ] = `Bearer ${connection.config.auth.token}`;
241
+ }
242
+ break;
243
+ }
244
+ }
245
+
246
+ switch (connection.config.transport) {
247
+ case "websocket": {
248
+ // Convert HTTP/HTTPS URLs to WS/WSS
249
+ const wsUrl = new URL(connection.config.url);
250
+ wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
251
+ transport = new WebSocketClientTransport(wsUrl);
252
+ // Note: WebSocket auth headers would need to be passed differently
253
+ // For now, auth is only supported on HTTP-based transports
254
+ break;
255
+ }
256
+
257
+ case "streamable-http":
258
+ transport = new StreamableHTTPClientTransport(url, {
259
+ requestInit:
260
+ Object.keys(headers).length > 0 ? { headers } : undefined,
261
+ });
262
+ break;
263
+
264
+ case "sse":
265
+ transport = new SSEClientTransport(url, {
266
+ requestInit:
267
+ Object.keys(headers).length > 0 ? { headers } : undefined,
268
+ });
269
+ break;
270
+
271
+ default:
272
+ throw new Error(
273
+ `Unsupported transport: ${connection.config.transport}`
274
+ );
275
+ }
276
+
277
+ // Set up error handling
278
+ client.onerror = (error) => {
279
+ console.error(`MCP Client error for ${serverId}:`, error);
280
+ connection.lastError = error.message;
281
+ connection.isConnected = false;
282
+ this.notifyStateChange();
283
+ };
284
+
285
+ // Connect to the server
286
+ await client.connect(transport);
287
+
288
+ // List available tools
289
+ const toolsResult = await client.listTools();
290
+
291
+ // Update connection state
292
+ connection.isConnected = true;
293
+ connection.tools = toolsResult.tools;
294
+ connection.lastError = undefined;
295
+ connection.lastConnected = new Date();
296
+
297
+ // Store client reference
298
+ this.clients.set(serverId, client);
299
+
300
+ this.notifyStateChange();
301
+ } catch (error) {
302
+ console.error(`Failed to connect to MCP server ${serverId}:`, error);
303
+ connection.isConnected = false;
304
+ connection.lastError =
305
+ error instanceof Error ? error.message : "Connection failed";
306
+ this.notifyStateChange();
307
+ throw error;
308
+ }
309
+ }
310
+
311
+ // Disconnect from an MCP server
312
+ async disconnectFromServer(serverId: string): Promise<void> {
313
+ const client = this.clients.get(serverId);
314
+ const connection = this.connections.get(serverId);
315
+
316
+ if (client) {
317
+ try {
318
+ await client.close();
319
+ } catch (error) {
320
+ console.error(`Error disconnecting from ${serverId}:`, error);
321
+ }
322
+ this.clients.delete(serverId);
323
+ }
324
+
325
+ if (connection) {
326
+ connection.isConnected = false;
327
+ connection.tools = [];
328
+ this.notifyStateChange();
329
+ }
330
+ }
331
+
332
+ // Get all tools from all connected servers
333
+ getAllTools(): Tool[] {
334
+ const allTools: Tool[] = [];
335
+
336
+ for (const connection of this.connections.values()) {
337
+ if (connection.isConnected && connection.config.enabled) {
338
+ allTools.push(...connection.tools);
339
+ }
340
+ }
341
+
342
+ return allTools;
343
+ }
344
+
345
+ // Call a tool on an MCP server
346
+ async callTool(
347
+ serverId: string,
348
+ toolName: string,
349
+ args: Record<string, unknown>
350
+ ): Promise<MCPToolResult> {
351
+ const client = this.clients.get(serverId);
352
+ const connection = this.connections.get(serverId);
353
+
354
+ if (!client || !connection?.isConnected) {
355
+ throw new Error(`Not connected to server ${serverId}`);
356
+ }
357
+
358
+ try {
359
+ const result = await client.callTool({
360
+ name: toolName,
361
+ arguments: args,
362
+ });
363
+
364
+ return {
365
+ content: Array.isArray(result.content) ? result.content : [],
366
+ isError: Boolean(result.isError),
367
+ };
368
+ } catch (error) {
369
+ console.error(`Error calling tool ${toolName} on ${serverId}:`, error);
370
+ throw error;
371
+ }
372
+ }
373
+
374
+ // Test connection to a server without saving it
375
+ async testConnection(config: MCPServerConfig): Promise<boolean> {
376
+ try {
377
+ const client = new Client(
378
+ {
379
+ name: "LFM2-WebGPU-Test",
380
+ version: "1.0.0",
381
+ },
382
+ {
383
+ capabilities: {
384
+ tools: {},
385
+ },
386
+ }
387
+ );
388
+
389
+ let transport;
390
+ const url = new URL(config.url);
391
+
392
+ switch (config.transport) {
393
+ case "websocket": {
394
+ const wsUrl = new URL(config.url);
395
+ wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
396
+ transport = new WebSocketClientTransport(wsUrl);
397
+ break;
398
+ }
399
+
400
+ case "streamable-http":
401
+ transport = new StreamableHTTPClientTransport(url);
402
+ break;
403
+
404
+ case "sse":
405
+ transport = new SSEClientTransport(url);
406
+ break;
407
+
408
+ default:
409
+ throw new Error(`Unsupported transport: ${config.transport}`);
410
+ }
411
+
412
+ await client.connect(transport);
413
+ await client.close();
414
+ return true;
415
+ } catch (error) {
416
+ console.error("Test connection failed:", error);
417
+ return false;
418
+ }
419
+ }
420
+
421
+ // Connect to all enabled servers
422
+ async connectAll(): Promise<void> {
423
+ const promises = Array.from(this.connections.entries())
424
+ .filter(
425
+ ([, connection]) => connection.config.enabled && !connection.isConnected
426
+ )
427
+ .map(([serverId]) =>
428
+ this.connectToServer(serverId).catch((error) => {
429
+ console.error(`Failed to connect to ${serverId}:`, error);
430
+ })
431
+ );
432
+
433
+ await Promise.all(promises);
434
+ }
435
+
436
+ // Disconnect from all servers
437
+ async disconnectAll(): Promise<void> {
438
+ const promises = Array.from(this.connections.keys()).map((serverId) =>
439
+ this.disconnectFromServer(serverId)
440
+ );
441
+
442
+ await Promise.all(promises);
443
+ }
444
+ }
src/services/oauth.ts ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Utility for OAuth authentication with MCP servers
2
+ // Usage: import { authenticateWithOAuth } from "./oauth";
3
+
4
+ export async function authenticateWithOAuth({
5
+ authUrl,
6
+ clientId,
7
+ redirectUri,
8
+ scope = "openid profile",
9
+ }: {
10
+ authUrl: string;
11
+ clientId: string;
12
+ redirectUri: string;
13
+ scope?: string;
14
+ }): Promise<string> {
15
+ // Open a popup for OAuth login
16
+ const state = Math.random().toString(36).substring(2);
17
+ const url = `${authUrl}?response_type=token&client_id=${encodeURIComponent(
18
+ clientId
19
+ )}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(
20
+ scope
21
+ )}&state=${state}`;
22
+
23
+ return new Promise((resolve, reject) => {
24
+ const popup = window.open(url, "oauth-login", "width=500,height=700");
25
+ if (!popup) return reject("Popup blocked");
26
+
27
+ const interval = setInterval(() => {
28
+ try {
29
+ if (!popup || popup.closed) {
30
+ clearInterval(interval);
31
+ reject("Popup closed");
32
+ }
33
+ // Check for access token in URL
34
+ const href = popup.location.href;
35
+ if (href.startsWith(redirectUri)) {
36
+ const hash = popup.location.hash;
37
+ const params = new URLSearchParams(hash.substring(1));
38
+ const token = params.get("access_token");
39
+ if (token) {
40
+ clearInterval(interval);
41
+ popup.close();
42
+ resolve(token);
43
+ } else {
44
+ clearInterval(interval);
45
+ popup.close();
46
+ reject("No access token found");
47
+ }
48
+ }
49
+ } catch (e) {
50
+ // Ignore cross-origin errors until redirect
51
+ }
52
+ }, 500);
53
+ });
54
+ }
src/types/mcp.ts ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Tool as MCPTool } from "@modelcontextprotocol/sdk/types.js";
2
+
3
+ export interface MCPServerConfig {
4
+ id: string;
5
+ name: string;
6
+ url: string;
7
+ enabled: boolean;
8
+ transport: 'websocket' | 'sse' | 'streamable-http';
9
+ auth?: {
10
+ type: 'bearer' | 'basic' | 'oauth';
11
+ token?: string;
12
+ username?: string;
13
+ password?: string;
14
+ };
15
+ }
16
+
17
+ export interface MCPServerConnection {
18
+ config: MCPServerConfig;
19
+ isConnected: boolean;
20
+ tools: MCPTool[];
21
+ lastError?: string;
22
+ lastConnected?: Date;
23
+ }
24
+
25
+ // Extended Tool interface to support both local and MCP tools
26
+ export interface ExtendedTool {
27
+ id: number | string;
28
+ name: string;
29
+ enabled: boolean;
30
+ isCollapsed?: boolean;
31
+
32
+ // Local tool properties
33
+ code?: string;
34
+ renderer?: string;
35
+
36
+ // MCP tool properties
37
+ mcpServerId?: string;
38
+ mcpTool?: MCPTool;
39
+ isRemote?: boolean;
40
+ }
41
+
42
+ // MCP Tool execution result
43
+ export interface MCPToolResult {
44
+ content: Array<{
45
+ type: string;
46
+ text?: string;
47
+ data?: unknown;
48
+ mimeType?: string;
49
+ }>;
50
+ isError?: boolean;
51
+ }
52
+
53
+ // MCP Client state
54
+ export interface MCPClientState {
55
+ servers: Record<string, MCPServerConnection>;
56
+ isLoading: boolean;
57
+ error?: string;
58
+ }