From 0052539c1ee828c7406035ff6faada4f9190412c Mon Sep 17 00:00:00 2001 From: kaleel Date: Thu, 10 Apr 2025 12:14:13 +0000 Subject: [PATCH 1/3] added client specific server --- client/package.json | 2 + client/src/components/SettingsModal.tsx | 580 ++++++++++++------------ client/src/contexts/SettingsContext.tsx | 339 +++++++------- client/src/mcp/manager.ts | 140 ++++++ client/src/mcp/sessionManager.ts | 73 +++ client/src/mcp/toolRegistry.ts | 45 ++ 6 files changed, 727 insertions(+), 452 deletions(-) create mode 100644 client/src/mcp/manager.ts create mode 100644 client/src/mcp/sessionManager.ts create mode 100644 client/src/mcp/toolRegistry.ts diff --git a/client/package.json b/client/package.json index 1c27c5a..7694db8 100644 --- a/client/package.json +++ b/client/package.json @@ -6,6 +6,7 @@ "@chakra-ui/react": "^2.8.1", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@modelcontextprotocol/sdk": "^1.9.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^14.5.1", @@ -21,6 +22,7 @@ "react-markdown": "latest", "react-scripts": "5.0.1", "react-syntax-highlighter": "^15.5.0", + "socket.io": "^4.8.1", "socket.io-client": "^4.7.2", "typescript": "^4.9.5", "uuid": "^9.0.1", diff --git a/client/src/components/SettingsModal.tsx b/client/src/components/SettingsModal.tsx index 92bb390..0779939 100644 --- a/client/src/components/SettingsModal.tsx +++ b/client/src/components/SettingsModal.tsx @@ -1,325 +1,329 @@ // client/src/components/SettingsModal.tsx import React, { useState, useEffect } from "react"; import { - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalFooter, - ModalBody, - ModalCloseButton, - Button, - FormControl, - FormLabel, - Input, - Tabs, - TabList, - TabPanels, - Tab, - TabPanel, - VStack, - FormHelperText, - Box, - Heading, - Text, - Divider, - Flex, - Switch, - useToast, - IconButton, - InputGroup, - InputRightElement, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, + Button, + FormControl, + FormLabel, + Input, + Tabs, + TabList, + TabPanels, + Tab, + TabPanel, + VStack, + FormHelperText, + Box, + Heading, + Text, + Divider, + Flex, + Switch, + useToast, + IconButton, + InputGroup, + InputRightElement, } from "@chakra-ui/react"; import { FaEye, FaEyeSlash, FaPlus } from "react-icons/fa"; import { useSettingsContext } from "../contexts/SettingsContext"; interface SettingsModalProps { - isOpen: boolean; - onClose: () => void; + isOpen: boolean; + onClose: () => void; } const SettingsModal: React.FC = ({ isOpen, onClose }) => { - const { apiKey, setApiKey, nandaServers, registerNandaServer } = - useSettingsContext(); - const [tempApiKey, setTempApiKey] = useState(""); - const [showApiKey, setShowApiKey] = useState(false); - const [newServer, setNewServer] = useState({ - id: "", - name: "", - url: "", - }); - const toast = useToast(); - - // Reset temp values when modal opens - useEffect(() => { - if (isOpen) { - setTempApiKey(apiKey || ""); - setShowApiKey(false); - } - }, [isOpen, apiKey]); - - const handleSaveApiKey = () => { - setApiKey(tempApiKey); - toast({ - title: "API Key Saved", - status: "success", - duration: 3000, - isClosable: true, - position: "top", - }); - }; + const { apiKey, setApiKey, nandaServers, registerNandaServer } = + useSettingsContext(); + const [tempApiKey, setTempApiKey] = useState(""); + const [showApiKey, setShowApiKey] = useState(false); + const [newServer, setNewServer] = useState({ + id: "", + name: "", + url: "", + }); + const toast = useToast(); - const toggleShowApiKey = () => { - setShowApiKey(!showApiKey); - }; + // Reset temp values when modal opens + useEffect(() => { + if (isOpen) { + setTempApiKey(apiKey || ""); + setShowApiKey(false); + } + }, [isOpen, apiKey]); - const handleAddServer = () => { - // Validate server info - if (!newServer.id || !newServer.name || !newServer.url) { + const handleSaveApiKey = () => { + setApiKey(tempApiKey); toast({ - title: "Validation Error", - description: "All server fields are required", - status: "error", - duration: 3000, - isClosable: true, - position: "top", + title: "API Key Saved", + status: "success", + duration: 3000, + isClosable: true, + position: "top", }); - return; - } + }; - // Validate URL - try { - new URL(newServer.url); - } catch (error) { - toast({ - title: "Invalid URL", - description: - "Please enter a valid URL (e.g., http://localhost:3001/sse)", - status: "error", - duration: 3000, - isClosable: true, - position: "top", - }); - return; - } + const toggleShowApiKey = () => { + setShowApiKey(!showApiKey); + }; - // Register new server - registerNandaServer({ - id: newServer.id, - name: newServer.name, - url: newServer.url, - }); + const handleAddServer = () => { + // Validate server info + if (!newServer.id || !newServer.name || !newServer.url) { + toast({ + title: "Validation Error", + description: "All server fields are required", + status: "error", + duration: 3000, + isClosable: true, + position: "top", + }); + return; + } - // Reset form - setNewServer({ - id: "", - name: "", - url: "", - }); + // Validate URL + try { + new URL(newServer.url); + } catch (error) { + toast({ + title: "Invalid URL", + description: + "Please enter a valid URL (e.g., http://localhost:3001/sse)", + status: "error", + duration: 3000, + isClosable: true, + position: "top", + }); + return; + } + + // Register new server + registerNandaServer({ + id: newServer.id, + name: newServer.name, + url: newServer.url, + }); - toast({ - title: "Server Added", - description: `Server "${newServer.name}" has been added`, - status: "success", - duration: 3000, - isClosable: true, - position: "top", - }); - }; + // Reset form + setNewServer({ + id: "", + name: "", + url: "", + }); + + toast({ + title: "Server Added", + description: `Server "${newServer.name}" has been added`, + status: "success", + duration: 3000, + isClosable: true, + position: "top", + }); + }; - return ( - - - - Settings - - - - - API - Nanda Servers - About - + return ( + + + + Settings + + + + + API + Nanda Servers + About + - - {/* API Settings Tab */} - - - - Anthropic API Key - - setTempApiKey(e.target.value)} - placeholder="sk-ant-api03-..." - autoComplete="off" - /> - - : } - size="sm" - variant="ghost" - onClick={toggleShowApiKey} - /> - - - - You can get your API key from the{" "} - - Anthropic Console - - - + + {/* API Settings Tab */} + + + + Anthropic API Key + + setTempApiKey(e.target.value)} + placeholder="sk-ant-api03-..." + autoComplete="off" + color={"black"} + /> + + : } + size="sm" + variant="ghost" + onClick={toggleShowApiKey} + /> + + + + You can get your API key from the{" "} + + Anthropic Console + + + - - - + + + - {/* Nanda Servers Tab */} - - - - - Registered Nanda Servers - - {nandaServers.length === 0 ? ( - No servers registered yet - ) : ( - nandaServers.map((server) => ( - - {server.name} - - ID: {server.id} - - - URL: {server.url} - - - )) - )} - + {/* Nanda Servers Tab */} + + + + + Registered Nanda Servers + + {nandaServers.length === 0 ? ( + No servers registered yet + ) : ( + nandaServers.map((server) => ( + + {server.name} + + ID: {server.id} + + + URL: {server.url} + + + )) + )} + - + - - - Add New Server - + + + Add New Server + - - - Server ID - - setNewServer({ ...newServer, id: e.target.value }) - } - placeholder="weather" - /> - - A unique identifier for this server - - + + + Server ID + + setNewServer({ ...newServer, id: e.target.value }) + } + color={"black"} + placeholder="weather" + /> + + A unique identifier for this server + + - - Server Name - - setNewServer({ ...newServer, name: e.target.value }) - } - placeholder="Weather Server" - /> - - A friendly name for this server - - + + Server Name + + setNewServer({ ...newServer, name: e.target.value }) + } + color={"black"} + placeholder="Weather Server" + /> + + A friendly name for this server + + - - Server URL - - setNewServer({ ...newServer, url: e.target.value }) - } - placeholder="http://localhost:3001/sse" - /> - - The SSE endpoint URL of the Nanda server - - + + Server URL + + setNewServer({ ...newServer, url: e.target.value }) + } + color={"black"} + placeholder="http://localhost:3001/sse" + /> + + The SSE endpoint URL of the Nanda server + + - - - - - + + + + + - {/* About Tab */} - - - Nanda Host - - A beautiful chat interface with Nanda integration for - enhanced capabilities through agents, resources, and tools. - + {/* About Tab */} + + + Nanda Host + + A beautiful chat interface with Nanda integration for + enhanced capabilities through agents, resources, and tools. + - - - What is Nanda? - - - Nanda is an open standard built on top of MCP for enabling - coordination between agents, resources and tools (ARTs). - It enables LLMs to discover and execute tools, access - resources, and use predefined prompts. - - + + + What is Nanda? + + + Nanda is an open standard built on top of MCP for enabling + coordination between agents, resources and tools (ARTs). + It enables LLMs to discover and execute tools, access + resources, and use predefined prompts. + + - + - - Version 1.0.0 • MIT License - - - - - - + + Version 1.0.0 • MIT License + + + + + + - - - - - - ); + + + + + + ); }; export default SettingsModal; diff --git a/client/src/contexts/SettingsContext.tsx b/client/src/contexts/SettingsContext.tsx index ad5c74f..9c81aea 100644 --- a/client/src/contexts/SettingsContext.tsx +++ b/client/src/contexts/SettingsContext.tsx @@ -1,34 +1,36 @@ // client/src/contexts/SettingsContext.tsx +import { setupMcpManager } from "../mcp/manager"; import React, { - createContext, - useContext, - useState, - useEffect, - useCallback, + createContext, + useContext, + useState, + useEffect, + useCallback, } from "react"; interface ServerConfig { - id: string; - name: string; - url: string; + id: string; + name: string; + url: string; } interface SettingsContextProps { - apiKey: string | null; - setApiKey: (key: string) => void; - nandaServers: ServerConfig[]; - registerNandaServer: (server: ServerConfig) => void; - removeNandaServer: (id: string) => void; - refreshRegistry: () => Promise<{ servers: ServerConfig[] }>; + apiKey: string | null; + setApiKey: (key: string) => void; + nandaServers: ServerConfig[]; + registerNandaServer: (server: ServerConfig) => void; + removeNandaServer: (id: string) => void; + refreshRegistry: () => Promise<{ servers: ServerConfig[] }>; } +export const mcpManager = setupMcpManager() const SettingsContext = createContext({ - apiKey: null, - setApiKey: () => {}, - nandaServers: [], - registerNandaServer: () => {}, - removeNandaServer: () => {}, - refreshRegistry: async () => ({ servers: [] }), + apiKey: null, + setApiKey: () => { }, + nandaServers: [], + registerNandaServer: () => { }, + removeNandaServer: () => { }, + refreshRegistry: async () => ({ servers: [] }), }); export const useSettingsContext = () => useContext(SettingsContext); @@ -38,159 +40,168 @@ const API_KEY_STORAGE_KEY = "nanda_host_api_key"; const NANDA_SERVERS_STORAGE_KEY = "nanda_host_servers"; interface SettingsProviderProps { - children: React.ReactNode; + children: React.ReactNode; } export const SettingsProvider: React.FC = ({ - children, + children, }) => { - const [apiKey, setApiKeyState] = useState(null); - const [nandaServers, setNandaServers] = useState([]); - - // Load settings from local storage on mount - useEffect(() => { - // Load API key - const storedApiKey = localStorage.getItem(API_KEY_STORAGE_KEY); - if (storedApiKey) { - setApiKeyState(storedApiKey); - } - - // Load Nanda servers - const storedServers = localStorage.getItem(NANDA_SERVERS_STORAGE_KEY); - if (storedServers) { - try { - const parsedServers = JSON.parse(storedServers); - if (Array.isArray(parsedServers)) { - setNandaServers(parsedServers); - } - } catch (error) { - console.error("Failed to parse stored Nanda servers:", error); - } - } - }, []); - - // Save API key to local storage - const setApiKey = useCallback((key: string) => { - setApiKeyState(key); - localStorage.setItem(API_KEY_STORAGE_KEY, key); - }, []); - - // Register Nanda server - const registerNandaServer = useCallback((server: ServerConfig) => { - setNandaServers((prevServers) => { - // Check if server with this ID already exists - const existingIndex = prevServers.findIndex((s) => s.id === server.id); - let newServers: ServerConfig[]; - - if (existingIndex >= 0) { - // Update existing server - newServers = [...prevServers]; - newServers[existingIndex] = server; - } else { - // Add new server - newServers = [...prevServers, server]; + const [apiKey, setApiKeyState] = useState(null); + const [nandaServers, setNandaServers] = useState([]); + + // Load settings from local storage on mount + useEffect(() => { + // Load API key + const storedApiKey = localStorage.getItem(API_KEY_STORAGE_KEY); + if (storedApiKey) { + setApiKeyState(storedApiKey); } - // Save to local storage - localStorage.setItem( - NANDA_SERVERS_STORAGE_KEY, - JSON.stringify(newServers) - ); - - // Also register with backend - fetch(`${process.env.REACT_APP_API_BASE_URL}/api/servers`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(server), - }).catch((error) => { - console.error("Failed to register server with backend:", error); + // Load Nanda servers + const storedServers = localStorage.getItem(NANDA_SERVERS_STORAGE_KEY); + if (storedServers) { + try { + const parsedServers = JSON.parse(storedServers); + if (Array.isArray(parsedServers)) { + setNandaServers(parsedServers); + } + } catch (error) { + console.error("Failed to parse stored Nanda servers:", error); + } + } + }, []); + + // Save API key to local storage + const setApiKey = useCallback((key: string) => { + setApiKeyState(key); + localStorage.setItem(API_KEY_STORAGE_KEY, key); + }, []); + + // Register Nanda server + const registerNandaServer = useCallback((server: ServerConfig) => { + setNandaServers((prevServers) => { + // Check if server with this ID already exists + const existingIndex = prevServers.findIndex((s) => s.id === server.id); + let newServers: ServerConfig[]; + + if (existingIndex >= 0) { + // Update existing server + newServers = [...prevServers]; + newServers[existingIndex] = server; + } else { + // Add new server + mcpManager.registerServer(server); + newServers = [...prevServers, server]; + } + + // Save to local storage + localStorage.setItem( + NANDA_SERVERS_STORAGE_KEY, + JSON.stringify(newServers) + ); + + // Also register with backend + // fetch(`${process.env.REACT_APP_API_BASE_URL}/api/servers`, { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // }, + // body: JSON.stringify(server), + // }).catch((error) => { + // console.error("Failed to register server with backend:", error); + // }); + return newServers; }); + }, []); + + // Remove Nanda server + const removeNandaServer = useCallback((id: string) => { + setNandaServers((prevServers) => { + const newServers = prevServers.filter((server) => server.id !== id); + localStorage.setItem( + NANDA_SERVERS_STORAGE_KEY, + JSON.stringify(newServers) + ); + return newServers; + }); + }, []); - return newServers; - }); - }, []); - - // Remove Nanda server - const removeNandaServer = useCallback((id: string) => { - setNandaServers((prevServers) => { - const newServers = prevServers.filter((server) => server.id !== id); - localStorage.setItem( - NANDA_SERVERS_STORAGE_KEY, - JSON.stringify(newServers) - ); - return newServers; - }); - }, []); - - // Register servers with backend on initial load - useEffect(() => { - if (nandaServers.length > 0) { - // Register all servers with the backend + // Register servers with backend on initial load + useEffect(() => { const registerAllServers = async () => { - for (const server of nandaServers) { - try { - await fetch(`${process.env.REACT_APP_API_BASE_URL}/api/servers`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(server), - }); - } catch (error) { - console.error( - `Failed to register server ${server.id} with backend:`, - error - ); - } - } - }; - - registerAllServers(); - } - }, []); - - // Refresh servers from registry - const refreshRegistry = useCallback(async () => { - try { - const response = await fetch( - `${process.env.REACT_APP_API_BASE_URL}/api/registry/refresh`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - } - ); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error( - errorData.error || "Failed to refresh servers from registry" - ); + for (const server of nandaServers) { + mcpManager.registerServer(server) + // try { + // await fetch(`${process.env.REACT_APP_API_BASE_URL}/api/servers`, { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // }, + // body: JSON.stringify(server), + // }); + // } catch (error) { + // console.error( + // `Failed to register server ${server.id} with backend:`, + // error + // ); + } } + if (nandaServers.length > 0) { + + if (mcpManager.getAvailableServers().length > 0) { + registerAllServers(); + } + }; - const data = await response.json(); - return data; - } catch (error) { - console.error("Error refreshing servers from registry:", error); - throw error; - } - }, []); - - return ( - - {children} - - ); + // const loadServersFromLocalstorage = () => { + // return localStorage.getItem(NANDA_SERVERS_STORAGE_KEY) + // } + + }, []); + + // Refresh servers from registry + const refreshRegistry = useCallback(async () => { + // try { + // const response = await fetch( + // `${process.env.REACT_APP_API_BASE_URL}/api/registry/refresh`, + // { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // }, + // } + // ); + + // if (!response.ok) { + // const errorData = await response.json(); + // throw new Error( + // errorData.error || "Failed to refresh servers from registry" + // ); + // } + + // const data = await response.json(); + // return data; + // } catch (error) { + // console.error("Error refreshing servers from registry:", error); + // throw error; + // } + // implment custom function for refreshRegistry + + return { servers: mcpManager.getAvailableServers() } + }, []); + + return ( + + {children} + + ); }; diff --git a/client/src/mcp/manager.ts b/client/src/mcp/manager.ts new file mode 100644 index 0000000..74a2378 --- /dev/null +++ b/client/src/mcp/manager.ts @@ -0,0 +1,140 @@ +// server/src/mcp/manager.ts +import { Server as SocketIoServer } from "socket.io"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; +import { Tool } from "@modelcontextprotocol/sdk/types.js"; +import { ToolRegistry } from "./toolRegistry"; + +export interface McpManager { + discoverTools: (sessionId: string) => Promise; + executeToolCall: ( + sessionId: string, + toolName: string, + args: any + ) => Promise; + registerServer: (serverConfig: ServerConfig) => Promise; + getAvailableServers: () => ServerConfig[]; + cleanup: () => Promise; +} + +export interface ServerConfig { + id: string; + name: string; + url: string; +} + +export function setupMcpManager(): McpManager { + // Registry to keep track of available MCP tools + const toolRegistry = new ToolRegistry(); + + // Session manager to handle client sessions + // const sessionManager = new SessionManager(); + + // Available server configurations + const servers: ServerConfig[] = []; + + // Cache of connected clients + const connectedClients: Map = new Map(); + + const registerServer = async (serverConfig: ServerConfig): Promise => { + servers.push(serverConfig); + + try { + // Create MCP client for this server using SSE transport + const sseUrl = new URL(serverConfig.url); + const transport = new SSEClientTransport(sseUrl); + + const client = new Client({ + name: "mcp-host", + version: "1.0.0", + }); + + await client.connect(transport); + + // Fetch available tools from the server + const toolsResult = await client.listTools(); + + // Register tools in our registry + if (toolsResult?.tools) { + toolRegistry.registerTools(serverConfig.id, client, toolsResult.tools); + } + + // Store the connected client for later use + connectedClients.set(serverConfig.id, client); + + console.log( + `Registered server ${serverConfig.name} with ${toolsResult?.tools?.length || 0 + } tools` + ); + } catch (error) { + console.error( + `Failed to connect to MCP server ${serverConfig.name}:`, + error + ); + // Remove the server from our list since we couldn't connect + const index = servers.findIndex((s) => s.id === serverConfig.id); + if (index !== -1) { + servers.splice(index, 1); + } + } + }; + + // Discover all available tools for a session + const discoverTools = async (): Promise => { + return toolRegistry.getAllTools(); + }; + + // Execute a tool call + const executeToolCall = async ( + sessionId: string, + toolName: string, + args: any + ): Promise => { + const toolInfo = toolRegistry.getToolInfo(toolName); + if (!toolInfo) { + throw new Error(`Tool ${toolName} not found`); + } + + const { client, tool } = toolInfo; + + try { + // Execute the tool via MCP + const result = await client.callTool({ + name: toolName, + arguments: args, + }); + + return result; + } catch (error) { + console.error(`Error executing tool ${toolName}:`, error); + throw error; + } + }; + + // Get all available server configurations + const getAvailableServers = (): ServerConfig[] => { + return [...servers]; + }; + + // Clean up connections when closing + const cleanup = async (): Promise => { + for (const [serverId, client] of Array.from(connectedClients.entries())) { + try { + await client.close(); + console.log(`Closed connection to server ${serverId}`); + } catch (error) { + console.error(`Error closing connection to server ${serverId}:`, error); + } + } + connectedClients.clear(); + }; + + // Return the MCP manager interface + return { + discoverTools, + executeToolCall, + registerServer, + getAvailableServers, + cleanup, + }; +} diff --git a/client/src/mcp/sessionManager.ts b/client/src/mcp/sessionManager.ts new file mode 100644 index 0000000..52331a6 --- /dev/null +++ b/client/src/mcp/sessionManager.ts @@ -0,0 +1,73 @@ +// server/src/mcp/sessionManager.ts +import { v4 as uuidv4 } from "uuid"; + +interface Session { + id: string; + anthropicApiKey?: string; + createdAt: Date; + lastActive: Date; +} + +export class SessionManager { + private sessions: Map = new Map(); + + // Session cleanup interval in milliseconds (1 hour) + private readonly CLEANUP_INTERVAL = 60 * 60 * 1000; + + constructor() { + // Set up session cleanup + setInterval(() => this.cleanupSessions(), this.CLEANUP_INTERVAL); + } + + createSession(): string { + const sessionId = uuidv4(); + const now = new Date(); + + this.sessions.set(sessionId, { + id: sessionId, + createdAt: now, + lastActive: now, + }); + + return sessionId; + } + + getSession(sessionId: string): Session | undefined { + const session = this.sessions.get(sessionId); + + if (session) { + // Update last active time + session.lastActive = new Date(); + this.sessions.set(sessionId, session); + } + + return session; + } + + setAnthropicApiKey(sessionId: string, apiKey: string): void { + const session = this.sessions.get(sessionId); + + if (session) { + session.anthropicApiKey = apiKey; + session.lastActive = new Date(); + this.sessions.set(sessionId, session); + } + } + + getAnthropicApiKey(sessionId: string): string | undefined { + return this.sessions.get(sessionId)?.anthropicApiKey; + } + + private cleanupSessions(): void { + const now = new Date(); + const SESSION_TIMEOUT = 24 * 60 * 60 * 1000; // 24 hours + + for (const [sessionId, session] of Array.from(this.sessions.entries())) { + const inactiveTime = now.getTime() - session.lastActive.getTime(); + + if (inactiveTime > SESSION_TIMEOUT) { + this.sessions.delete(sessionId); + } + } + } +} diff --git a/client/src/mcp/toolRegistry.ts b/client/src/mcp/toolRegistry.ts new file mode 100644 index 0000000..ce7d306 --- /dev/null +++ b/client/src/mcp/toolRegistry.ts @@ -0,0 +1,45 @@ +// server/src/mcp/toolRegistry.ts +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { Tool } from "@modelcontextprotocol/sdk/types.js"; + +interface ToolInfo { + serverId: string; + client: Client; + tool: Tool; +} + +export class ToolRegistry { + private tools: Map = new Map(); + + registerTools(serverId: string, client: Client, tools: Tool[]): void { + for (const tool of tools) { + this.tools.set(tool.name, { + serverId, + client, + tool, + }); + } + } + + getToolInfo(toolName: string): ToolInfo | undefined { + return this.tools.get(toolName); + } + + getAllTools(): Tool[] { + return Array.from(this.tools.values()).map((info) => info.tool); + } + + getToolsByServerId(serverId: string): Tool[] { + return Array.from(this.tools.values()) + .filter((info) => info.serverId === serverId) + .map((info) => info.tool); + } + + removeToolsByServerId(serverId: string): void { + for (const [toolName, info] of Array.from(this.tools.entries())) { + if (info.serverId === serverId) { + this.tools.delete(toolName); + } + } + } +} From 983783a4ed40da0c1dd08f03c0e900ade0158098 Mon Sep 17 00:00:00 2001 From: kaleel Date: Thu, 10 Apr 2025 12:23:19 +0000 Subject: [PATCH 2/3] fixed: registerAllServers --- client/src/contexts/SettingsContext.tsx | 23 ++--------------------- client/src/mcp/manager.ts | 1 - 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/client/src/contexts/SettingsContext.tsx b/client/src/contexts/SettingsContext.tsx index 9c81aea..5842f2f 100644 --- a/client/src/contexts/SettingsContext.tsx +++ b/client/src/contexts/SettingsContext.tsx @@ -99,17 +99,6 @@ export const SettingsProvider: React.FC = ({ NANDA_SERVERS_STORAGE_KEY, JSON.stringify(newServers) ); - - // Also register with backend - // fetch(`${process.env.REACT_APP_API_BASE_URL}/api/servers`, { - // method: "POST", - // headers: { - // "Content-Type": "application/json", - // }, - // body: JSON.stringify(server), - // }).catch((error) => { - // console.error("Failed to register server with backend:", error); - // }); return newServers; }); }, []); @@ -146,17 +135,9 @@ export const SettingsProvider: React.FC = ({ // ); } } - if (nandaServers.length > 0) { - - if (mcpManager.getAvailableServers().length > 0) { - registerAllServers(); - } + if (nandaServers.length > 0 && mcpManager.getAvailableServers().length > 0) { + registerAllServers(); }; - - // const loadServersFromLocalstorage = () => { - // return localStorage.getItem(NANDA_SERVERS_STORAGE_KEY) - // } - }, []); // Refresh servers from registry diff --git a/client/src/mcp/manager.ts b/client/src/mcp/manager.ts index 74a2378..0c4e5ca 100644 --- a/client/src/mcp/manager.ts +++ b/client/src/mcp/manager.ts @@ -1,5 +1,4 @@ // server/src/mcp/manager.ts -import { Server as SocketIoServer } from "socket.io"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { Tool } from "@modelcontextprotocol/sdk/types.js"; From aba807038571aeb50a084c9f84c4afa543a2c9d9 Mon Sep 17 00:00:00 2001 From: kaleel Date: Thu, 10 Apr 2025 20:06:37 +0000 Subject: [PATCH 3/3] fix indentation --- client/src/contexts/SettingsContext.tsx | 316 ++++++++++++------------ client/src/mcp/manager.ts | 2 +- 2 files changed, 159 insertions(+), 159 deletions(-) diff --git a/client/src/contexts/SettingsContext.tsx b/client/src/contexts/SettingsContext.tsx index 5842f2f..7088561 100644 --- a/client/src/contexts/SettingsContext.tsx +++ b/client/src/contexts/SettingsContext.tsx @@ -1,36 +1,36 @@ // client/src/contexts/SettingsContext.tsx import { setupMcpManager } from "../mcp/manager"; import React, { - createContext, - useContext, - useState, - useEffect, - useCallback, + createContext, + useContext, + useState, + useEffect, + useCallback, } from "react"; interface ServerConfig { - id: string; - name: string; - url: string; + id: string; + name: string; + url: string; } interface SettingsContextProps { - apiKey: string | null; - setApiKey: (key: string) => void; - nandaServers: ServerConfig[]; - registerNandaServer: (server: ServerConfig) => void; - removeNandaServer: (id: string) => void; - refreshRegistry: () => Promise<{ servers: ServerConfig[] }>; + apiKey: string | null; + setApiKey: (key: string) => void; + nandaServers: ServerConfig[]; + registerNandaServer: (server: ServerConfig) => void; + removeNandaServer: (id: string) => void; + refreshRegistry: () => Promise<{ servers: ServerConfig[] }>; } export const mcpManager = setupMcpManager() const SettingsContext = createContext({ - apiKey: null, - setApiKey: () => { }, - nandaServers: [], - registerNandaServer: () => { }, - removeNandaServer: () => { }, - refreshRegistry: async () => ({ servers: [] }), + apiKey: null, + setApiKey: () => { }, + nandaServers: [], + registerNandaServer: () => { }, + removeNandaServer: () => { }, + refreshRegistry: async () => ({ servers: [] }), }); export const useSettingsContext = () => useContext(SettingsContext); @@ -40,149 +40,149 @@ const API_KEY_STORAGE_KEY = "nanda_host_api_key"; const NANDA_SERVERS_STORAGE_KEY = "nanda_host_servers"; interface SettingsProviderProps { - children: React.ReactNode; + children: React.ReactNode; } export const SettingsProvider: React.FC = ({ - children, + children, }) => { - const [apiKey, setApiKeyState] = useState(null); - const [nandaServers, setNandaServers] = useState([]); - - // Load settings from local storage on mount - useEffect(() => { - // Load API key - const storedApiKey = localStorage.getItem(API_KEY_STORAGE_KEY); - if (storedApiKey) { - setApiKeyState(storedApiKey); + const [apiKey, setApiKeyState] = useState(null); + const [nandaServers, setNandaServers] = useState([]); + + // Load settings from local storage on mount + useEffect(() => { + // Load API key + const storedApiKey = localStorage.getItem(API_KEY_STORAGE_KEY); + if (storedApiKey) { + setApiKeyState(storedApiKey); + } + + // Load Nanda servers + const storedServers = localStorage.getItem(NANDA_SERVERS_STORAGE_KEY); + if (storedServers) { + try { + const parsedServers = JSON.parse(storedServers); + if (Array.isArray(parsedServers)) { + setNandaServers(parsedServers); + } + } catch (error) { + console.error("Failed to parse stored Nanda servers:", error); } - - // Load Nanda servers - const storedServers = localStorage.getItem(NANDA_SERVERS_STORAGE_KEY); - if (storedServers) { - try { - const parsedServers = JSON.parse(storedServers); - if (Array.isArray(parsedServers)) { - setNandaServers(parsedServers); - } - } catch (error) { - console.error("Failed to parse stored Nanda servers:", error); - } + } + }, []); + + // Save API key to local storage + const setApiKey = useCallback((key: string) => { + setApiKeyState(key); + localStorage.setItem(API_KEY_STORAGE_KEY, key); + }, []); + + // Register Nanda server + const registerNandaServer = useCallback((server: ServerConfig) => { + setNandaServers((prevServers) => { + // Check if server with this ID already exists + const existingIndex = prevServers.findIndex((s) => s.id === server.id); + let newServers: ServerConfig[]; + + if (existingIndex >= 0) { + // Update existing server + newServers = [...prevServers]; + newServers[existingIndex] = server; + } else { + // Add new server + mcpManager.registerServer(server); + newServers = [...prevServers, server]; } - }, []); - - // Save API key to local storage - const setApiKey = useCallback((key: string) => { - setApiKeyState(key); - localStorage.setItem(API_KEY_STORAGE_KEY, key); - }, []); - - // Register Nanda server - const registerNandaServer = useCallback((server: ServerConfig) => { - setNandaServers((prevServers) => { - // Check if server with this ID already exists - const existingIndex = prevServers.findIndex((s) => s.id === server.id); - let newServers: ServerConfig[]; - - if (existingIndex >= 0) { - // Update existing server - newServers = [...prevServers]; - newServers[existingIndex] = server; - } else { - // Add new server - mcpManager.registerServer(server); - newServers = [...prevServers, server]; - } - - // Save to local storage - localStorage.setItem( - NANDA_SERVERS_STORAGE_KEY, - JSON.stringify(newServers) - ); - return newServers; - }); - }, []); - - // Remove Nanda server - const removeNandaServer = useCallback((id: string) => { - setNandaServers((prevServers) => { - const newServers = prevServers.filter((server) => server.id !== id); - localStorage.setItem( - NANDA_SERVERS_STORAGE_KEY, - JSON.stringify(newServers) - ); - return newServers; - }); - }, []); - - // Register servers with backend on initial load - useEffect(() => { - const registerAllServers = async () => { - for (const server of nandaServers) { - mcpManager.registerServer(server) - // try { - // await fetch(`${process.env.REACT_APP_API_BASE_URL}/api/servers`, { - // method: "POST", - // headers: { - // "Content-Type": "application/json", - // }, - // body: JSON.stringify(server), - // }); - // } catch (error) { - // console.error( - // `Failed to register server ${server.id} with backend:`, - // error - // ); - } + + // Save to local storage + localStorage.setItem( + NANDA_SERVERS_STORAGE_KEY, + JSON.stringify(newServers) + ); + return newServers; + }); + }, []); + + // Remove Nanda server + const removeNandaServer = useCallback((id: string) => { + setNandaServers((prevServers) => { + const newServers = prevServers.filter((server) => server.id !== id); + localStorage.setItem( + NANDA_SERVERS_STORAGE_KEY, + JSON.stringify(newServers) + ); + return newServers; + }); + }, []); + + // Register servers with mcpmanager on initial load + useEffect(() => { + const registerAllServers = async () => { + for (const server of nandaServers) { + mcpManager.registerServer(server) + // try { + // await fetch(`${process.env.REACT_APP_API_BASE_URL}/api/servers`, { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // }, + // body: JSON.stringify(server), + // }); + // } catch (error) { + // console.error( + // `Failed to register server ${server.id} with backend:`, + // error + // ); } - if (nandaServers.length > 0 && mcpManager.getAvailableServers().length > 0) { - registerAllServers(); - }; - }, []); - - // Refresh servers from registry - const refreshRegistry = useCallback(async () => { - // try { - // const response = await fetch( - // `${process.env.REACT_APP_API_BASE_URL}/api/registry/refresh`, - // { - // method: "POST", - // headers: { - // "Content-Type": "application/json", - // }, - // } - // ); - - // if (!response.ok) { - // const errorData = await response.json(); - // throw new Error( - // errorData.error || "Failed to refresh servers from registry" - // ); - // } - - // const data = await response.json(); - // return data; - // } catch (error) { - // console.error("Error refreshing servers from registry:", error); - // throw error; - // } - // implment custom function for refreshRegistry - - return { servers: mcpManager.getAvailableServers() } - }, []); - - return ( - - {children} - - ); + } + if (nandaServers.length > 0 && mcpManager.getAvailableServers().length > 0) { + registerAllServers(); + }; + }, []); + + // Refresh servers from registry + const refreshRegistry = useCallback(async () => { + // try { + // const response = await fetch( + // `${process.env.REACT_APP_API_BASE_URL}/api/registry/refresh`, + // { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // }, + // } + // ); + + // if (!response.ok) { + // const errorData = await response.json(); + // throw new Error( + // errorData.error || "Failed to refresh servers from registry" + // ); + // } + + // const data = await response.json(); + // return data; + // } catch (error) { + // console.error("Error refreshing servers from registry:", error); + // throw error; + // } + // implment custom function for refreshRegistry + + return { servers: mcpManager.getAvailableServers() } + }, []); + + return ( + + {children} + + ); }; diff --git a/client/src/mcp/manager.ts b/client/src/mcp/manager.ts index 0c4e5ca..e426732 100644 --- a/client/src/mcp/manager.ts +++ b/client/src/mcp/manager.ts @@ -5,7 +5,7 @@ import { Tool } from "@modelcontextprotocol/sdk/types.js"; import { ToolRegistry } from "./toolRegistry"; export interface McpManager { - discoverTools: (sessionId: string) => Promise; + discoverTools: () => Promise; executeToolCall: ( sessionId: string, toolName: string,