diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..60b355f
Binary files /dev/null and b/.DS_Store differ
diff --git a/.env b/.env
new file mode 100644
index 0000000..9842338
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+API_BASE_URL=http://localhost:3000
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index db67f50..7399fdc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,5 @@ client/node_modules
client/dist
client/package-lock.json
client/build
+**/.DS_Store
+node_modules
\ No newline at end of file
diff --git a/Dockerfile.client b/Dockerfile.client
index 6302ea2..fe5cba4 100644
--- a/Dockerfile.client
+++ b/Dockerfile.client
@@ -11,6 +11,10 @@ RUN npm install
COPY client/ ./
COPY shared/ ./shared/
+# Set API base URL as an ARG that can be passed during build
+ARG REACT_APP_API_BASE_URL=http://localhost:4000
+ENV REACT_APP_API_BASE_URL=${REACT_APP_API_BASE_URL}
+
# Build the application
RUN npm run build
@@ -23,9 +27,6 @@ COPY --from=build /app/build /usr/share/nginx/html
# Copy nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
-# Set environment variables
-ENV API_BASE_URL=http://localhost:4000
-
# Expose port
EXPOSE 3000
diff --git a/Dockerfile.server b/Dockerfile.server
index 97c9519..d13aa81 100644
--- a/Dockerfile.server
+++ b/Dockerfile.server
@@ -1,18 +1,25 @@
# Dockerfile.server
FROM node:20-alpine
+# Set base working directory
WORKDIR /app
-# Copy package files
+# Copy shared code first
+COPY shared/ ./shared/
+
+# Create server directory and copy server-specific files
+WORKDIR /app/server
COPY server/package*.json ./
COPY server/tsconfig.json ./
+
+# Install dependencies in the server directory
RUN npm install
-# Copy source code
+# Copy server source code relative to the new WORKDIR (/app/server)
COPY server/src ./src
-COPY shared/ ./shared/
-# Build the application
+# Build the application (tsc will run in /app/server)
+# With rootDir: "..", output structure will be nested under ./dist
RUN npm run build
# Expose the server port
@@ -22,5 +29,6 @@ EXPOSE 4000
ENV NODE_ENV=production
ENV PORT=4000
-# Start the server
-CMD ["node", "dist/index.js"]
\ No newline at end of file
+# Start the server from the nested dist folder structure
+# e.g., /app/server/dist/server/src/index.js
+CMD ["node", "dist/server/src/index.js"]
\ No newline at end of file
diff --git a/README.md b/README.md
index 9036730..ac82bae 100644
--- a/README.md
+++ b/README.md
@@ -2,11 +2,11 @@
MCP Host is a complete end-to-end implementation of a Model Context Protocol (MCP) host with an in-built MCP client. It provides a beautiful, polished chat interface with tool selection capabilities using MCP, and integrates with the MCP Registry for discovering available servers.
-
+
## Features
-- **Beautiful Chat Interface**: A clean, modern UI with glassmorphism design and a crimson color theme
+- **Chat Interface**: A modern UI with a glassmorphism design and crimson theme
- **MCP Client Integration**: Discover and use tools from MCP servers
- **Registry Integration**: Automatically connect to servers listed in the MCP Registry
- **Anthropic API Integration**: Powered by Claude, one of the most capable AI assistants
@@ -25,8 +25,8 @@ MCP Host is a complete end-to-end implementation of a Model Context Protocol (MC
1. Clone the repository:
```bash
- git clone https://github.com/yourusername/mcp-host.git
- cd mcp-host
+ git clone https://github.com/aidecentralized/canvas.git
+ cd canvas
```
2. Start the application with Docker Compose:
@@ -96,7 +96,7 @@ server.tool("my-tool", { param: z.string() }, async ({ param }) => ({
const app = express();
const port = 3001;
-// To support multiple simultaneous connections, we use a lookup object
+// To support multiple simultaneous connections, a session-based lookup object is used
const transports: { [sessionId: string]: SSEServerTransport } = {};
// SSE endpoint
diff --git a/alyssa-changes.txt b/alyssa-changes.txt
new file mode 100644
index 0000000..a963d14
--- /dev/null
+++ b/alyssa-changes.txt
@@ -0,0 +1,21 @@
+1. In docker-compose.yml version is depricated in new format of Docker. Removed this line version: "3.8" from line 1
+
+2. Added .env file with API_BASE_URL as http://localhost:3000
+
+3. In server/src/routes.ts:
+try {
+ await mcpManager.registerServer({ id, name, url });
+ res.json({ success: true }); <------------------------ this always returns success regardless of outcome
+ } catch (error) {
+ console.error("Error registering server:", error);
+ res.status(500).json({
+ error:
+ error.message || "An error occurred while registering the server",
+ });
+ }
+ });
+
+ Fix:
+ A. In server/src/mcp/manager.ts, in registerServer() made a chnage to returned values
+ B. Changed the receival in routes.ts
+ C. In client/src/components/SettingsModal.tsx edited handleAddServer
\ No newline at end of file
diff --git a/client/.env b/client/.env
new file mode 100644
index 0000000..66f49f9
--- /dev/null
+++ b/client/.env
@@ -0,0 +1 @@
+REACT_APP_API_BASE_URL=http://localhost:4000
\ No newline at end of file
diff --git a/client/package.json b/client/package.json
index 1c27c5a..95efcd3 100644
--- a/client/package.json
+++ b/client/package.json
@@ -10,11 +10,13 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.5.1",
"@types/jest": "^28.1.8",
+ "@types/lodash": "^4.17.16",
"@types/node": "^12.20.55",
"@types/react": "^18.2.31",
"@types/react-dom": "^18.2.14",
"axios": "^1.8.4",
"framer-motion": "^6.5.1",
+ "lodash": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.11.0",
@@ -52,7 +54,7 @@
},
"devDependencies": {
"@types/react-syntax-highlighter": "^15.5.9",
- "@types/uuid": "^9.0.6"
+ "@types/uuid": "^9.0.8"
},
"proxy": "http://localhost:4000"
}
diff --git a/client/public/NANDA.png b/client/public/NANDA.png
new file mode 100644
index 0000000..de1bcbb
Binary files /dev/null and b/client/public/NANDA.png differ
diff --git a/client/public/index.html b/client/public/index.html
index 200457c..2a663dc 100644
--- a/client/public/index.html
+++ b/client/public/index.html
@@ -5,18 +5,29 @@
-
+
+
+
+
+
Nanda Host
@@ -28,6 +39,9 @@
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the tag.
+
+ To begin the development, run `npm start` or `yarn start`.
+ To create a production bundle, use `npm run build` or `yarn build`.
-->
diff --git a/client/public/manifest.json b/client/public/manifest.json
index ba41248..ffd8a42 100644
--- a/client/public/manifest.json
+++ b/client/public/manifest.json
@@ -1,19 +1,19 @@
{
- "short_name": "Nanda Host",
- "name": "Nanda Host",
+ "short_name": "NANDA Host",
+ "name": "NANDA Host",
"icons": [
{
- "src": "favicon.ico",
+ "src": "NANDA.png",
"sizes": "64x64 32x32 24x24 16x16",
- "type": "image/x-icon"
+ "type": "image/png"
},
{
- "src": "logo192.png",
+ "src": "NANDA.png",
"type": "image/png",
"sizes": "192x192"
},
{
- "src": "logo512.png",
+ "src": "NANDA.png",
"type": "image/png",
"sizes": "512x512"
}
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 558fecd..c4c84ed 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -25,9 +25,25 @@ function App() {
@@ -37,6 +53,8 @@ function App() {
align="center"
justify="center"
p={4}
+ position="relative"
+ zIndex={1}
>
diff --git a/client/src/components/ActivityLog.tsx b/client/src/components/ActivityLog.tsx
new file mode 100644
index 0000000..6033112
--- /dev/null
+++ b/client/src/components/ActivityLog.tsx
@@ -0,0 +1,190 @@
+import React from 'react';
+import {
+ Box,
+ VStack,
+ Text,
+ Heading,
+ Flex,
+ Icon,
+ Badge,
+ Divider,
+ Collapse,
+ Button,
+ useDisclosure,
+ Tooltip,
+} from '@chakra-ui/react';
+import { FaServer, FaTools, FaExclamationTriangle, FaInfo, FaAngleDown, FaAngleUp, FaTrash } from 'react-icons/fa';
+import { useChatContext } from '../contexts/ChatContext';
+import type { LogEntry } from '../contexts/ChatContext';
+
+const getLogIcon = (type: LogEntry['type']) => {
+ switch (type) {
+ case 'server-selection':
+ return FaServer;
+ case 'tool-execution':
+ return FaTools;
+ case 'error':
+ return FaExclamationTriangle;
+ case 'info':
+ default:
+ return FaInfo;
+ }
+};
+
+const getLogColor = (type: LogEntry['type']) => {
+ switch (type) {
+ case 'server-selection':
+ return '#547AA5';
+ case 'tool-execution':
+ return '#4A6B52';
+ case 'error':
+ return '#8B3A4A';
+ case 'info':
+ default:
+ return '#7E5A8E';
+ }
+};
+
+const LogEntryItem: React.FC<{ log: LogEntry }> = ({ log }) => {
+ const { isOpen, onToggle } = useDisclosure();
+ const colorMap = {
+ 'server-selection': { icon: '#7EAFD8', bg: '#1A2A3A', border: '#3B5D7D' },
+ 'tool-execution': { icon: '#6CAC70', bg: '#1A2A20', border: '#3E5A43' },
+ 'error': { icon: '#BC6C78', bg: '#2A1F24', border: '#8B3A4A' },
+ 'info': { icon: '#B79EC0', bg: '#25202A', border: '#6C5A78' }
+ };
+
+ const style = colorMap[log.type] || colorMap.info;
+
+ return (
+
+
+
+
+
+ {log.message}
+
+
+
+
+ {new Date(log.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
+
+
+
+
+
+
+
+ {log.details && (
+
+ {Object.entries(log.details).map(([key, value]) => (
+
+ {key}:
+
+ {typeof value === 'object'
+ ? JSON.stringify(value, null, 2)
+ : String(value)
+ }
+
+
+ ))}
+
+ )}
+
+
+
+ );
+};
+
+const ActivityLog: React.FC = () => {
+ const { activityLogs, clearLogs } = useChatContext();
+ const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
+
+ return (
+
+
+
+
+ Activity Log
+
+
+ {activityLogs.length}
+
+
+
+
+ }
+ onClick={clearLogs}
+ >
+ Clear
+
+
+
+
+
+
+ {activityLogs.length === 0 ? (
+
+ No activity logged yet
+
+ ) : (
+
+ {activityLogs.map((log) => (
+
+ ))}
+
+ )}
+
+
+
+ );
+};
+
+export default ActivityLog;
\ No newline at end of file
diff --git a/client/src/components/ChatInterface.tsx b/client/src/components/ChatInterface.tsx
index 80c5a75..50875e5 100644
--- a/client/src/components/ChatInterface.tsx
+++ b/client/src/components/ChatInterface.tsx
@@ -8,18 +8,30 @@ import {
useTheme,
Icon,
Button,
+ Badge,
+ Tooltip,
+ Divider,
+ Image,
} from "@chakra-ui/react";
-import { FaRobot, FaUser, FaTools } from "react-icons/fa";
+import { FaRobot, FaUser, FaTools, FaRegLightbulb, FaNetworkWired } from "react-icons/fa";
import { useChatContext } from "../contexts/ChatContext";
+import { useSettingsContext } from "../contexts/SettingsContext";
import MessageInput from "./MessageInput";
import MessageContent from "./MessageContent";
import ToolCallDisplay from "./ToolCallDisplay";
+import ActivityLog from "./ActivityLog";
const ChatInterface: React.FC = () => {
const theme = useTheme();
const { messages, isLoading } = useChatContext();
+ const settingsContext = useSettingsContext();
const messagesEndRef = useRef(null);
+ // Log the session ID to help debug
+ useEffect(() => {
+ console.log("ChatInterface using session ID:", settingsContext.sessionId);
+ }, [settingsContext.sessionId]);
+
// Scroll to bottom when messages change
useEffect(() => {
if (messagesEndRef.current) {
@@ -27,161 +39,297 @@ const ChatInterface: React.FC = () => {
}
}, [messages]);
+ const getMessageGradient = (isUser: boolean) => {
+ return isUser
+ ? "linear-gradient(135deg, var(--chakra-colors-primary-500) 0%, var(--chakra-colors-primary-600) 100%)"
+ : "linear-gradient(135deg, var(--chakra-colors-dark-200) 0%, var(--chakra-colors-dark-300) 100%)";
+ };
+
return (
{/* Chat header */}
-
-
- Nanda Chat Interface
-
+
+
+
+ NANDA Chat
+
+
- {/* Messages container */}
-
- {messages.length === 0 ? (
-
-
-
- Welcome to Nanda Chat Interface
-
-
- This AI uses the Nanda Protocol to enhance capabilities with
- tools. Ask me anything, and I'll try to help by using my
- intelligence and other tools and knowledge.
-
-
-
- Try asking:
+
+ {/* Messages container */}
+
+ {messages.length === 0 ? (
+
+
+
+ Welcome to NANDA Chat
- {
- // Enter message to ask about available tools
+
+ This AI uses the NANDA Protocol to enhance capabilities with
+ tools. Ask me anything, and I'll try to help by using my
+ intelligence and other tools and knowledge.
+
+
+ ) : (
+ messages
+ .filter(message => !message.isIntermediate)
+ .map((message, index) => (
+
- "What tools do you have available?"
-
-
-
- ) : (
- messages.map((message, index) => (
+
+
+
+ {message.role === "user" ? (
+
+ ) : (
+
+ )}
+
+
+
+ {message.role === "user" ? "You" : "Assistant"}
+
+
+ {new Date(message.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
+
+
+
+ {/* Handle different content types */}
+
+
+ {/* Display tool calls if present */}
+ {message.role === "assistant" &&
+ message.toolCalls &&
+ message.toolCalls.length > 0 && (
+
+ {message.toolCalls.map((toolCall, idx) => (
+
+ ))}
+
+ )}
+
+ ))
+ )}
+
+ {/* Loading indicator */}
+ {isLoading && (
-
+
+
+ Assistant
+
+
+
+
+
-
- {" "}
- {message.role === "user" ? "You" : "Assistant"}
-
+
+ Thinking...
-
- {/* Handle different content types */}
-
-
- {/* Display tool calls if present */}
- {message.role === "assistant" &&
- message.toolCalls &&
- message.toolCalls.length > 0 && (
-
- {message.toolCalls.map((toolCall, idx) => (
-
- ))}
-
- )}
- ))
- )}
+ )}
- {/* Loading indicator */}
- {isLoading && (
-
-
-
- Assistant
-
- Thinking...
-
- )}
+ {/* Invisible element to scroll to */}
+
+
- {/* Invisible element to scroll to */}
-
-
+ {/* Activity Log Panel */}
+
+
+
+
{/* Input area */}
diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx
index a03c20c..066fa9a 100644
--- a/client/src/components/Header.tsx
+++ b/client/src/components/Header.tsx
@@ -1,81 +1,65 @@
// client/src/components/Header.tsx
import React from "react";
-import {
- Flex,
- Box,
- Heading,
- IconButton,
- useColorMode,
- Icon,
- Tooltip,
-} from "@chakra-ui/react";
-import { FaCog, FaMoon, FaSun } from "react-icons/fa";
+import { Box, Flex, IconButton, Heading, Icon, Image } from "@chakra-ui/react";
+import { FaCog, FaCode, FaNetworkWired } from "react-icons/fa";
interface HeaderProps {
onOpenSettings: () => void;
}
const Header: React.FC = ({ onOpenSettings }) => {
- const { colorMode, toggleColorMode } = useColorMode();
-
return (
-
-
-
-
-
- MCP Host
-
-
-
-
-
- }
- variant="ghost"
- colorScheme="whiteAlpha"
- fontSize="xl"
- mr={2}
- onClick={onOpenSettings}
- />
-
-
-
+
+
- : }
- variant="ghost"
- colorScheme="whiteAlpha"
- fontSize="xl"
- onClick={toggleColorMode}
- />
-
+ NANDA Host
+
+ }
+ onClick={onOpenSettings}
+ variant="ghost"
+ size="md"
+ color="whiteAlpha.800"
+ _hover={{
+ bg: "rgba(255, 255, 255, 0.1)",
+ transform: "rotate(30deg)",
+ }}
+ transition="all 0.3s ease"
+ />
);
};
diff --git a/client/src/components/MessageContent.tsx b/client/src/components/MessageContent.tsx
index c38609a..fb1942a 100644
--- a/client/src/components/MessageContent.tsx
+++ b/client/src/components/MessageContent.tsx
@@ -1,143 +1,173 @@
// client/src/components/MessageContent.tsx
import React from "react";
-import { Box, Text, Image, Flex, Icon } from "@chakra-ui/react";
-import { FaInfoCircle, FaTools } from "react-icons/fa";
-import ReactMarkdown from "react-markdown";
+import {
+ Box,
+ Text,
+ Code,
+ Image,
+ Flex,
+ Icon,
+ Divider,
+} from "@chakra-ui/react";
+import { FaTools, FaCode, FaImage, FaInfoCircle } from "react-icons/fa";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
-import { atomDark } from "react-syntax-highlighter/dist/esm/styles/prism";
-
-interface ContentItem {
- type: string;
- text?: string;
- id?: string;
- name?: string;
- input?: any;
- data?: string; // Added data property for image content type
-}
+import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
interface MessageContentProps {
- content: ContentItem | ContentItem[];
-}
-
-// Define types for ReactMarkdown components
-interface CodeProps {
- node?: any;
- inline?: boolean;
- className?: string;
- children?: React.ReactNode;
+ content: any;
}
const MessageContent: React.FC = ({ content }) => {
- // If content is not an array, convert it to array for consistent handling
- const contentArray = Array.isArray(content) ? content : [content];
+ // Debug log to help identify unhandled content types
+ console.log("MessageContent received:", content);
+
+ // If content is null or undefined, show a placeholder
+ if (content == null) {
+ return No content available ;
+ }
+
+ // If content is already a string, return it wrapped in a Text component
+ if (typeof content === "string") {
+ return {content} ;
+ }
+
+ // If content has a "text" property, return it wrapped in a Text component
+ if (content && typeof content.text === "string") {
+ return {content.text} ;
+ }
+
+ // Handle Claude's response format with type property at the top level
+ if (content && content.type === "text" && typeof content.text === "string") {
+ return {content.text} ;
+ }
+
+ // If content is an array, map through and display each item
+ if (Array.isArray(content)) {
+ // If it's an empty array, show a placeholder
+ if (content.length === 0) {
+ return No content available ;
+ }
+
+ return (
+
+ {content.map((item, index) => {
+ // Handle string items
+ if (typeof item === "string") {
+ return {item} ;
+ }
+
+ // Handle null or undefined items
+ if (item == null) {
+ return null;
+ }
+
+ // Handle object items
+ if (typeof item === "object") {
+ // Handle text type
+ if (item.type === "text" && item.text) {
+ return {item.text} ;
+ }
+ // Handle code type
+ else if (item.type === "code" && item.code) {
+ return (
+
+
+
+
+ {item.language || "Code"}
+
+
+
+ {item.code}
+
+
+ );
+ }
+
+ // For any object with text property but no type
+ else if (typeof item.text === 'string') {
+ return {item.text} ;
+ }
+
+ // For any other object, convert to string if possible
+ else if (item.toString && item.toString() !== '[object Object]') {
+ return {item.toString()} ;
+ }
+
+ // For remaining objects, show as JSON
+ else {
+ return (
+
+ {JSON.stringify(item, null, 2)}
+
+ );
+ }
+ }
+
+ // For primitive values (number, boolean)
+ return {String(item)} ;
+ })}
+
+ );
+ }
+
+ // Handle the Claude response format directly - if content has a role and content properties
+ if (content && content.role && content.content) {
+ // The content field could be an array or an object
+ return ;
+ }
+ // If we got here, it's an object that didn't match our known patterns
+ // Show it as JSON for debugging
return (
-
- {contentArray.map((item, index) => {
- if (item.type === "text" && item.text) {
- return (
-
- {
- const match = /language-(\w+)/.exec(className || "");
- return !inline && match ? (
-
- {String(children).replace(/\n$/, "")}
-
- ) : (
-
- {children}
-
- );
- },
- p({ children }) {
- return {children} ;
- },
- a({ node, children, ...props }) {
- return (
-
- {children}
-
- );
- },
- }}
- >
- {item.text}
-
-
- );
- } else if (item.type === "image") {
- return (
-
-
-
- );
- } else if (item.type === "tool_use") {
- return (
-
-
-
- Using tool: {item.name}
-
-
- {JSON.stringify(item.input, null, 2)}
-
-
- );
- } else {
- // Default case for unknown content types
- return (
-
-
-
-
- Unsupported content type: {item.type}
-
-
-
- );
- }
- })}
+
+
+ Content received in unknown format
+
+
+ {JSON.stringify(content, null, 2)}
+
);
};
diff --git a/client/src/components/MessageInput.tsx b/client/src/components/MessageInput.tsx
index 2a01d12..e388fd6 100644
--- a/client/src/components/MessageInput.tsx
+++ b/client/src/components/MessageInput.tsx
@@ -1,7 +1,15 @@
// client/src/components/MessageInput.tsx
import React, { useState, useRef } from "react";
-import { Flex, Textarea, Button, Icon, useToast, Box } from "@chakra-ui/react";
-import { FaPaperPlane, FaSpinner } from "react-icons/fa";
+import {
+ Flex,
+ Textarea,
+ Button,
+ Icon,
+ useToast,
+ Box,
+ Tooltip
+} from "@chakra-ui/react";
+import { FaPaperPlane, FaSpinner, FaMicrophone } from "react-icons/fa";
import { useChatContext } from "../contexts/ChatContext";
import { useSettingsContext } from "../contexts/SettingsContext";
@@ -23,6 +31,7 @@ const MessageInput: React.FC = () => {
duration: 5000,
isClosable: true,
position: "top",
+ variant: "solid",
});
return;
}
@@ -54,7 +63,17 @@ const MessageInput: React.FC = () => {
};
return (
-
+
+
);
};
diff --git a/client/src/components/SettingsModal.tsx b/client/src/components/SettingsModal.tsx
index f97b963..4429141 100644
--- a/client/src/components/SettingsModal.tsx
+++ b/client/src/components/SettingsModal.tsx
@@ -1,5 +1,5 @@
// client/src/components/SettingsModal.tsx
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useRef, useCallback } from "react";
import {
Modal,
ModalOverlay,
@@ -24,56 +24,453 @@ import {
Text,
Divider,
Flex,
- Switch,
+ Link,
useToast,
IconButton,
InputGroup,
InputRightElement,
+ Accordion,
+ AccordionItem,
+ AccordionButton,
+ AccordionPanel,
+ AccordionIcon,
Spinner,
+ Center,
+ Alert,
+ AlertIcon,
+ AlertTitle,
+ AlertDescription,
Badge,
+ HStack,
+ SimpleGrid,
+ Card,
+ CardHeader,
+ CardBody,
+ CardFooter,
+ InputLeftElement,
} from "@chakra-ui/react";
-import {
- FaEye,
- FaEyeSlash,
- FaPlus,
- FaSync,
- FaCheck,
- FaStar,
-} from "react-icons/fa";
+import { FaEye, FaEyeSlash, FaPlus, FaExternalLinkAlt, FaSync, FaTrash, FaSearch } from "react-icons/fa";
import { useSettingsContext } from "../contexts/SettingsContext";
+import { debounce } from "lodash";
+
+// Define the types locally instead of importing from a non-existent file
+interface CredentialRequirement {
+ id: string;
+ name: string;
+ description?: string;
+ acquisition?: {
+ url?: string;
+ instructions?: string;
+ };
+}
+
+interface ToolCredentialInfo {
+ toolName: string;
+ serverName: string;
+ serverId: string;
+ credentials: CredentialRequirement[];
+}
+
+// Define the ServerConfig interface for registry servers
+interface ServerConfig {
+ id: string;
+ name: string;
+ url: string;
+ description?: string;
+ types?: string[];
+ tags?: string[];
+ verified?: boolean;
+ rating?: number;
+}
interface SettingsModalProps {
isOpen: boolean;
onClose: () => void;
}
+// Component for a single tool credential form
+interface ToolCredentialFormProps {
+ tool: ToolCredentialInfo;
+ onSave: (
+ toolName: string,
+ serverId: string,
+ credentials: Record
+ ) => Promise;
+}
+
+const ToolCredentialForm: React.FC = ({
+ tool,
+ onSave
+}) => {
+ const [credentials, setCredentials] = useState>({});
+ const [showPasswords, setShowPasswords] = useState>({});
+ const [isSaving, setIsSaving] = useState(false);
+ const toast = useToast();
+
+ const handleInputChange = (id: string, value: string) => {
+ setCredentials((prev) => ({
+ ...prev,
+ [id]: value,
+ }));
+ };
+
+ const togglePasswordVisibility = (id: string) => {
+ setShowPasswords((prev) => ({
+ ...prev,
+ [id]: !prev[id],
+ }));
+ };
+
+ const handleSave = async () => {
+ // Check that all required fields are filled
+ const missingFields = tool.credentials
+ .map(cred => cred.id)
+ .filter(id => !credentials[id]);
+
+ if (missingFields.length > 0) {
+ toast({
+ title: "Missing credentials",
+ description: `Please fill in all required fields: ${missingFields.join(", ")}`,
+ status: "error",
+ duration: 3000,
+ isClosable: true,
+ });
+ return;
+ }
+
+ setIsSaving(true);
+ try {
+ const success = await onSave(tool.toolName, tool.serverId, credentials);
+
+ if (success) {
+ toast({
+ title: "Credentials saved",
+ description: `Credentials for ${tool.toolName} have been saved`,
+ status: "success",
+ duration: 3000,
+ isClosable: true,
+ });
+ } else {
+ throw new Error("Failed to save credentials");
+ }
+ } catch (error) {
+ toast({
+ title: "Error saving credentials",
+ description: error instanceof Error ? error.message : "Unknown error",
+ status: "error",
+ duration: 3000,
+ isClosable: true,
+ });
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ return (
+
+
+ {tool.toolName}
+
+
+ Server: {tool.serverName}
+
+
+
+ {tool.credentials.map((cred) => (
+
+ {cred.name || cred.id}
+
+ handleInputChange(cred.id, e.target.value)}
+ placeholder={`Enter ${cred.name || cred.id}`}
+ />
+
+ : }
+ size="sm"
+ variant="ghost"
+ onClick={() => togglePasswordVisibility(cred.id)}
+ />
+
+
+ {cred.description && (
+ {cred.description}
+ )}
+
+ ))}
+
+ {tool.credentials.some(cred => cred.acquisition?.url) && (
+
+
+ Where to get credentials:
+
+ {tool.credentials
+ .filter(cred => cred.acquisition?.url)
+ .map(cred => (
+
+
+ {cred.name} credentials
+
+
+
+ ))}
+
+ )}
+
+
+ Save Credentials
+
+
+
+ );
+};
+
+// New component for displaying a registry server card
+interface ServerCardProps {
+ server: ServerConfig;
+ onAdd: (server: ServerConfig) => void;
+ isAlreadyAdded: boolean;
+}
+
+const ServerCard: React.FC = ({ server, onAdd, isAlreadyAdded }) => {
+ return (
+
+
+
+ {server.name}
+ {server.verified && (
+
+ Verified
+
+ )}
+
+
+
+
+
+ {server.description || "No description provided"}
+
+
+
+ {server.types?.map((type: string, index: number) => (
+
+ {type}
+
+ ))}
+ {server.tags?.map((tag: string, index: number) => (
+
+ {tag}
+
+ ))}
+
+
+ {server.rating && (
+
+ Rating: {server.rating.toFixed(1)}/5.0
+
+ )}
+
+
+
+ onAdd(server)}
+ isDisabled={isAlreadyAdded}
+ width="full"
+ >
+ {isAlreadyAdded ? "Already Added" : "Add Server"}
+
+
+
+ );
+};
+
const SettingsModal: React.FC = ({ isOpen, onClose }) => {
+ const [tempApiKey, setTempApiKey] = useState("");
+ const [showApiKey, setShowApiKey] = useState(false);
+ const [newServer, setNewServer] = useState({
+ id: "",
+ name: "",
+ url: "",
+ });
+ const [toolsWithCredentials, setToolsWithCredentials] = useState([]);
+ const [isLoadingTools, setIsLoadingTools] = useState(false);
+ const [loadAttempts, setLoadAttempts] = useState(0);
+ const retryTimerRef = useRef(null);
+ const mountCount = useRef(0);
+
+ const [registryServers, setRegistryServers] = useState([]);
+ const [isLoadingRegistry, setIsLoadingRegistry] = useState(false);
+ const [searchQuery, setSearchQuery] = useState("");
+
const {
apiKey,
setApiKey,
nandaServers,
registerNandaServer,
+ removeNandaServer,
refreshRegistry,
+ getToolsWithCredentialRequirements,
+ setToolCredentials: saveToolCredentials,
} = useSettingsContext();
- const [tempApiKey, setTempApiKey] = useState("");
- const [showApiKey, setShowApiKey] = useState(false);
- const [newServer, setNewServer] = useState({
- id: "",
- name: "",
- url: "",
- });
- const [registryServers, setRegistryServers] = useState([]);
- const [isRefreshing, setIsRefreshing] = useState(false);
+
const toast = useToast();
+
+ // Increment mount count on component mount
+ useEffect(() => {
+ mountCount.current += 1;
+ console.log(`Settings modal mounted ${mountCount.current} times`);
+
+ return () => {
+ if (retryTimerRef.current) {
+ clearTimeout(retryTimerRef.current);
+ }
+ };
+ }, []);
// Reset temp values when modal opens
useEffect(() => {
if (isOpen) {
setTempApiKey(apiKey || "");
setShowApiKey(false);
+ loadToolsWithCredentials();
+ } else {
+ // Cancel loading and clear timer if modal closes
+ if (retryTimerRef.current) {
+ clearTimeout(retryTimerRef.current);
+ retryTimerRef.current = null;
+ }
+ setIsLoadingTools(false);
}
}, [isOpen, apiKey]);
+ // Reload tools when servers change
+ useEffect(() => {
+ if (isOpen && nandaServers.length > 0) {
+ // Reset load attempts when server list changes
+ setLoadAttempts(0);
+ loadToolsWithCredentials();
+ }
+ }, [nandaServers, isOpen]);
+
+ // Add debug info to assist with troubleshooting
+ const debugToolCredentials = () => {
+ if (toolsWithCredentials.length > 0) {
+ const serverIds = Array.from(new Set(toolsWithCredentials.map(tool => tool.serverId)));
+ console.log(`Found tools from ${serverIds.length} servers:`, serverIds);
+
+ serverIds.forEach(serverId => {
+ const serverTools = toolsWithCredentials.filter(tool => tool.serverId === serverId);
+ console.log(`Server ${serverId} has ${serverTools.length} tools:`,
+ serverTools.map(t => t.toolName));
+ });
+ } else {
+ console.log("No tools with credentials found");
+ }
+ };
+
+ const loadToolsWithCredentials = async () => {
+ // Re-enabled credential functionality now that backend is fixed
+ // Clear any pending retries
+ if (retryTimerRef.current) {
+ clearTimeout(retryTimerRef.current);
+ retryTimerRef.current = null;
+ }
+
+ // Avoid redundant requests when already loading
+ if (isLoadingTools) {
+ console.log("Already loading tools, skipping duplicate request");
+ return;
+ }
+
+ setIsLoadingTools(true);
+
+ try {
+ console.log("Fetching tools with credential requirements...");
+ const tools = await getToolsWithCredentialRequirements();
+
+ if (tools.length > 0) {
+ console.log("Found tools with credentials:", tools);
+ setToolsWithCredentials(tools);
+ // Debug information to help troubleshoot
+ setTimeout(debugToolCredentials, 100);
+ setLoadAttempts(0); // Reset load attempts on success
+ setIsLoadingTools(false);
+ } else if (loadAttempts < 2 && nandaServers.length > 0) { // Reduced from 3 to 2 attempts
+ // If no tools found but servers exist, try again after delay - but only once
+ console.log(`No tools found on attempt ${loadAttempts + 1}, retrying...`);
+ setLoadAttempts(prev => prev + 1);
+
+ // Use reference to store timeout ID
+ retryTimerRef.current = setTimeout(() => {
+ // Don't recursively call the function - this creates multiple connections
+ // Instead, just reset the loading state and increment the attempt counter
+ setIsLoadingTools(false);
+ // Only schedule another attempt if we haven't mounted too many times
+ if (mountCount.current < 3) {
+ console.log(`Scheduling retry attempt ${loadAttempts + 1}`);
+ loadToolsWithCredentials();
+ } else {
+ console.log("Too many mount attempts, abandoning tool loading");
+ }
+ }, 5000); // Increased from 2000ms to 5000ms to reduce frequency
+ } else {
+ // If max attempts reached or no servers, just set empty array
+ console.log("Max retry attempts reached or no servers configured");
+ setToolsWithCredentials([]);
+ setIsLoadingTools(false);
+ }
+ } catch (error) {
+ console.error("Failed to load tools with credential requirements:", error);
+ toast({
+ title: "Error",
+ description: "Failed to load tools that require credentials",
+ status: "error",
+ duration: 3000,
+ isClosable: true,
+ });
+ setToolsWithCredentials([]);
+ setIsLoadingTools(false);
+ }
+
+ // Safety mechanism: ensure loading state is reset after 10 seconds at most
+ setTimeout(() => {
+ if (isLoadingTools) {
+ console.log("Safety timeout triggered - forcing loading state to false");
+ setIsLoadingTools(false);
+ }
+ }, 10000);
+ };
+
const handleSaveApiKey = () => {
setApiKey(tempApiKey);
toast({
@@ -81,7 +478,6 @@ const SettingsModal: React.FC = ({ isOpen, onClose }) => {
status: "success",
duration: 3000,
isClosable: true,
- position: "top",
});
};
@@ -89,7 +485,7 @@ const SettingsModal: React.FC = ({ isOpen, onClose }) => {
setShowApiKey(!showApiKey);
};
- const handleAddServer = () => {
+ const handleAddServer = async () => {
// Validate server info
if (!newServer.id || !newServer.name || !newServer.url) {
toast({
@@ -98,117 +494,299 @@ const SettingsModal: React.FC = ({ isOpen, onClose }) => {
status: "error",
duration: 3000,
isClosable: true,
- position: "top",
});
return;
}
- // Validate URL
- try {
- new URL(newServer.url);
- } catch (error) {
+ // Validate URL format
+ 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;
+ }
+
+ // 🌐 Send request to backend to actually register the server
+ try {
+ const res = await fetch("/api/servers", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ id: newServer.id,
+ name: newServer.name,
+ url: newServer.url,
+ }),
+ });
+
+ const data = await res.json();
+
+ if (res.ok && data.success) {
+ // Register new server
+ registerNandaServer({
+ id: newServer.id,
+ name: newServer.name,
+ url: newServer.url,
+ });
+
+ setNewServer({ id: "", name: "", url: "" });
+
toast({
- title: "Invalid URL",
- description:
- "Please enter a valid URL (e.g., http://localhost:3001/sse)",
- status: "error",
+ title: "Server Added",
+ description: `Server "${newServer.name}" was successfully registered.`,
+ status: "success",
duration: 3000,
isClosable: true,
+ });
+ } else {
+ // Backend responded with failure
+ toast({
+ title: "Server Registration Failed",
+ description: data.message || "Failed to register server.",
+ status: "error",
+ duration: 4000,
+ isClosable: true,
position: "top",
});
- return;
}
-
- // Register new server
- registerNandaServer({
- id: newServer.id,
- name: newServer.name,
- url: newServer.url,
- });
-
- // Reset form
- setNewServer({
- id: "",
- name: "",
- url: "",
+ } catch (error) {
+ console.error("Error adding server:", error);
+ toast({
+ title: "Network Error",
+ description: "Could not connect to server. Check the URL.",
+ status: "error",
+ duration: 4000,
+ isClosable: true,
});
+ }
+};
+ // Handle removing a server
+ const handleRemoveServer = (serverId: string) => {
+ // Remove the server using the context function
+ removeNandaServer(serverId);
+
+ // Show success message
toast({
- title: "Server Added",
- description: `Server "${newServer.name}" has been added`,
+ title: "Server Removed",
+ description: "The server has been removed successfully",
status: "success",
duration: 3000,
isClosable: true,
- position: "top",
});
+
+ // Optionally, refresh tools to update credentials UI
+ loadToolsWithCredentials();
};
- const handleRefreshRegistry = async () => {
- setIsRefreshing(true);
+ // Add a function to load registry servers
+ const loadRegistryServers = async () => {
+ setIsLoadingRegistry(true);
try {
- const result = await refreshRegistry();
- setRegistryServers(result.servers || []);
-
- toast({
- title: "Registry Refreshed",
- description: `Found ${result.servers.length} servers from the registry`,
- status: "success",
- duration: 3000,
- isClosable: true,
- position: "top",
- });
+ // If there's a search query, pass it to refreshRegistry
+ const result = await refreshRegistry(searchQuery);
+ if (result && result.servers) {
+ setRegistryServers(result.servers);
+
+ // Only show green success notification for popular servers (not for search)
+ if (!searchQuery) {
+ toast({
+ title: "Registry servers loaded",
+ description: result.message || `Found ${result.servers.length} servers in the registry`,
+ status: "success",
+ duration: 3000,
+ isClosable: true,
+ });
+ } else {
+ // For search results, just update UI without success toast
+ console.log(`Found ${result.servers.length} servers matching "${searchQuery}"`);
+ }
+ }
} catch (error) {
toast({
- title: "Refresh Failed",
- description:
- error instanceof Error
- ? error.message
- : "Failed to refresh registry servers",
+ title: "Error loading registry servers",
+ description: error instanceof Error ? error.message : "Failed to load registry servers",
status: "error",
duration: 3000,
isClosable: true,
- position: "top",
});
} finally {
- setIsRefreshing(false);
+ setIsLoadingRegistry(false);
}
};
- const handleAddRegistryServer = (server: any) => {
- registerNandaServer({
- id: server.id,
- name: server.name,
- url: server.url,
- });
+ // Debounced search handler
+ const debouncedSearchHandler = useCallback(
+ debounce(() => {
+ if (searchQuery && searchQuery.length >= 2) {
+ loadRegistryServers();
+ }
+ }, 500),
+ [searchQuery]
+ );
+
+ // Effect to trigger search when query changes
+ useEffect(() => {
+ if (searchQuery && searchQuery.length >= 2) {
+ debouncedSearchHandler();
+ }
+ }, [searchQuery, debouncedSearchHandler]);
+
+ // Check if a server is already added
+ const isServerAdded = (serverId: string) => {
+ return nandaServers.some(server => server.id === serverId);
+ };
+ // Add server from registry
+ const handleAddRegistryServer = (server: ServerConfig) => {
+ if (isServerAdded(server.id)) return;
+
+ // Make sure the URL has /sse at the end if not already
+ let url = server.url;
+ if (!url.endsWith("/sse")) {
+ url = url.endsWith("/") ? `${url}sse` : `${url}/sse`;
+ }
+
+ const serverToAdd = {
+ ...server,
+ url
+ };
+
+ registerNandaServer(serverToAdd);
+
toast({
- title: "Server Added",
- description: `Registry server "${server.name}" has been added`,
+ title: "Server added",
+ description: `${server.name} has been added to your servers`,
status: "success",
duration: 3000,
isClosable: true,
- position: "top",
});
};
- // Check if a registry server is already registered
- const isServerRegistered = (serverId: string) => {
- return nandaServers.some((server) => server.id === serverId);
- };
+ // Filter registry servers based on search query
+ const filteredRegistryServers = registryServers.filter(server => {
+ if (!searchQuery) return true;
+
+ const query = searchQuery.toLowerCase();
+ return (
+ server.name.toLowerCase().includes(query) ||
+ server.description?.toLowerCase().includes(query) ||
+ server.tags?.some((tag: string) => tag.toLowerCase().includes(query)) ||
+ server.types?.some((type: string) => type.toLowerCase().includes(query))
+ );
+ });
return (
-
-
- Settings
-
+
+
+
+ Settings
+
+
+
+
-
-
- API
- Nanda Servers
- Registry
- About
+
+
+
+ API
+
+
+ Added Servers
+
+
+ Tool Credentials
+
+
+ Registry
+
+
+ About
+
@@ -216,7 +794,7 @@ const SettingsModal: React.FC = ({ isOpen, onClose }) => {
- Anthropic API Key
+ Anthropic API Key
= ({ isOpen, onClose }) => {
onChange={(e) => setTempApiKey(e.target.value)}
placeholder="sk-ant-api03-..."
autoComplete="off"
+ variant="filled"
+ bg="rgba(0, 0, 0, 0.2)"
+ borderColor="rgba(255, 255, 255, 0.1)"
+ _hover={{
+ borderColor: "primary.400",
+ }}
+ _focus={{
+ borderColor: "primary.500",
+ bg: "rgba(0, 0, 0, 0.3)",
+ }}
/>
= ({ isOpen, onClose }) => {
size="sm"
variant="ghost"
onClick={toggleShowApiKey}
+ color="whiteAlpha.700"
+ _hover={{ color: "white", bg: "rgba(255, 255, 255, 0.1)" }}
/>
-
+
You can get your API key from the{" "}
-
Anthropic Console
-
+
-
+
Save API Key
- {/* Nanda Servers Tab */}
+ {/* Added Servers Tab */}
-
- Registered Nanda Servers
+
+ Registered Added Servers
{nandaServers.length === 0 ? (
- No servers registered yet
+ No servers registered yet
) : (
nandaServers.map((server) => (
- {server.name}
-
+
+
+ {server.name}
+
ID: {server.id}
-
+
URL: {server.url}
+
+ }
+ size="sm"
+ colorScheme="red"
+ variant="ghost"
+ onClick={() => handleRemoveServer(server.id)}
+ />
+
))
)}
-
+
-
+
Add New Server
- Server ID
+ Server ID
@@ -350,136 +968,205 @@ const SettingsModal: React.FC = ({ isOpen, onClose }) => {
- {/* Registry Tab */}
+ {/* Tool Credentials Tab */}
-
-
+
+ Tool API Credentials
+
+ }
+ onClick={loadToolsWithCredentials}
+ isLoading={isLoadingTools}
+ >
+ Refresh
+
+
+
+ Some tools require API keys or other credentials to function.
+ Configure them here.
+
+
+ {isLoadingTools ? (
+
+
+
+
+ {loadAttempts > 0
+ ? `Loading tools (attempt ${loadAttempts+1}/4)...`
+ : "Loading tools..."}
+
+
+
+ ) : toolsWithCredentials.length === 0 ? (
+
+
+
+ No tools found
+
+
+ {nandaServers.length === 0 ? (
+ <>
+ You need to register a Nanda server before tools will appear.
+ Go to the "Added Servers" tab to add a server.
+ >
+ ) : (
+ <>
+ No tools requiring credentials were found.
+ Try refreshing or check your server configuration.
+ >
+ )}
+
+
+ ) : (
+ <>
+ {/* Group tools by server for better organization */}
+ {(() => {
+ // Create a map of serverId -> tools
+ const serverMap: Record = {};
+
+ // Group tools by server
+ toolsWithCredentials.forEach(tool => {
+ if (!serverMap[tool.serverId]) {
+ serverMap[tool.serverId] = [];
+ }
+ serverMap[tool.serverId].push(tool);
+ });
+
+ // Render each server group
+ return Object.entries(serverMap).map(([serverId, serverTools]) => {
+ const serverName = serverTools[0]?.serverName || serverId;
+
+ return (
+
+
+ Server: {serverName}
+
+
+ {serverTools.map((tool) => (
+
+ ))}
+
+
+ );
+ });
+ })()}
+ >
+ )}
+
+
+
+ {/* Registry Tab Panel */}
+
+
+
+ Global Nanda Registry
+ }
+ colorScheme="primary"
+ size="sm"
+ onClick={loadRegistryServers}
+ isLoading={isLoadingRegistry}
>
- MCP Registry Servers
+ Refresh Servers
+
+
+
+
+ Browse and add servers from the global Nanda Registry.
+ Added servers will be stored locally in your browser.
+
+
+ {/* Search input */}
+
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+ {isLoadingRegistry ? (
+
+
+
+
+ Loading servers from registry...
+
+
+
+ ) : registryServers.length === 0 ? (
+
+
+ No servers loaded from the registry yet.
+
:
- }
- colorScheme="crimson"
- size="sm"
- onClick={handleRefreshRegistry}
- isLoading={isRefreshing}
+ colorScheme="primary"
+ onClick={loadRegistryServers}
>
- Refresh Registry
+ Load Registry Servers
-
-
- {!isRefreshing && registryServers.length === 0 ? (
-
- No registry servers loaded. Click the refresh button to
- load servers from the registry.
-
- ) : (
- <>
- {isRefreshing ? (
-
-
-
- ) : (
- registryServers.map((server) => (
-
-
-
- {server.name}
- {server.verified && (
-
- Verified
-
- )}
-
- {server.rating && (
-
-
-
- {server.rating.toFixed(1)}
-
-
- )}
-
-
-
- ID: {server.id}
-
-
- URL: {server.url}
-
-
- {server.description && (
-
- {server.description}
-
- )}
-
- {server.tags && server.tags.length > 0 && (
-
- {server.tags.map(
- (tag: string, index: number) => (
-
- {tag}
-
- )
- )}
-
- )}
-
-
-
- ) : (
-
- )
- }
- isDisabled={isServerRegistered(server.id)}
- onClick={() =>
- handleAddRegistryServer(server)
- }
- >
- {isServerRegistered(server.id)
- ? "Added"
- : "Add Server"}
-
-
-
- ))
- )}
- >
- )}
-
+
+ ) : filteredRegistryServers.length === 0 ? (
+
+
+ No matches found
+
+ No servers match your search query. Try a different search.
+
+
+ ) : (
+
+ {filteredRegistryServers.map((server: ServerConfig) => (
+
+ ))}
+
+ )}
diff --git a/client/src/components/ToolCallDisplay.tsx b/client/src/components/ToolCallDisplay.tsx
index fea3076..84185d9 100644
--- a/client/src/components/ToolCallDisplay.tsx
+++ b/client/src/components/ToolCallDisplay.tsx
@@ -3,8 +3,8 @@ import React from "react";
import {
Box,
Text,
- Flex,
Icon,
+ Flex,
Badge,
Accordion,
AccordionItem,
@@ -12,13 +12,15 @@ import {
AccordionPanel,
AccordionIcon,
} from "@chakra-ui/react";
-import { FaTools, FaCheck, FaExclamationTriangle } from "react-icons/fa";
+import { FaTools, FaCheckCircle, FaTimesCircle } from "react-icons/fa";
interface ToolCallProps {
toolCall: {
id: string;
name: string;
input: any;
+ serverId?: string;
+ serverName?: string;
result?: {
content: any[];
isError?: boolean;
@@ -27,67 +29,102 @@ interface ToolCallProps {
}
const ToolCallDisplay: React.FC = ({ toolCall }) => {
- const { name, input, result } = toolCall;
-
+ const { name, input, result, serverId, serverName } = toolCall;
const hasResult = !!result;
- const isError = result?.isError || false;
+ const isError = result?.isError;
return (
-
-
-
-
-
-
- {name}
-
-
+
+
+
+
- {!hasResult ? (
- "Pending"
- ) : isError ? (
- <>
-
- Error
- >
- ) : (
- <>
-
- Success
- >
+
+
+
+
+
+ Using tool: {name}
+
+ {hasResult && (
+
+
+ {isError ? "Failed" : "Success"}
+
+ )}
+
+ {!hasResult && (
+
+ Click to see details
+
)}
-
+
-
+
{/* Input */}
-
-
- Input:
+
+
+ Input Parameters:
{JSON.stringify(input, null, 2)}
@@ -97,29 +134,61 @@ const ToolCallDisplay: React.FC = ({ toolCall }) => {
{hasResult && (
Result:
{result.content.map((item, idx) => (
-
+
{item.type === "text" ? item.text : JSON.stringify(item)}
))}
)}
+
+ {/* Server info if available */}
+ {serverId && serverName && (
+
+
+ Server: {serverName} ({serverId.substring(0, 8)}...)
+
+
+ )}
diff --git a/client/src/contexts/ChatContext.tsx b/client/src/contexts/ChatContext.tsx
index 5abb7d8..86b0c61 100644
--- a/client/src/contexts/ChatContext.tsx
+++ b/client/src/contexts/ChatContext.tsx
@@ -5,9 +5,22 @@ import React, {
useState,
useCallback,
useRef,
+ useEffect,
} from "react";
import { useSettingsContext } from "./SettingsContext";
import { v4 as uuidv4 } from "uuid";
+import { socketService } from "../services/socketService";
+
+// Fix for undefined environment variables
+const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || "";
+
+export interface LogEntry {
+ id: string;
+ timestamp: Date;
+ type: 'server-selection' | 'tool-execution' | 'error' | 'info';
+ message: string;
+ details?: any;
+}
export interface Message {
id: string;
@@ -22,21 +35,30 @@ export interface Message {
content: any[];
isError?: boolean;
};
+ serverId?: string;
+ serverName?: string;
}>;
+ isIntermediate?: boolean;
}
interface ChatContextProps {
messages: Message[];
isLoading: boolean;
+ activityLogs: LogEntry[];
sendMessage: (content: string) => Promise;
clearMessages: () => void;
+ clearLogs: () => void;
+ addLogEntry: (type: LogEntry['type'], message: string, details?: any) => void;
}
const ChatContext = createContext({
messages: [],
isLoading: false,
+ activityLogs: [],
sendMessage: async () => {},
clearMessages: () => {},
+ clearLogs: () => {},
+ addLogEntry: () => {},
});
export const useChatContext = () => useContext(ChatContext);
@@ -47,42 +69,178 @@ interface ChatProviderProps {
export const ChatProvider: React.FC = ({ children }) => {
const [messages, setMessages] = useState([]);
+ const [activityLogs, setActivityLogs] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const { apiKey } = useSettingsContext();
- const sessionId = useRef(uuidv4());
+ const settingsContext = useSettingsContext();
+ const fallbackSessionId = useRef(uuidv4());
+
+ // Get the session ID - use from settings context if available, fallback if not
+ const getSessionId = useCallback(() => {
+ return settingsContext.sessionId || fallbackSessionId.current;
+ }, [settingsContext.sessionId]);
+
+ // Add log entry
+ const addLogEntry = useCallback((type: LogEntry['type'], message: string, details?: any) => {
+ const newEntry: LogEntry = {
+ id: uuidv4(),
+ timestamp: new Date(),
+ type,
+ message,
+ details,
+ };
+
+ setActivityLogs(prevLogs => [...prevLogs, newEntry]);
+ }, []);
+
+ // Set up socket.io handler for tool execution updates
+ useEffect(() => {
+ const handleToolExecution = (data: {
+ toolName: string;
+ serverId: string;
+ serverName: string;
+ result: {
+ content: any[];
+ isError?: boolean;
+ }
+ }) => {
+ console.log('Tool execution handler called with data:', data);
+ const { toolName, serverId, serverName, result } = data;
+
+ // Update message with tool call result
+ setMessages(prevMessages => {
+ console.log('Current messages:', prevMessages);
+ // Find the message with the matching tool call
+ const updatedMessages = prevMessages.map(message => {
+ // Only update assistant messages with tool calls
+ if (message.role === 'assistant' && message.toolCalls) {
+ // Find the tool call that matches this execution
+ const hasMatchingTool = message.toolCalls.some(tc => tc.name === toolName && !tc.result);
+
+ if (hasMatchingTool) {
+ console.log(`Found message with matching tool: ${toolName}`, message);
+ // Update the tool calls with results
+ const updatedToolCalls = message.toolCalls.map(tc => {
+ if (tc.name === toolName && !tc.result) {
+ console.log(`Updating tool call with result: ${toolName}`, result);
+ // Update this tool call with the result
+ return {
+ ...tc,
+ result: {
+ content: result.content || [],
+ isError: result.isError || false
+ },
+ serverId,
+ serverName
+ };
+ }
+ return tc;
+ });
+
+ return {
+ ...message,
+ toolCalls: updatedToolCalls
+ };
+ }
+ }
+ return message;
+ });
+
+ console.log('Updated messages:', updatedMessages);
+ return updatedMessages;
+ });
+
+ addLogEntry('tool-execution', `Tool executed: ${toolName}`, {
+ serverName,
+ serverId,
+ resultSummary: result.content ? JSON.stringify(result.content).substring(0, 100) + '...' : 'No content'
+ });
+ };
+
+ console.log('Setting up tool execution handler');
+ // Register handler
+ socketService.addToolExecutionHandler(handleToolExecution);
+
+ return () => {
+ console.log('Cleaning up tool execution handler');
+ // Clean up
+ socketService.removeToolExecutionHandler(handleToolExecution);
+ };
+ }, [addLogEntry]);
// Process assistant response to extract tool calls
const processAssistantResponse = useCallback((response: any) => {
+ console.log("Raw response from API:", response);
+
const toolCalls = [];
// Check for tool_use items in the content array
if (response.content && Array.isArray(response.content)) {
for (const item of response.content) {
if (item.type === "tool_use") {
+ // Get server info from cache if available
+ const serverInfo = socketService.getServerInfo(item.name);
+
toolCalls.push({
id: item.id,
name: item.name,
input: item.input,
+ serverId: serverInfo?.serverId,
+ serverName: serverInfo?.serverName,
+ result: item.result
+ });
+
+ // Log tool usage
+ addLogEntry('tool-execution', `Tool selected: ${item.name}`, {
+ toolId: item.id,
+ toolName: item.name,
+ inputSummary: JSON.stringify(item.input).substring(0, 100) + (JSON.stringify(item.input).length > 100 ? '...' : '')
});
}
}
}
+ // Log MCP server information if available
+ if (response.serverInfo) {
+ addLogEntry('server-selection', `MCP Server: ${response.serverInfo.id || 'Unknown'}`, response.serverInfo);
+ }
+
+ // If the content isn't already an array, convert it to one
+ const processedContent = Array.isArray(response.content)
+ ? response.content
+ : [{ type: "text", text: typeof response.content === "string"
+ ? response.content
+ : "Response received in unexpected format" }];
+
return {
- content: response.content,
+ content: processedContent,
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
};
- }, []);
+ }, [addLogEntry]);
// Send a message to the assistant
const sendMessage = useCallback(
async (messageText: string) => {
if (!apiKey) {
console.error("API key not set");
+ // Add error message about missing API key
+ const errorMessage: Message = {
+ id: uuidv4(),
+ role: "assistant",
+ content: {
+ type: "text",
+ text: "Error: Please set your Anthropic API key in the settings (gear icon) before sending messages."
+ },
+ timestamp: new Date(),
+ };
+
+ addLogEntry('error', 'Missing API key', { requiredSetting: 'API Key' });
+ setMessages((prevMessages) => [...prevMessages, errorMessage]);
return;
}
setIsLoading(true);
+ addLogEntry('info', 'Processing message', { messageText: messageText.substring(0, 50) + (messageText.length > 50 ? '...' : '') });
try {
// Add user message
@@ -101,17 +259,21 @@ export const ChatProvider: React.FC = ({ children }) => {
content: Array.isArray(msg.content) ? msg.content : [msg.content],
}));
+ console.log(`Sending message to ${API_BASE_URL}/api/chat/completions with session ID: ${getSessionId()}`);
+ addLogEntry('info', 'Sending request to API', {
+ endpoint: `${API_BASE_URL}/api/chat/completions`,
+ sessionId: getSessionId()
+ });
+
// Send request to our backend
const response = await fetch(
- `${
- process.env.REACT_APP_API_BASE_URL || "http://localhost:3000"
- }/api/chat/completions`,
+ `${API_BASE_URL}/api/chat/completions`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": apiKey,
- "X-Session-Id": sessionId.current,
+ "X-Session-Id": getSessionId() || "",
},
body: JSON.stringify({
messages: apiMessages,
@@ -120,15 +282,51 @@ export const ChatProvider: React.FC = ({ children }) => {
}
);
+ // Check the content type before trying to parse JSON
+ const contentType = response.headers.get("content-type");
+ if (!contentType || !contentType.includes("application/json")) {
+ // Handle non-JSON response (like HTML error pages)
+ const text = await response.text();
+ console.error("Received non-JSON response:", text.substring(0, 200) + "...");
+ addLogEntry('error', 'Invalid response format', {
+ contentType,
+ statusCode: response.status,
+ previewText: text.substring(0, 100) + '...'
+ });
+ throw new Error(`Invalid response from server: Not JSON (${response.status} ${response.statusText})`);
+ }
+
if (!response.ok) {
const errorData = await response.json();
- throw new Error(errorData.error || "Failed to send message");
+ addLogEntry('error', `Server error: ${response.status}`, errorData);
+ throw new Error(errorData.error || `Server error: ${response.status} ${response.statusText}`);
}
const responseData = await response.json();
+ addLogEntry('info', 'Response received', {
+ statusCode: response.status,
+ hasTools: responseData.toolsUsed || responseData.content?.some((item: any) => item.type === 'tool_use') || false,
+ serverInfo: responseData.serverInfo || 'Not provided'
+ });
+
// Process the response to extract tool calls
const { content, toolCalls } = processAssistantResponse(responseData);
+ // Process intermediate responses first
+ if (responseData.intermediateResponses && Array.isArray(responseData.intermediateResponses)) {
+ for (const intermediateResponse of responseData.intermediateResponses) {
+ const { content: intermediateContent } = processAssistantResponse(intermediateResponse);
+ const intermediateMessage: Message = {
+ id: uuidv4(),
+ role: "assistant",
+ content: intermediateContent,
+ timestamp: new Date(intermediateResponse.timestamp || Date.now()),
+ isIntermediate: true
+ };
+ setMessages((prevMessages) => [...prevMessages, intermediateMessage]);
+ }
+ }
+
// Add assistant message
const assistantMessage: Message = {
id: uuidv4(),
@@ -141,6 +339,7 @@ export const ChatProvider: React.FC = ({ children }) => {
setMessages((prevMessages) => [...prevMessages, assistantMessage]);
} catch (error) {
console.error("Error sending message:", error);
+ addLogEntry('error', `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, { error });
// Add error message
const errorMessage: Message = {
@@ -162,7 +361,7 @@ export const ChatProvider: React.FC = ({ children }) => {
setIsLoading(false);
}
},
- [apiKey, messages, processAssistantResponse]
+ [apiKey, messages, processAssistantResponse, getSessionId, addLogEntry]
);
// Clear all messages
@@ -170,13 +369,21 @@ export const ChatProvider: React.FC = ({ children }) => {
setMessages([]);
}, []);
+ // Clear all logs
+ const clearLogs = useCallback(() => {
+ setActivityLogs([]);
+ }, []);
+
return (
{children}
diff --git a/client/src/contexts/SettingsContext.tsx b/client/src/contexts/SettingsContext.tsx
index 0c3334d..f4a7b7d 100644
--- a/client/src/contexts/SettingsContext.tsx
+++ b/client/src/contexts/SettingsContext.tsx
@@ -5,26 +5,60 @@ import React, {
useState,
useEffect,
useCallback,
+ useRef,
} from "react";
+import { v4 as uuidv4 } from "uuid";
+// Define the types locally instead of importing from a non-existent file
interface ServerConfig {
id: string;
name: string;
url: string;
+}
+
+interface CredentialRequirement {
+ id: string;
+ name: string;
description?: string;
- types?: string[];
- tags?: string[];
- verified?: boolean;
- rating?: number;
+ acquisition?: {
+ url?: string;
+ instructions?: string;
+ };
+}
+
+interface ToolCredentialInfo {
+ toolName: string;
+ serverName: string;
+ serverId: string;
+ credentials: CredentialRequirement[];
+}
+
+interface ToolCredentialRequest {
+ toolName: string;
+ serverId: string;
+ credentials: Record;
}
+// Fix for undefined environment variables
+const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || "";
+const STORAGE_KEY_API_KEY = "nanda-host-api-key";
+const STORAGE_KEY_SERVERS = "nanda-mcp-servers"; // Key for storing servers in localStorage
+const STORAGE_KEY_SESSION_ID = "nanda-session-id"; // Key for storing session ID
+
interface SettingsContextProps {
apiKey: string | null;
setApiKey: (key: string) => void;
nandaServers: ServerConfig[];
registerNandaServer: (server: ServerConfig) => void;
removeNandaServer: (id: string) => void;
- refreshRegistry: () => Promise<{ servers: ServerConfig[] }>;
+ refreshRegistry: (searchQuery?: string) => Promise;
+ getToolsWithCredentialRequirements: () => Promise;
+ setToolCredentials: (
+ toolName: string,
+ serverId: string,
+ credentials: Record
+ ) => Promise;
+ sessionId: string | null;
}
const SettingsContext = createContext({
@@ -34,137 +68,267 @@ const SettingsContext = createContext({
registerNandaServer: () => {},
removeNandaServer: () => {},
refreshRegistry: async () => ({ servers: [] }),
+ getToolsWithCredentialRequirements: async () => [],
+ setToolCredentials: async () => false,
+ sessionId: null,
});
export const useSettingsContext = () => useContext(SettingsContext);
-// Local storage keys
-const API_KEY_STORAGE_KEY = "nanda_host_api_key";
-const NANDA_SERVERS_STORAGE_KEY = "nanda_host_servers";
-
interface SettingsProviderProps {
children: React.ReactNode;
}
export const SettingsProvider: React.FC = ({
children,
-}) => {
- const [apiKey, setApiKeyState] = useState(null);
+}: SettingsProviderProps) => {
+ const [apiKey, setInternalApiKey] = useState(null);
const [nandaServers, setNandaServers] = useState([]);
+ const [sessionId, setSessionId] = useState(null);
+ const fallbackSessionId = useRef(uuidv4());
- // Load settings from local storage on mount
+ // Initialize API key from localStorage
useEffect(() => {
- // Load API key
- const storedApiKey = localStorage.getItem(API_KEY_STORAGE_KEY);
- if (storedApiKey) {
- setApiKeyState(storedApiKey);
+ const storedKey = localStorage.getItem(STORAGE_KEY_API_KEY);
+ if (storedKey) {
+ setInternalApiKey(storedKey);
}
-
- // Load Nanda servers
- const storedServers = localStorage.getItem(NANDA_SERVERS_STORAGE_KEY);
+
+ // Initialize MCP servers from localStorage
+ const storedServers = localStorage.getItem(STORAGE_KEY_SERVERS);
if (storedServers) {
try {
const parsedServers = JSON.parse(storedServers);
- if (Array.isArray(parsedServers)) {
- setNandaServers(parsedServers);
- }
+ setNandaServers(parsedServers);
} catch (error) {
- console.error("Failed to parse stored Nanda servers:", error);
+ console.error("Error parsing stored servers:", error);
+ // If parsing fails, initialize with empty array
+ setNandaServers([]);
}
}
}, []);
- // Save API key to local storage
+ // Set API key and store in localStorage
const setApiKey = useCallback((key: string) => {
- setApiKeyState(key);
- localStorage.setItem(API_KEY_STORAGE_KEY, key);
+ setInternalApiKey(key);
+ localStorage.setItem(STORAGE_KEY_API_KEY, key);
}, []);
+ // Initialize session
+ useEffect(() => {
+ const initSession = async () => {
+ try {
+ // Check for stored session ID
+ const storedSessionId = localStorage.getItem(STORAGE_KEY_SESSION_ID);
+
+ if (storedSessionId) {
+ console.log("Using stored session ID:", storedSessionId);
+ setSessionId(storedSessionId);
+ return;
+ }
+
+ // Create new session
+ try {
+ const response = await fetch(`${API_BASE_URL}/api/session`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to create session: ${response.status} ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ console.log("Created new session with ID:", data.sessionId);
+
+ // Save session ID
+ localStorage.setItem(STORAGE_KEY_SESSION_ID, data.sessionId);
+ setSessionId(data.sessionId);
+ } catch (error) {
+ // Use fallback ID if API call fails
+ const fallbackId = fallbackSessionId.current;
+ console.log("Using fallback session ID:", fallbackId);
+ localStorage.setItem(STORAGE_KEY_SESSION_ID, fallbackId);
+ setSessionId(fallbackId);
+ }
+
+ } catch (error) {
+ console.error("Error initializing session:", error);
+ }
+ };
+
+ initSession();
+ }, [apiKey]);
+
+ // Register servers with backend when they change
+ useEffect(() => {
+ if (nandaServers.length > 0 && sessionId) {
+ // Save servers to localStorage whenever they change
+ localStorage.setItem(STORAGE_KEY_SERVERS, JSON.stringify(nandaServers));
+
+ const registerAllServers = async () => {
+ for (const server of nandaServers) {
+ try {
+ const response = await fetch(
+ `${API_BASE_URL}/api/servers`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(server),
+ }
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ console.error(`Failed to register server ${server.id}:`, errorData);
+ // Continue with other servers even if one fails
+ } else {
+ const data = await response.json();
+ console.log(`Server ${server.id} registered successfully:`, data);
+ }
+ } catch (error) {
+ console.error(`Error registering server ${server.id}:`, error);
+ // Continue with other servers even if one fails
+ }
+ }
+ };
+
+ registerAllServers();
+ }
+ }, [nandaServers, sessionId]);
+
// 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) {
+
+ if (existingIndex !== -1) {
// Update existing server
- newServers = [...prevServers];
- newServers[existingIndex] = server;
+ const updatedServers = [...prevServers];
+ updatedServers[existingIndex] = server;
+ return updatedServers;
} else {
// Add new server
- newServers = [...prevServers, server];
+ return [...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;
+ // Filter out the server with the matching ID
+ return prevServers.filter((server) => server.id !== id);
});
}, []);
- // Register servers with backend on initial load
- useEffect(() => {
- if (nandaServers.length > 0) {
- // Register all servers with the backend
- 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
- );
- }
+ // Get tools that require credentials
+ const getToolsWithCredentialRequirements = useCallback(async (): Promise => {
+ if (!sessionId) {
+ console.warn("Cannot get tools: No session ID available");
+ return [];
+ }
+
+ console.log(`Fetching tools requiring credentials using session ID: ${sessionId}`);
+ console.log(`Using API base URL: ${API_BASE_URL}`);
+
+ try {
+ const response = await fetch(
+ `${API_BASE_URL}/api/tools/credentials`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ "X-Session-ID": sessionId,
+ },
}
- };
+ );
- registerAllServers();
+ if (!response.ok) {
+ throw new Error(`Failed to get tools: ${response.status} ${response.statusText}`);
+ }
+
+ const contentType = response.headers.get("content-type");
+ if (!contentType || !contentType.includes("application/json")) {
+ console.error("Received non-JSON response:", contentType);
+ return [];
+ }
+
+ const data = await response.json();
+ console.log("Tools with credential requirements:", data);
+ return data.tools || [];
+ } catch (error) {
+ console.error("Error getting tools with credential requirements:", error);
+ return [];
}
- }, []);
+ }, [sessionId]);
+
+ // Set credentials for a tool
+ const setToolCredentials = useCallback(
+ async (
+ toolName: string,
+ serverId: string,
+ credentials: Record
+ ): Promise => {
+ if (!sessionId) {
+ console.warn("Cannot set credentials: No session ID available");
+ return false;
+ }
+
+ console.log(`Setting credentials for tool ${toolName} from server ${serverId}`);
+
+ try {
+ const response = await fetch(
+ `${API_BASE_URL}/api/tools/credentials`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-Session-ID": sessionId,
+ },
+ body: JSON.stringify({
+ toolName,
+ serverId,
+ credentials,
+ } as ToolCredentialRequest),
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error(`Failed to set credentials: ${response.status} ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ console.log("Credentials set successfully:", data);
+ return data.success || false;
+ } catch (error) {
+ console.error(`Error setting credentials for tool ${toolName}:`, error);
+ return false;
+ }
+ },
+ [sessionId]
+ );
// Refresh servers from registry
- const refreshRegistry = useCallback(async () => {
+ const refreshRegistry = useCallback(async (searchQuery?: string) => {
try {
+ // If there's a search query, use the search endpoint
+ const endpoint = searchQuery
+ ? `${API_BASE_URL}/api/registry/search?q=${encodeURIComponent(searchQuery)}`
+ : `${API_BASE_URL}/api/registry/refresh`;
+
+ const method = searchQuery ? "GET" : "POST";
+ console.log(`Fetching servers from ${endpoint} with method ${method}`);
+
const response = await fetch(
- `${
- process.env.REACT_APP_API_BASE_URL ?? "http://localhost:3000"
- }/api/registry/refresh`,
+ endpoint,
{
- method: "POST",
+ method,
headers: {
"Content-Type": "application/json",
},
@@ -174,14 +338,14 @@ export const SettingsProvider: React.FC = ({
if (!response.ok) {
const errorData = await response.json();
throw new Error(
- errorData.error || "Failed to refresh servers from registry"
+ errorData.error || "Failed to fetch servers from registry"
);
}
const data = await response.json();
return data;
} catch (error) {
- console.error("Error refreshing servers from registry:", error);
+ console.error("Error fetching servers from registry:", error);
throw error;
}
}, []);
@@ -195,6 +359,9 @@ export const SettingsProvider: React.FC = ({
registerNandaServer,
removeNandaServer,
refreshRegistry,
+ getToolsWithCredentialRequirements,
+ setToolCredentials,
+ sessionId,
}}
>
{children}
diff --git a/client/src/index.css b/client/src/index.css
index e85508e..0760177 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -6,7 +6,7 @@ body {
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
- background-color: #0a0a0a;
+ background-color: #1c1921;
overflow: hidden;
}
@@ -41,12 +41,12 @@ code {
}
::-webkit-scrollbar-thumb {
- background: rgba(255, 255, 255, 0.2);
+ background: rgba(220, 20, 60, 0.2);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
- background: rgba(255, 255, 255, 0.3);
+ background: rgba(220, 20, 60, 0.3);
}
/* Typography */
@@ -65,12 +65,13 @@ p {
}
a {
- color: #d11f41;
+ color: #dc143c;
text-decoration: none;
}
a:hover {
text-decoration: underline;
+ color: #ff6a8a;
}
/* Markdown styling */
@@ -83,7 +84,7 @@ pre {
}
blockquote {
- border-left: 3px solid #d11f41;
+ border-left: 3px solid #dc143c;
margin-left: 0;
padding-left: 1em;
color: rgba(255, 255, 255, 0.7);
@@ -91,9 +92,33 @@ blockquote {
/* Glassmorphism utilities */
.glass {
- background: rgba(255, 255, 255, 0.1);
+ background: rgba(42, 36, 41, 0.15);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.18);
- box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
+ box-shadow: 0 8px 32px 0 rgba(220, 20, 60, 0.15);
+}
+
+/* Network visualization styling */
+.network-node {
+ fill: #dc143c;
+ stroke: #ff6a8a;
+ transition: all 0.3s ease;
+}
+
+.network-link {
+ stroke: rgba(220, 20, 60, 0.6);
+ stroke-width: 1.5;
+}
+
+.network-node.active {
+ fill: #f3cc24;
+ stroke: #f9de72;
+ filter: drop-shadow(0 0 8px rgba(220, 20, 60, 0.5));
+}
+
+/* Button and UI element overrides */
+.btn-primary,
+.chakra-button {
+ transition: all 0.2s ease !important;
}
diff --git a/client/src/react-app-env.d.ts b/client/src/react-app-env.d.ts
new file mode 100644
index 0000000..b7ef3b8
--- /dev/null
+++ b/client/src/react-app-env.d.ts
@@ -0,0 +1,4 @@
+///
+
+// This file is automatically generated by running react-scripts.
+// It provides type definitions for the environment.
\ No newline at end of file
diff --git a/client/src/services/socketService.ts b/client/src/services/socketService.ts
new file mode 100644
index 0000000..b67143f
--- /dev/null
+++ b/client/src/services/socketService.ts
@@ -0,0 +1,95 @@
+import { io, Socket } from 'socket.io-client';
+
+type ToolExecutionHandler = (data: {
+ toolName: string;
+ serverId: string;
+ serverName: string;
+ result: {
+ content: any[];
+ isError?: boolean;
+ }
+}) => void;
+
+class SocketService {
+ private socket: Socket | null = null;
+ private toolExecutionHandlers: ToolExecutionHandler[] = [];
+ private toolExecutionCache = new Map();
+
+ constructor() {
+ const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || '';
+ console.log('Initializing socket connection to:', API_BASE_URL);
+
+ this.socket = io(API_BASE_URL, {
+ path: '/socket.io',
+ transports: ['websocket', 'polling'],
+ reconnectionAttempts: 5,
+ reconnectionDelay: 1000,
+ timeout: 20000
+ });
+
+ this.setupListeners();
+ }
+
+ private setupListeners() {
+ if (!this.socket) return;
+
+ this.socket.on('connect', () => {
+ console.log('Socket connected:', this.socket?.id);
+ });
+
+ this.socket.on('connect_error', (err) => {
+ console.error('Socket connection error:', err);
+ });
+
+ this.socket.on('disconnect', (reason: string) => {
+ console.log('Socket disconnected:', reason);
+ });
+
+ this.socket.on('tool_executed', (data: any) => {
+ console.log('Tool executed event received:', data);
+
+ const { toolName, serverId, serverName, result } = data;
+
+ // Store in cache
+ this.toolExecutionCache.set(toolName, {
+ toolName,
+ serverId,
+ serverName,
+ timestamp: new Date().toISOString()
+ });
+
+ // Notify handlers
+ this.toolExecutionHandlers.forEach(handler => {
+ handler(data);
+ });
+ });
+ }
+
+ public addToolExecutionHandler(handler: ToolExecutionHandler) {
+ this.toolExecutionHandlers.push(handler);
+ console.log(`Added tool execution handler, total: ${this.toolExecutionHandlers.length}`);
+ }
+
+ public removeToolExecutionHandler(handler: ToolExecutionHandler) {
+ this.toolExecutionHandlers = this.toolExecutionHandlers.filter(h => h !== handler);
+ }
+
+ public getServerInfo(toolName: string) {
+ return this.toolExecutionCache.get(toolName);
+ }
+
+ public disconnect() {
+ if (this.socket) {
+ this.socket.disconnect();
+ this.socket = null;
+ }
+ }
+}
+
+// Export as a singleton
+export const socketService = new SocketService();
\ No newline at end of file
diff --git a/client/src/theme.ts b/client/src/theme.ts
index 28980c9..781c87e 100644
--- a/client/src/theme.ts
+++ b/client/src/theme.ts
@@ -1,138 +1,169 @@
// client/src/theme.ts
-import { extendTheme } from "@chakra-ui/react";
+import { extendTheme, ThemeConfig } from "@chakra-ui/react";
-// Define the crimson color palette
-const colors = {
- crimson: {
- 50: "#ffe5e9",
- 100: "#fabbcb",
- 200: "#f590ad",
- 300: "#f0658e",
- 400: "#eb3a70",
- 500: "#d22152",
- 600: "#a41840",
- 700: "#771030",
- 800: "#4a0820",
- 900: "#200010",
- },
+// Color scheme configuration
+const config: ThemeConfig = {
+ initialColorMode: "dark",
+ useSystemColorMode: false,
};
-// Create glassmorphism styles
-const glassmorphism = {
- backgroundFilter: {
- backdropFilter: "blur(10px)",
- backgroundColor: "rgba(255, 255, 255, 0.1)",
- boxShadow: "0 8px 32px 0 rgba(31, 38, 135, 0.37)",
- border: "1px solid rgba(255, 255, 255, 0.18)",
- borderRadius: "10px",
- },
- card: {
- background: "rgba(255, 255, 255, 0.1)",
- backdropFilter: "blur(10px)",
- boxShadow: "0 8px 32px 0 rgba(31, 38, 135, 0.37)",
- borderRadius: "10px",
- border: "1px solid rgba(255, 255, 255, 0.18)",
- },
-};
-
-// Custom theme definition
+// Custom theme for the application
export const theme = extendTheme({
- colors,
+ config,
+ colors: {
+ // Crimson/red color palette
+ primary: {
+ 50: "#ffe5e9",
+ 100: "#ffccd3",
+ 200: "#ffabbd",
+ 300: "#ff8ca6",
+ 400: "#ff6a8a",
+ 500: "#dc143c", // Primary accent color - Crimson
+ 600: "#c41236",
+ 700: "#a81030",
+ 800: "#8c0d29",
+ 900: "#710a22",
+ },
+ // Secondary accent color (gold-like for complementary effect)
+ secondary: {
+ 50: "#fef8e7",
+ 100: "#fcefc0",
+ 200: "#fbe799",
+ 300: "#f9de72",
+ 400: "#f6d54b",
+ 500: "#f3cc24", // Secondary accent
+ 600: "#deb51b",
+ 700: "#b89214",
+ 800: "#93740f",
+ 900: "#6e560b",
+ },
+ // Dark theme background gradients
+ dark: {
+ 100: "#2a2429", // Lighter background with slight red tint
+ 200: "#22202a", // Container background
+ 300: "#1c1921", // Main background
+ 400: "#16151a", // Darker background
+ 500: "#100e12", // Deepest background
+ },
+ },
styles: {
global: {
body: {
- fontFamily: "Inter, system-ui, sans-serif",
+ bg: "linear-gradient(135deg, #1c1921 0%, #2a1a20 100%)", // Darker crimson gradient
color: "white",
- lineHeight: "tall",
+ fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif",
+ },
+ "*::placeholder": {
+ color: "whiteAlpha.400",
+ },
+ "*, *::before, &::after": {
+ borderColor: "whiteAlpha.200",
},
},
},
+ fonts: {
+ heading: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif",
+ body: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif",
+ },
components: {
Button: {
baseStyle: {
- fontWeight: "bold",
+ fontWeight: "600",
borderRadius: "md",
},
variants: {
solid: {
- bg: "crimson.500",
+ bg: "primary.500",
color: "white",
_hover: {
- bg: "crimson.600",
+ bg: "primary.600",
+ _disabled: {
+ bg: "primary.500",
+ },
+ },
+ _active: {
+ bg: "primary.700",
},
},
outline: {
- color: "crimson.500",
- borderColor: "crimson.500",
+ borderColor: "primary.500",
+ color: "primary.500",
_hover: {
- bg: "rgba(210, 33, 82, 0.1)",
+ bg: "rgba(220, 20, 60, 0.1)", // Transparent crimson
},
},
ghost: {
- color: "white",
+ color: "whiteAlpha.900",
_hover: {
- bg: "rgba(255, 255, 255, 0.1)",
+ bg: "whiteAlpha.100",
},
},
},
- defaultProps: {
- variant: "solid",
- },
},
Input: {
variants: {
- glass: {
+ outline: {
field: {
- bg: "rgba(255, 255, 255, 0.1)",
- backdropFilter: "blur(10px)",
- border: "1px solid rgba(255, 255, 255, 0.18)",
- borderRadius: "md",
- color: "white",
- _placeholder: {
- color: "rgba(255, 255, 255, 0.5)",
+ borderColor: "whiteAlpha.300",
+ bg: "whiteAlpha.50",
+ _hover: {
+ borderColor: "primary.300",
},
_focus: {
- borderColor: "crimson.300",
- boxShadow: "0 0 0 1px rgba(210, 33, 82, 0.6)",
+ borderColor: "primary.500",
+ boxShadow: "0 0 0 1px var(--chakra-colors-primary-500)",
},
},
},
},
- defaultProps: {
- variant: "glass",
- },
},
Textarea: {
variants: {
- glass: {
- bg: "rgba(255, 255, 255, 0.1)",
- backdropFilter: "blur(10px)",
- border: "1px solid rgba(255, 255, 255, 0.18)",
- borderRadius: "md",
- color: "white",
- _placeholder: {
- color: "rgba(255, 255, 255, 0.5)",
+ outline: {
+ borderColor: "whiteAlpha.300",
+ bg: "whiteAlpha.50",
+ _hover: {
+ borderColor: "primary.300",
},
_focus: {
- borderColor: "crimson.300",
- boxShadow: "0 0 0 1px rgba(210, 33, 82, 0.6)",
+ borderColor: "primary.500",
+ boxShadow: "0 0 0 1px var(--chakra-colors-primary-500)",
},
},
},
- defaultProps: {
- variant: "glass",
- },
},
Modal: {
baseStyle: {
dialog: {
- ...glassmorphism.card,
- bg: "rgba(26, 32, 44, 0.8)",
- color: "white",
+ bg: "dark.200",
+ boxShadow: "xl",
+ borderRadius: "lg",
+ border: "1px solid",
+ borderColor: "whiteAlpha.100",
+ },
+ header: {
+ borderBottomWidth: "1px",
+ borderColor: "whiteAlpha.100",
},
},
},
},
- // Custom glassmorphism styles that can be accessed globally
- glassmorphism,
+ layerStyles: {
+ card: {
+ bg: "rgba(42, 36, 41, 0.7)", // Slightly reddish tint
+ borderRadius: "xl",
+ boxShadow: "lg",
+ backdropFilter: "blur(10px)",
+ border: "1px solid rgba(255, 255, 255, 0.08)",
+ },
+ },
+ glassmorphism: {
+ card: {
+ bg: "rgba(42, 36, 41, 0.7)", // Slightly reddish tint
+ backdropFilter: "blur(10px)",
+ borderRadius: "xl",
+ boxShadow: "0 8px 32px rgba(0, 0, 0, 0.1)",
+ border: "1px solid rgba(255, 255, 255, 0.08)",
+ },
+ },
});
diff --git a/docker-compose.yml b/docker-compose.yml
index 6a42fcc..72038ce 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,5 +1,3 @@
-version: "3.8"
-
services:
# Backend server
server:
@@ -14,22 +12,27 @@ services:
- NODE_ENV=production
- PORT=4000
- CLIENT_URL=http://localhost:3000
+ - CREDENTIAL_ENCRYPTION_KEY=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
volumes:
- ./server/logs:/app/logs
networks:
- mcp-network
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
# Frontend client
client:
build:
context: .
dockerfile: Dockerfile.client
+ args:
+ - REACT_APP_API_BASE_URL=http://localhost:4000
container_name: mcp-host-client
restart: unless-stopped
ports:
- "3000:3000"
environment:
- - REACT_APP_API_BASE_URL=${API_BASE_URL}
+ - REACT_APP_API_BASE_URL=http://localhost:4000
depends_on:
- server
networks:
diff --git a/host_screenshot.png b/host_screenshot.png
new file mode 100644
index 0000000..d0ddee3
Binary files /dev/null and b/host_screenshot.png differ
diff --git a/nginx.conf b/nginx.conf
index e193e05..66f3770 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -1,19 +1,25 @@
server {
listen 3000;
+ server_name localhost;
- location / {
- root /usr/share/nginx/html;
- index index.html;
- try_files $uri $uri/ /index.html;
- }
+ root /usr/share/nginx/html;
+ index index.html;
- # Proxy requests to the backend server
- location /api {
- proxy_pass http://server:4000;
+ # Handle API requests - proxy to the server on port 4000
+ location /api/ {
+ proxy_pass http://server:4000/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
+ # Set longer timeout for API requests
+ proxy_read_timeout 120s;
+ proxy_connect_timeout 120s;
+ }
+
+ # Serve static content
+ location / {
+ try_files $uri $uri/ /index.html;
}
}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..983909b
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1658 @@
+{
+ "name": "canvas",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "canvas",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.9.0",
+ "express": "^4.21.2",
+ "zod": "^3.24.2"
+ },
+ "devDependencies": {
+ "@types/react": "^19.1.1",
+ "@types/socket.io": "^3.0.1"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.9.0.tgz",
+ "integrity": "sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "cors": "^2.8.5",
+ "cross-spawn": "^7.0.3",
+ "eventsource": "^3.0.2",
+ "express": "^5.0.1",
+ "express-rate-limit": "^7.5.0",
+ "pkce-challenge": "^5.0.0",
+ "raw-body": "^3.0.0",
+ "zod": "^3.23.8",
+ "zod-to-json-schema": "^3.24.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
+ "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.0",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.6.3",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.0",
+ "raw-body": "^3.0.0",
+ "type-is": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
+ "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/debug": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/express": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
+ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.0",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
+ "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/send": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
+ "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.5",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "mime-types": "^3.0.1",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
+ "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/cors": {
+ "version": "2.8.17",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
+ "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "22.14.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
+ "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.1.1",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.1.tgz",
+ "integrity": "sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/socket.io": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz",
+ "integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "socket.io": "*"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/base64id": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^4.5.0 || >= 5.9"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.13.0",
+ "raw-body": "2.5.2",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/raw-body": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/engine.io": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
+ "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/cors": "^2.8.12",
+ "@types/node": ">=10.0.0",
+ "accepts": "~1.3.4",
+ "base64id": "2.0.0",
+ "cookie": "~0.7.2",
+ "cors": "~2.8.5",
+ "debug": "~4.3.1",
+ "engine.io-parser": "~5.2.1",
+ "ws": "~8.17.1"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/engine.io-parser": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/engine.io/node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/engine.io/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/engine.io/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/eventsource": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz",
+ "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==",
+ "license": "MIT",
+ "dependencies": {
+ "eventsource-parser": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/eventsource-parser": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz",
+ "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.3",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.7.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.3.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.13.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.19.0",
+ "serve-static": "1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express-rate-limit": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz",
+ "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/express-rate-limit"
+ },
+ "peerDependencies": {
+ "express": "^4.11 || 5 || ^5.0.0-beta.1"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
+ },
+ "node_modules/pkce-challenge": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
+ "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
+ "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.6.3",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/raw-body/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/router/node_modules/debug": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/router/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/router/node_modules/path-to-regexp": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
+ "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/send": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.19.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/socket.io": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
+ "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.4",
+ "base64id": "~2.0.0",
+ "cors": "~2.8.5",
+ "debug": "~4.3.2",
+ "engine.io": "~6.6.0",
+ "socket.io-adapter": "~2.5.2",
+ "socket.io-parser": "~4.2.4"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/socket.io-adapter": {
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
+ "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "~4.3.4",
+ "ws": "~8.17.1"
+ }
+ },
+ "node_modules/socket.io-adapter/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-adapter/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/socket.io-parser": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/socket.io/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/ws": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.24.2",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
+ "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-to-json-schema": {
+ "version": "3.24.5",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz",
+ "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==",
+ "license": "ISC",
+ "peerDependencies": {
+ "zod": "^3.24.1"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..f66b2df
--- /dev/null
+++ b/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "canvas",
+ "version": "1.0.0",
+ "description": "MCP Host is a complete end-to-end implementation of a Model Context Protocol (MCP) host with an in-built MCP client. It provides a beautiful, polished chat interface with tool selection capabilities using MCP.",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.9.0",
+ "express": "^4.21.2",
+ "zod": "^3.24.2"
+ },
+ "type": "module",
+ "devDependencies": {
+ "@types/react": "^19.1.1",
+ "@types/socket.io": "^3.0.1"
+ }
+}
diff --git a/server/.env b/server/.env
new file mode 100644
index 0000000..e11b63a
--- /dev/null
+++ b/server/.env
@@ -0,0 +1,4 @@
+PORT=4000
+CLIENT_URL=http://localhost:3000
+# Optional: Add this if you have a registry API key
+# REGISTRY_API_KEY=your_registry_api_key_here
\ No newline at end of file
diff --git a/server/package.json b/server/package.json
index a06462f..de21845 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,38 +1,33 @@
{
- "name": "mcp-host-server",
+ "name": "canvas",
"version": "1.0.0",
- "description": "Backend server for MCP Host",
- "main": "dist/index.js",
- "type": "module",
+ "description": "MCP Host is a complete end-to-end implementation of a Model Context Protocol (MCP) host with an in-built MCP client. It provides a beautiful, polished chat interface with tool selection capabilities using MCP.",
+ "main": "index.js",
"scripts": {
- "build": "tsc",
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "build": "tsc && cp -r ../shared ./dist/",
"start": "node dist/index.js",
"dev": "nodemon --exec ts-node src/index.ts",
- "test": "echo \"Error: no test specified\" && exit 1"
+ "clean": "rm -rf dist"
},
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
- "@modelcontextprotocol/sdk": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.9.0",
"axios": "^1.8.4",
- "cors": "^2.8.5",
- "dotenv": "^16.3.1",
- "express": "^4.18.2",
- "socket.io": "^4.7.2",
- "uuid": "^9.0.1",
- "zod": "^3.22.4"
+ "dotenv": "^16.5.0",
+ "express": "^4.21.2",
+ "uuid": "^11.1.0",
+ "zod": "^3.24.2"
},
+ "type": "module",
"devDependencies": {
- "@types/cors": "^2.8.15",
- "@types/express": "^4.17.20",
- "@types/node": "^20.8.10",
- "@types/uuid": "^9.0.6",
- "nodemon": "^3.0.1",
- "ts-node": "^10.9.1",
- "typescript": "^5.2.2"
- },
- "engines": {
- "node": ">=18.0.0"
- },
- "author": "",
- "license": "MIT"
+ "@types/react": "^19.1.1",
+ "@types/socket.io": "^3.0.1",
+ "nodemon": "^3.1.0",
+ "ts-node": "^10.9.2",
+ "typescript": "^5.4.2"
+ }
}
diff --git a/server/src/index.ts b/server/src/index.ts
index 97b7344..ae6683d 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -37,10 +37,26 @@ const io = new SocketIoServer(server, {
methods: ["GET", "POST"],
credentials: true,
},
+ // Increased timeouts and improved reconnection settings
+ pingTimeout: 60000, // 60 seconds ping timeout
+ pingInterval: 25000, // 25 seconds ping interval
+ connectTimeout: 30000 // 30 seconds connect timeout
});
-// Initialize MCP Manager with registry URL
-const mcpManager = setupMcpManager(io, REGISTRY_URL, REGISTRY_API_KEY);
+// Store io instance in app for access in routes
+app.set('io', io);
+
+// Socket.IO event handling
+io.on('connection', (socket) => {
+ console.log(`Socket connected: ${socket.id}`);
+
+ socket.on('disconnect', () => {
+ console.log(`Socket disconnected: ${socket.id}`);
+ });
+});
+
+// Initialize MCP Manager
+const mcpManager = setupMcpManager(io);
// Setup routes
setupRoutes(app, mcpManager);
@@ -48,12 +64,12 @@ setupRoutes(app, mcpManager);
// Load servers from registry on startup
(async () => {
try {
- // console.log("Fetching servers from registry...");
- const registryServers = await mcpManager.fetchRegistryServers();
- console.log(`Loaded ${registryServers.length} servers from registry`);
- // console.warn("Registry URL is not set. Skipping server loading.");
+ // No need to fetch from registry as servers are loaded from local storage
+ console.log(`Loading servers from local storage...`);
+ const availableServers = mcpManager.getAvailableServers();
+ console.log(`Loaded ${availableServers.length} servers from local storage`);
} catch (error) {
- console.error("Error loading servers from registry:", error);
+ console.error("Error loading servers:", error);
}
})();
@@ -69,9 +85,6 @@ process.on("SIGTERM", async () => {
});
});
-// Setup routes
-setupRoutes(app, mcpManager);
-
// Start the server
const PORT = process.env.PORT || 4000;
server.listen(PORT, () => {
diff --git a/server/src/mcp/manager.ts b/server/src/mcp/manager.ts
index 86fd315..8859f23 100644
--- a/server/src/mcp/manager.ts
+++ b/server/src/mcp/manager.ts
@@ -5,200 +5,321 @@ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import { ToolRegistry } from "./toolRegistry.js";
import { SessionManager } from "./sessionManager.js";
+import { ToolInfo, CredentialRequirement, ServerConfig } from "./types.js";
+import { v4 as uuidv4 } from 'uuid';
+import fs from 'fs';
+import path from 'path';
import { RegistryClient } from "../registry/client.js";
+// Disable server-side persistence - we'll use browser-based storage instead
+// const STORAGE_DIR = process.env.MCP_STORAGE_DIR || path.join(process.cwd(), 'storage');
+// const SERVERS_FILE = path.join(STORAGE_DIR, 'servers.json');
+
+// // Ensure the storage directory exists
+// if (!fs.existsSync(STORAGE_DIR)) {
+// fs.mkdirSync(STORAGE_DIR, { recursive: true });
+// }
+
+// // Create servers file if it doesn't exist - with empty array
+// if (!fs.existsSync(SERVERS_FILE)) {
+// fs.writeFileSync(SERVERS_FILE, JSON.stringify([], null, 2));
+// }
+
+// // Ensure the file is only readable by the server process
+// try {
+// fs.chmodSync(SERVERS_FILE, 0o600);
+// } catch (error) {
+// console.warn('Unable to set file permissions, server configuration may not be secure');
+// }
+
+// Disable loading servers from file - rely on client registrations only
+const loadServers = (): ServerConfig[] => {
+ // Comment out file loading
+ // try {
+ // const data = fs.readFileSync(SERVERS_FILE, 'utf8');
+ // return JSON.parse(data);
+ // } catch (error) {
+ // console.error('Error loading servers:', error);
+ // return [];
+ // }
+ return []; // Return empty array - no pre-loaded servers
+};
+
+// Disable saving servers to file
+const saveServers = (servers: ServerConfig[]) => {
+ // Comment out file saving
+ // try {
+ // fs.writeFileSync(SERVERS_FILE, JSON.stringify(servers, null, 2));
+ // } catch (error) {
+ // console.error('Error saving servers:', error);
+ // }
+ // No-op - we don't save servers anymore
+};
+
+// Local type declarations instead of importing from shared
+// Remove local type declarations since we're importing them now
+
+interface ToolCredentialInfo {
+ toolName: string;
+ serverName: string;
+ serverId: string;
+ credentials: CredentialRequirement[];
+}
+
export interface McpManager {
- discoverTools: (sessionId: string) => Promise;
+ discoverTools: (sessionId: string) => Promise;
executeToolCall: (
sessionId: string,
toolName: string,
args: any
) => Promise;
- registerServer: (serverConfig: ServerConfig) => Promise;
- fetchRegistryServers: () => Promise;
+ registerServer: (serverConfig: ServerConfig) => Promise;
getAvailableServers: () => ServerConfig[];
+ getToolsWithCredentialRequirements: (sessionId: string) => ToolCredentialInfo[];
+ setToolCredentials: (
+ sessionId: string,
+ toolName: string,
+ serverId: string,
+ credentials: Record
+ ) => Promise;
cleanup: () => Promise;
+ getSessionManager: () => SessionManager;
+ fetchRegistryServers: () => Promise;
}
-export interface ServerConfig {
- id: string;
- name: string;
- url: string;
- description?: string;
- types?: string[];
- tags?: string[];
- verified?: boolean;
- rating?: number;
+// Remove local ServerConfig interface
+
+// Rate limiting data structures
+interface RateLimitInfo {
+ lastRequestTime: number;
+ requestCount: number;
+ isProcessing: boolean;
+ queue: Array<{
+ resolve: (value: any) => void;
+ reject: (error: any) => void;
+ toolName: string;
+ sessionId: string;
+ args: any;
+ }>;
}
-export function setupMcpManager(
- io: SocketIoServer,
- registryUrl?: string,
- registryApiKey?: string
-): McpManager {
+// Rate limiting configuration
+const RATE_LIMIT_CONFIG = {
+ // Maximum requests per minute to a server
+ requestsPerMinute: 10,
+ // Minimum time between requests in ms (100ms = 0.1s)
+ minRequestSpacing: 500,
+ // Maximum queue length per server
+ maxQueueLength: 50,
+};
+
+export function setupMcpManager(io: SocketIoServer): McpManager {
+ console.log("--- McpManager setup initiated ---");
+
// 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
+ // Available server configurations - start with empty array
const servers: ServerConfig[] = [];
// Cache of connected clients
const connectedClients: Map = new Map();
- // Registry client if URL is provided
- const registryClient = registryUrl
- ? new RegistryClient({
- url: registryUrl,
- apiKey: registryApiKey,
- })
- : null;
-
- // Fetch servers from registry
- // In the fetchRegistryServers function
- const fetchRegistryServers = async (): Promise => {
- if (!registryClient) {
- return [];
+ // Track rate limit information for each server
+ const rateLimits = new Map();
+
+ // Create a registry client instance
+ const registryClient = new RegistryClient(process.env.REGISTRY_URL || "https://nanda-registry.com", process.env.REGISTRY_API_KEY);
+
+ // Registry cache settings
+ const registryCache = {
+ servers: [] as ServerConfig[],
+ lastFetched: 0,
+ expiryMs: 60 * 60 * 1000, // 1 hour cache expiry
+ isValid: function() {
+ return this.servers.length > 0 &&
+ (Date.now() - this.lastFetched) < this.expiryMs;
+ },
+ update: function(servers: ServerConfig[]) {
+ this.servers = servers;
+ this.lastFetched = Date.now();
+ console.log(`Updated registry cache with ${servers.length} servers at ${new Date().toISOString()}`);
}
+ };
+ const registerServer = async (serverConfig: ServerConfig): Promise => {
try {
- const registryServers = await registryClient.getServers();
-
- // Register all discovered servers
- for (const serverConfig of registryServers) {
- // Check if server is already registered by ID or URL
- const existingServerWithSameUrl = servers.find(
- (s) => s.url === serverConfig.url
- );
-
- if (existingServerWithSameUrl) {
- // Server with same URL exists, update it with new metadata but keep track
- console.log(
- `Server with URL ${serverConfig.url} already exists with ID ${existingServerWithSameUrl.id}, updating metadata`
- );
-
- // Update the existing server entry with new metadata
- Object.assign(existingServerWithSameUrl, {
- ...serverConfig,
- id: existingServerWithSameUrl.id, // Keep the original ID
- });
- } else if (!servers.some((s) => s.id === serverConfig.id)) {
- // This is a completely new server, register it
- try {
- await registerServer(serverConfig);
- } catch (error) {
- console.error(
- `Failed to register server ${serverConfig.name} from registry:`,
- error
- );
- }
- }
+ console.log(`Registering server: ${JSON.stringify(serverConfig)}`);
+
+ // Initialize rate limit tracking for this server if it doesn't exist
+ if (!rateLimits.has(serverConfig.id)) {
+ rateLimits.set(serverConfig.id, {
+ lastRequestTime: 0,
+ requestCount: 0,
+ isProcessing: false,
+ queue: []
+ });
}
-
- return registryServers;
- } catch (error) {
- console.error("Error fetching servers from registry:", error);
- return [];
- }
- };
-
- // In the registerServer function
- const registerServer = async (serverConfig: ServerConfig): Promise => {
- // Check if server with same URL already exists
- const existingUrlIndex = servers.findIndex(
- (s) => s.url === serverConfig.url
- );
-
- if (existingUrlIndex !== -1) {
- // A server with this URL already exists, update it
- const existingId = servers[existingUrlIndex].id;
- console.log(
- `Updating server with URL ${serverConfig.url} (existing ID: ${existingId}, new ID: ${serverConfig.id})`
- );
-
- // Clean up old client connection if IDs differ
- if (existingId !== serverConfig.id && connectedClients.has(existingId)) {
- const oldClient = connectedClients.get(existingId);
- try {
- await oldClient?.close();
- } catch (error) {
- console.error(
- `Error closing old client connection for ${existingId}:`,
- error
- );
- }
- connectedClients.delete(existingId);
-
- // Remove tools from the old server
- toolRegistry.removeToolsByServerId(existingId);
+
+ // If client already exists, just return
+ if (connectedClients.has(serverConfig.id)) {
+ console.log(`Already connected to server: ${serverConfig.id}`);
+ return true;
}
-
- // Update the server entry
- servers[existingUrlIndex] = serverConfig;
- } else {
- // Check if ID already exists
- const existingIdIndex = servers.findIndex(
- (s) => s.id === serverConfig.id
- );
- if (existingIdIndex !== -1) {
- // Update existing server with same ID
- servers[existingIdIndex] = serverConfig;
+
+ // Check if this server already exists
+ const existingIndex = servers.findIndex((s) => s.id === serverConfig.id);
+ if (existingIndex !== -1) {
+ servers[existingIndex] = serverConfig;
} else {
- // Add new server
servers.push(serverConfig);
}
- }
- try {
// Create MCP client for this server using SSE transport
const sseUrl = new URL(serverConfig.url);
+
+ // Use standard SSE transport with default timeout
const transport = new SSEClientTransport(sseUrl);
const client = new Client({
name: "mcp-host",
version: "1.0.0",
+ // Set the timeout at the client level
+ defaultTimeout: 300000, // 5 minutes timeout (increased from 3 minutes)
+ // Add retry configuration
+ retryConfig: {
+ maxRetries: 5, // Increased from 3
+ initialDelay: 2000, // Start with 2 seconds delay (increased from 1)
+ maxDelay: 20000, // Max 20 second delay (increased from 10)
+ backoffFactor: 2 // Exponential backoff factor
+ }
});
await client.connect(transport);
+ console.log(`Successfully connected to server: ${serverConfig.id}`);
// Fetch available tools from the server
const toolsResult = await client.listTools();
+ console.log(`Tools discovered: ${JSON.stringify(toolsResult?.tools?.map(t => t.name) || [])}`);
- // Register tools in our registry
- if (toolsResult?.tools) {
- toolRegistry.registerTools(serverConfig.id, client, toolsResult.tools);
+ // Ensure the server has tools
+ if (!toolsResult?.tools || toolsResult.tools.length === 0) {
+ throw new Error("No tools discovered on MCP server");
}
+ // Register tools in our registry
+ toolRegistry.registerTools(
+ serverConfig.id,
+ serverConfig.name,
+ serverConfig.rating ?? 0,
+ 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
+ toolsResult?.tools?.length
} tools`
);
+
+ // Successful Registration
+ return true;
} catch (error) {
console.error(
- `Failed to connect to MCP server ${serverConfig.name}:`,
+ `Failed to register server ${serverConfig.name}:`,
error
);
- // Remove the server from our list since we couldn't connect
+
+ // Clean up in-memory state
const index = servers.findIndex((s) => s.id === serverConfig.id);
if (index !== -1) {
servers.splice(index, 1);
}
- throw error;
+
+ // Failed registration
+ return false;
}
};
// Discover all available tools for a session
- const discoverTools = async (sessionId: string): Promise => {
- return toolRegistry.getAllTools();
+ const discoverTools = async (sessionId: string): Promise => {
+ // Get all tools from the registry
+ const tools = toolRegistry.getAllTools();
+
+ // For each tool, check if we have stored credentials and modify schemas accordingly
+ const modifiedTools = tools.map(toolInfo => {
+ const { serverId, name } = toolInfo;
+
+ // Only process tools with credential requirements
+ if (toolInfo.credentialRequirements && toolInfo.credentialRequirements.length > 0) {
+ // Check if we have stored credentials for this tool
+ const credentials = sessionManager.getToolCredentials(sessionId, name, serverId);
+
+ if (credentials) {
+ console.log(`🔑 Modifying schema for tool ${name} to mark credentials as optional since they are stored`);
+
+ // Create a copy of the tool info to modify
+ const modifiedTool = { ...toolInfo };
+
+ // If the tool has inputSchema, create a modified version
+ if (modifiedTool.inputSchema) {
+ // Create a deep copy of the input schema
+ const modifiedSchema = JSON.parse(JSON.stringify(modifiedTool.inputSchema));
+
+ // If the schema has a __credentials property, mark it as not required
+ if (modifiedSchema.properties && modifiedSchema.properties.__credentials &&
+ modifiedSchema.required && modifiedSchema.required.includes('__credentials')) {
+ modifiedSchema.required = modifiedSchema.required.filter(req => req !== '__credentials');
+ }
+
+ // For common credential parameters like api_key, make them optional too
+ if (modifiedSchema.required) {
+ toolInfo.credentialRequirements.forEach(cred => {
+ const credId = cred.id;
+ if (modifiedSchema.required.includes(credId)) {
+ modifiedSchema.required = modifiedSchema.required.filter(req => req !== credId);
+ }
+ });
+ }
+
+ // Update the description to indicate credentials are auto-injected
+ if (credentials) {
+ modifiedSchema.description = (modifiedSchema.description || '') +
+ ' (Credentials are automatically applied from your saved settings)';
+
+ // For each credential parameter, add a hint in the description
+ toolInfo.credentialRequirements.forEach(cred => {
+ const credId = cred.id;
+ if (modifiedSchema.properties[credId]) {
+ modifiedSchema.properties[credId].description =
+ '✓ Using saved credential from your settings (you don\'t need to provide this)';
+ }
+ });
+ }
+
+ // Update the modified schema
+ modifiedTool.inputSchema = modifiedSchema;
+ }
+
+ return modifiedTool;
+ }
+ }
+
+ // Return the original tool info if no changes needed
+ return toolInfo;
+ });
+
+ console.log(`Discovered tools for session ${sessionId}: ${JSON.stringify(modifiedTools.map(t => t.name))}`);
+ return modifiedTools;
};
- // Execute a tool call
+ // Execute a tool call with rate limiting
const executeToolCall = async (
sessionId: string,
toolName: string,
@@ -209,27 +330,313 @@ export function setupMcpManager(
throw new Error(`Tool ${toolName} not found`);
}
- const { client, tool } = toolInfo;
+ const { serverId } = toolInfo;
- try {
- // Execute the tool via MCP
- const result = await client.callTool({
- name: toolName,
- arguments: args,
+ // Create rate limit info for this server if it doesn't exist
+ if (!rateLimits.has(serverId)) {
+ rateLimits.set(serverId, {
+ lastRequestTime: 0,
+ requestCount: 0,
+ isProcessing: false,
+ queue: [],
});
+ }
- return result;
- } catch (error) {
- console.error(`Error executing tool ${toolName}:`, error);
- throw error;
+ const rateLimit = rateLimits.get(serverId);
+
+ // Check if we've exceeded the queue limit
+ if (rateLimit.queue.length >= RATE_LIMIT_CONFIG.maxQueueLength) {
+ return {
+ content: [
+ {
+ type: "text",
+ text: `I'm sorry, but there are too many pending requests to this server. Please try again later.`
+ }
+ ],
+ serverInfo: {
+ id: serverId,
+ name: toolInfo.serverName || serverId,
+ tool: toolName
+ }
+ };
+ }
+
+ // Add this request to the queue
+ return new Promise((resolve, reject) => {
+ rateLimit.queue.push({
+ resolve,
+ reject,
+ toolName,
+ sessionId,
+ args,
+ });
+
+ // Start processing the queue if it's not already being processed
+ processQueue(serverId);
+ });
+ };
+
+ // Process queue for a server
+ const processQueue = async (serverId: string) => {
+ const rateLimit = rateLimits.get(serverId);
+ if (!rateLimit || rateLimit.queue.length === 0 || rateLimit.isProcessing) {
+ return;
+ }
+
+ rateLimit.isProcessing = true;
+
+ try {
+ // Calculate time to wait before next request
+ const now = Date.now();
+ const timeSinceLastRequest = now - rateLimit.lastRequestTime;
+ const timeToWait = Math.max(0, RATE_LIMIT_CONFIG.minRequestSpacing - timeSinceLastRequest);
+
+ if (timeToWait > 0) {
+ await new Promise(resolve => setTimeout(resolve, timeToWait));
+ }
+
+ // Get the next request from the queue
+ const nextRequest = rateLimit.queue.shift();
+ if (!nextRequest) {
+ rateLimit.isProcessing = false;
+ return;
+ }
+
+ // Update rate limit info
+ rateLimit.lastRequestTime = Date.now();
+ rateLimit.requestCount++;
+
+ // Execute the actual tool call
+ const toolInfo = toolRegistry.getToolInfo(nextRequest.toolName);
+ if (!toolInfo) {
+ nextRequest.reject(new Error(`Tool ${nextRequest.toolName} not found`));
+ rateLimit.isProcessing = false;
+ setTimeout(() => processQueue(serverId), 0);
+ return;
+ }
+
+ const { client, tool, serverId: toolServerId } = toolInfo;
+ const maxRetries = 2;
+ let retries = 0;
+ let lastError: any = null;
+
+ while (retries <= maxRetries) {
+ try {
+ // Check if the tool requires credentials
+ const requiresCredentials = toolInfo.credentialRequirements &&
+ toolInfo.credentialRequirements.length > 0;
+
+ // Prepare args with credentials if needed
+ let callArgs = {...nextRequest.args};
+
+ if (requiresCredentials) {
+ // Get credentials from session manager
+ const credentials = sessionManager.getToolCredentials(
+ nextRequest.sessionId,
+ nextRequest.toolName,
+ toolServerId
+ );
+
+ if (credentials) {
+ console.log(`🔑 Using stored credentials for tool ${nextRequest.toolName}`);
+
+ // Apply credentials to the args
+ // Check credential requirement IDs to determine how to inject credentials
+ toolInfo.credentialRequirements?.forEach(cred => {
+ const credId = cred.id;
+ if (credentials[credId]) {
+ // Add the credential directly to args
+ console.log(`Adding credential: ${credId}`);
+ callArgs[credId] = credentials[credId];
+ }
+ });
+
+ // If the tool expects a __credentials object, create it
+ const needsCredentialsObject = tool?.inputSchema?.properties?.__credentials;
+ if (needsCredentialsObject && !callArgs.__credentials) {
+ callArgs.__credentials = {};
+ toolInfo.credentialRequirements?.forEach(cred => {
+ if (credentials[cred.id]) {
+ callArgs.__credentials[cred.id] = credentials[cred.id];
+ }
+ });
+ }
+
+ // Add a flag to tell the AI that credentials are being automatically used
+ // This helps the LLM understand that credentials are already handled
+ callArgs.__injectedCredentials = true;
+ } else {
+ console.log(`⚠️ Tool ${nextRequest.toolName} requires credentials, but none were found in session ${nextRequest.sessionId}`);
+
+ // If args don't contain credential parameters, ask the user to save credentials first
+ const missingCredentials = toolInfo.credentialRequirements?.filter(
+ cred => !callArgs[cred.id]
+ );
+
+ if (missingCredentials && missingCredentials.length > 0) {
+ // Create a user-friendly error message
+ const missingList = missingCredentials.map(cred => cred.name || cred.id).join(", ");
+ console.log(`Missing required credentials: ${missingList}`);
+
+ // Return a friendly message to the user instead of executing the tool
+ nextRequest.resolve({
+ content: [
+ {
+ type: "text",
+ text: `This tool requires the following credentials: ${missingList}. Please go to Settings > Tool Credentials to save your credentials first.`,
+ }
+ ],
+ serverInfo: {
+ id: serverId,
+ name: toolInfo.serverName || serverId,
+ tool: nextRequest.toolName
+ }
+ });
+
+ rateLimit.isProcessing = false;
+ setTimeout(() => processQueue(serverId), 0);
+ return;
+ }
+ // Otherwise continue with provided parameters
+ }
+ } else {
+ console.log(`🔧 Tool ${nextRequest.toolName} does not require credentials`);
+ }
+
+ // Execute the tool via MCP with the prepared arguments
+ console.log(`🔧 Executing tool ${nextRequest.toolName} (attempt ${retries + 1}/${maxRetries + 1})`);
+ const result = await client.callTool({
+ name: nextRequest.toolName,
+ arguments: callArgs,
+ // The timeout is set at client level already
+ });
+
+ // Add server info to the result for debugging
+ const enhancedResult = {
+ ...result,
+ content: Array.isArray(result.content)
+ ? result.content
+ : [{
+ type: "text",
+ text: `Tool result for ${nextRequest.toolName}`,
+ }],
+ serverInfo: {
+ id: serverId,
+ name: toolInfo.serverName || serverId,
+ tool: nextRequest.toolName
+ }
+ };
+
+ // For ALL tool responses, add instructions for Claude to display the result verbatim
+ // This is universal and doesn't depend on tool naming conventions
+ if (enhancedResult.content && enhancedResult.content.length > 0) {
+ // Get the original result text
+ const originalResultText = enhancedResult.content[0].text;
+
+ // Create a new enhanced content with clear instructions for Claude
+ enhancedResult.content = [
+ {
+ type: "text",
+ text: `TOOL RESULT:\n\n${originalResultText}\n\nNOTE TO CLAUDE: Display the above result exactly as shown without analysis or commentary. If this is an optimization or transformation, focus on showing the exact output.`
+ }
+ ];
+ }
+
+ console.log(`✅ Tool ${nextRequest.toolName} executed successfully after ${retries} retries`);
+
+ // Resolve the promise with the result
+ nextRequest.resolve(enhancedResult);
+ break;
+ } catch (error) {
+ lastError = error;
+ console.error(`Error executing tool ${nextRequest.toolName} (attempt ${retries + 1}/${maxRetries + 1}):`, error);
+
+ // If it's a timeout error, try again
+ if (error.code === -32001) { // This is the timeout error code
+ retries++;
+ if (retries <= maxRetries) {
+ const backoffMs = Math.min(1000 * Math.pow(2, retries), 10000); // Exponential backoff up to 10 seconds
+ console.log(`Retrying in ${backoffMs}ms...`);
+ await new Promise(resolve => setTimeout(resolve, backoffMs));
+ continue;
+ }
+ } else {
+ // For non-timeout errors, don't retry
+ break;
+ }
+ }
+ }
+
+ // If we're here and all retries failed, reject the promise
+ if (retries > maxRetries) {
+ console.error(`All ${maxRetries + 1} attempts to execute tool ${nextRequest.toolName} failed.`);
+
+ // Create a fallback response for timeout errors
+ if (lastError && lastError.code === -32001) {
+ nextRequest.resolve({
+ content: [
+ {
+ type: "text",
+ text: `I'm sorry, but I couldn't get a response from the ${nextRequest.toolName} service. The request timed out after multiple attempts. This might be due to network issues or the service being temporarily unavailable.`,
+ }
+ ]
+ });
+ } else {
+ // For other errors, reject with the last error
+ nextRequest.reject(lastError);
+ }
+ }
+ } finally {
+ // Mark as no longer processing and process the next item in the queue
+ rateLimit.isProcessing = false;
+ setTimeout(() => processQueue(serverId), 0);
}
};
// Get all available server configurations
const getAvailableServers = (): ServerConfig[] => {
+ console.log(`Available servers: ${JSON.stringify(servers.map(s => s.id))}`);
return [...servers];
};
+ // Get tools that require credentials
+ const getToolsWithCredentialRequirements = (sessionId: string): ToolCredentialInfo[] => {
+ // Re-enabled credential checking
+ console.log(`Tool credential check for session ${sessionId}`);
+
+ const tools = toolRegistry.getToolsWithCredentialRequirements();
+ console.log(`Tools with credential requirements for session ${sessionId}: ${JSON.stringify(tools.map(t => t.toolName))}`);
+ return tools;
+ };
+
+ // Set credentials for a tool
+ const setToolCredentials = async (
+ sessionId: string,
+ toolName: string,
+ serverId: string,
+ credentials: Record
+ ): Promise => {
+ console.log(`Setting credentials for tool ${toolName} from server ${serverId}`);
+ try {
+ // Store credentials in session manager only (browser session)
+ sessionManager.setToolCredentials(
+ sessionId,
+ toolName,
+ serverId,
+ credentials
+ );
+
+ console.log(`🔐 Storing credentials for tool: ${toolName}, server: ${serverId}`);
+ console.log(`🔑 Credential keys: ${JSON.stringify(Object.keys(credentials))}`);
+ console.log(`✅ Credentials stored successfully for ${toolName}`);
+
+ return true;
+ } catch (error) {
+ console.error(`Error setting credentials for tool ${toolName}:`, error);
+ return false;
+ }
+ };
+
// Clean up connections when closing
const cleanup = async (): Promise => {
for (const [serverId, client] of connectedClients.entries()) {
@@ -243,13 +650,80 @@ export function setupMcpManager(
connectedClients.clear();
};
+ // Implementation of fetchRegistryServers method
+ const fetchRegistryServers = async (): Promise => {
+ try {
+ // Check if we have valid cached data
+ if (registryCache.isValid()) {
+ console.log(`Using cached registry data (${registryCache.servers.length} servers) from ${new Date(registryCache.lastFetched).toISOString()}`);
+ return registryCache.servers;
+ }
+
+ console.log("Fetching servers from NANDA registry...");
+ const registryServers = await registryClient.getAllServers(100);
+ console.log(`Fetched ${registryServers.length} servers from registry`);
+
+ // If we got servers, update the cache
+ if (registryServers.length > 0) {
+ // Convert RegistryServer to ServerConfig
+ const serverConfigs: ServerConfig[] = registryServers.map(server => ({
+ id: server.id,
+ name: server.name,
+ url: server.url,
+ description: server.description,
+ types: server.types,
+ tags: server.tags,
+ verified: server.verified,
+ rating: server.rating
+ }));
+
+ // Update the cache
+ registryCache.update(serverConfigs);
+
+ return serverConfigs;
+ } else if (registryCache.servers.length > 0) {
+ // If new fetch returned empty but we have cached data, use the cache even if expired
+ console.log(`Registry API returned 0 servers. Using older cached data (${registryCache.servers.length} servers)`);
+ return registryCache.servers;
+ }
+
+ return [];
+ } catch (error) {
+ console.error("Error fetching servers from registry:", error);
+
+ // If there's an error but we have cached data, use it (even if expired)
+ if (registryCache.servers.length > 0) {
+ console.log(`Error fetching from registry. Using cached data (${registryCache.servers.length} servers)`);
+ return registryCache.servers;
+ }
+
+ return [];
+ }
+ };
+
+ // Disable auto-registration process - rely on client registrations instead
+ // const autoRegisterServers = async () => {
+ // console.log(`Auto-registering ${servers.length} servers from storage...`);
+ // for (const server of servers) {
+ // await registerServer(server);
+ // }
+ // };
+
+ // // Start auto-registration process in the background
+ // autoRegisterServers().catch(error => {
+ // console.error("Error auto-registering servers:", error);
+ // });
+
// Return the MCP manager interface
return {
discoverTools,
executeToolCall,
registerServer,
- fetchRegistryServers,
getAvailableServers,
+ getToolsWithCredentialRequirements,
+ setToolCredentials,
cleanup,
+ getSessionManager: () => sessionManager,
+ fetchRegistryServers
};
}
diff --git a/server/src/mcp/sessionManager.ts b/server/src/mcp/sessionManager.ts
index c79c53e..3412394 100644
--- a/server/src/mcp/sessionManager.ts
+++ b/server/src/mcp/sessionManager.ts
@@ -1,15 +1,24 @@
// server/src/mcp/sessionManager.ts
import { v4 as uuidv4 } from "uuid";
+import crypto from "crypto";
+
+interface ToolCredential {
+ toolName: string;
+ serverId: string;
+ data: string; // Encrypted credentials
+}
interface Session {
id: string;
anthropicApiKey?: string;
+ credentials: ToolCredential[];
createdAt: Date;
lastActive: Date;
}
export class SessionManager {
private sessions: Map = new Map();
+ private encryptionKey: Buffer;
// Session cleanup interval in milliseconds (1 hour)
private readonly CLEANUP_INTERVAL = 60 * 60 * 1000;
@@ -17,6 +26,13 @@ export class SessionManager {
constructor() {
// Set up session cleanup
setInterval(() => this.cleanupSessions(), this.CLEANUP_INTERVAL);
+
+ // Generate or load encryption key (in production, this should be loaded from a secure source)
+ this.encryptionKey = Buffer.from(
+ process.env.CREDENTIAL_ENCRYPTION_KEY ||
+ crypto.randomBytes(32).toString('hex'),
+ 'hex'
+ );
}
createSession(): string {
@@ -25,6 +41,7 @@ export class SessionManager {
this.sessions.set(sessionId, {
id: sessionId,
+ credentials: [],
createdAt: now,
lastActive: now,
});
@@ -32,15 +49,31 @@ export class SessionManager {
return sessionId;
}
- getSession(sessionId: string): Session | undefined {
- const session = this.sessions.get(sessionId);
-
+ // Get an existing session or create a new one if it doesn't exist
+ getOrCreateSession(sessionId: string): Session {
+ // Try to get existing session first
+ let session = this.sessions.get(sessionId);
+
+ // If session exists, update last active time and return it
if (session) {
- // Update last active time
+ console.log(`Using existing session: ${sessionId}`);
session.lastActive = new Date();
this.sessions.set(sessionId, session);
+ return session;
}
-
+
+ // If session doesn't exist, create a new one with the requested ID
+ console.log(`Creating new session with provided ID: ${sessionId}`);
+ const now = new Date();
+
+ session = {
+ id: sessionId,
+ credentials: [],
+ createdAt: now,
+ lastActive: now,
+ };
+
+ this.sessions.set(sessionId, session);
return session;
}
@@ -58,6 +91,114 @@ export class SessionManager {
return this.sessions.get(sessionId)?.anthropicApiKey;
}
+ // Store credentials for a tool
+ setToolCredentials(
+ sessionId: string,
+ toolName: string,
+ serverId: string,
+ credentials: Record
+ ): void {
+ const session = this.sessions.get(sessionId);
+ if (!session) {
+ console.error(`❌ Cannot store credentials: Session ${sessionId} not found`);
+ return;
+ }
+
+ // Log the credentials being stored (key names only for security)
+ console.log(`🔐 Storing credentials for tool: ${toolName}, server: ${serverId}`);
+ console.log(`🔑 Credential keys: ${JSON.stringify(Object.keys(credentials))}`);
+
+ // Remove any existing credentials for this tool
+ session.credentials = session.credentials.filter(
+ cred => !(cred.toolName === toolName && cred.serverId === serverId)
+ );
+
+ // Encrypt the credentials
+ const encrypted = this.encryptData(JSON.stringify(credentials));
+
+ // Add the new credentials
+ session.credentials.push({
+ toolName,
+ serverId,
+ data: encrypted
+ });
+
+ console.log(`✅ Credentials stored successfully for ${toolName}`);
+
+ session.lastActive = new Date();
+ this.sessions.set(sessionId, session);
+ }
+
+ // Get credentials for a tool
+ getToolCredentials(
+ sessionId: string,
+ toolName: string,
+ serverId: string
+ ): Record | null {
+ const session = this.sessions.get(sessionId);
+ if (!session) {
+ console.log(`❌ Cannot retrieve credentials: Session ${sessionId} not found`);
+ return null;
+ }
+
+ const credential = session.credentials.find(
+ cred => cred.toolName === toolName && cred.serverId === serverId
+ );
+
+ if (!credential) {
+ console.log(`❌ No credentials found for tool ${toolName}, server ${serverId}`);
+ return null;
+ }
+
+ try {
+ // Decrypt the credentials
+ const decrypted = this.decryptData(credential.data);
+ const parsedCredentials = JSON.parse(decrypted);
+
+ console.log(`✅ Credentials retrieved successfully for ${toolName}, server ${serverId}`);
+ console.log(`🔑 Retrieved credential keys: ${JSON.stringify(Object.keys(parsedCredentials))}`);
+
+ return parsedCredentials;
+ } catch (error) {
+ console.error(`Error decrypting credentials for tool ${toolName}:`, error);
+ return null;
+ }
+ }
+
+ // Encrypt data using AES-256-GCM
+ private encryptData(data: string): string {
+ const iv = crypto.randomBytes(16);
+ const cipher = crypto.createCipheriv('aes-256-gcm', this.encryptionKey, iv);
+
+ const encrypted = Buffer.concat([
+ cipher.update(data, 'utf8'),
+ cipher.final()
+ ]);
+
+ const authTag = cipher.getAuthTag();
+
+ // Return IV + AuthTag + Encrypted data as base64 string
+ return Buffer.concat([iv, authTag, encrypted]).toString('base64');
+ }
+
+ // Decrypt data using AES-256-GCM
+ private decryptData(encryptedBase64: string): string {
+ const encryptedBuffer = Buffer.from(encryptedBase64, 'base64');
+
+ // Extract IV, AuthTag, and encrypted data
+ const iv = encryptedBuffer.subarray(0, 16);
+ const authTag = encryptedBuffer.subarray(16, 32);
+ const encrypted = encryptedBuffer.subarray(32);
+
+ const decipher = crypto.createDecipheriv('aes-256-gcm', this.encryptionKey, iv);
+ decipher.setAuthTag(authTag);
+
+ return Buffer.concat([
+ decipher.update(encrypted),
+ decipher.final()
+ ]).toString('utf8');
+ }
+
private cleanupSessions(): void {
const now = new Date();
const SESSION_TIMEOUT = 24 * 60 * 60 * 1000; // 24 hours
@@ -70,4 +211,16 @@ export class SessionManager {
}
}
}
+
+ 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;
+ }
}
diff --git a/server/src/mcp/toolRegistry.ts b/server/src/mcp/toolRegistry.ts
index 88adc13..91cec68 100644
--- a/server/src/mcp/toolRegistry.ts
+++ b/server/src/mcp/toolRegistry.ts
@@ -2,22 +2,105 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { Tool } from "@modelcontextprotocol/sdk/types.js";
+// Local declarations instead of importing from shared
+interface CredentialRequirement {
+ id: string;
+ name: string;
+ description?: string;
+ acquisition?: {
+ url?: string;
+ instructions?: string;
+ };
+}
+
+interface SharedToolInfo {
+ name: string;
+ description?: string;
+ inputSchema: any;
+ credentialRequirements?: CredentialRequirement[];
+ serverId?: string;
+ serverName?: string;
+}
+
interface ToolInfo {
serverId: string;
+ serverName: string;
client: Client;
tool: Tool;
+ credentialRequirements?: CredentialRequirement[];
+ rating?: number;
}
export class ToolRegistry {
private tools: Map = new Map();
- registerTools(serverId: string, client: Client, tools: Tool[]): void {
+ registerTools(serverId: string, serverName: string, rating: number, client: Client, tools: Tool[]): void {
+ console.log(`ToolRegistry: Registering ${tools.length} tools from server ID: ${serverId}`);
+
for (const tool of tools) {
+ console.log(`ToolRegistry: Processing tool: ${tool.name}`);
+
+ // Restore credential requirements extraction
+ let credentialRequirements: CredentialRequirement[] = [];
+
+ // Extract credential requirements from the tool's input schema if available
+ if (tool.inputSchema && typeof tool.inputSchema === 'object') {
+ // Case 1: Check for __credentials object (traditional format)
+ if (tool.inputSchema.properties &&
+ typeof tool.inputSchema.properties === 'object' &&
+ tool.inputSchema.properties.__credentials) {
+
+ const credentialsSchema = tool.inputSchema.properties.__credentials as any;
+
+ if (credentialsSchema.properties && typeof credentialsSchema.properties === 'object') {
+ const credProps = credentialsSchema.properties;
+
+ credentialRequirements = Object.entries(credProps).map(([id, schema]) => ({
+ id,
+ name: (schema as any).title || id,
+ description: (schema as any).description || '',
+ acquisition: (schema as any).acquisition || {}
+ }));
+
+ console.log(`ToolRegistry: Found ${credentialRequirements.length} credential requirements in __credentials for tool ${tool.name}`);
+ }
+ }
+ // Case 2: Check for common credential parameter names directly in properties
+ else if (tool.inputSchema.properties && typeof tool.inputSchema.properties === 'object') {
+ const commonCredentialNames = [
+ 'api_key', 'apiKey', 'apikey', 'key', 'token', 'access_token',
+ 'accessToken', 'auth_token', 'authToken', 'password', 'secret',
+ 'client_id', 'clientId', 'client_secret', 'clientSecret'
+ ];
+
+ for (const credName of commonCredentialNames) {
+ if (credName in tool.inputSchema.properties) {
+ const schema = tool.inputSchema.properties[credName] as any;
+
+ credentialRequirements.push({
+ id: credName,
+ name: schema.title || `${credName.charAt(0).toUpperCase() + credName.slice(1).replace(/_/g, ' ')}`,
+ description: schema.description || `${credName} required for authentication`,
+ acquisition: schema.acquisition || {}
+ });
+
+ console.log(`ToolRegistry: Found direct credential parameter '${credName}' for tool ${tool.name}`);
+ }
+ }
+ }
+ }
+
+ // Register the tool with credential requirements if any
this.tools.set(tool.name, {
serverId,
+ serverName,
client,
tool,
+ credentialRequirements: credentialRequirements || [],
+ rating,
});
+
+ console.log(`ToolRegistry: Registered tool ${tool.name} with ${credentialRequirements.length} credential requirements`);
}
}
@@ -25,14 +108,54 @@ export class ToolRegistry {
return this.tools.get(toolName);
}
- getAllTools(): Tool[] {
- return Array.from(this.tools.values()).map((info) => info.tool);
+ getAllTools(): SharedToolInfo[] {
+ const tools = Array.from(this.tools.values()).map((info) => ({
+ name: info.tool.name,
+ description: info.tool.description,
+ inputSchema: info.tool.inputSchema,
+ credentialRequirements: info.credentialRequirements,
+ serverId: info.serverId,
+ serverName: info.serverName,
+ rating: info.rating ?? 0
+ }));
+
+ console.log(`ToolRegistry: getAllTools returning ${tools.length} tools`);
+ return tools;
}
- getToolsByServerId(serverId: string): Tool[] {
+ getToolsByServerId(serverId: string): SharedToolInfo[] {
return Array.from(this.tools.values())
.filter((info) => info.serverId === serverId)
- .map((info) => info.tool);
+ .map((info) => ({
+ name: info.tool.name,
+ description: info.tool.description,
+ inputSchema: info.tool.inputSchema,
+ credentialRequirements: info.credentialRequirements,
+ serverId: info.serverId,
+ serverName: info.serverName
+ }));
+ }
+
+ getToolsWithCredentialRequirements(): {
+ toolName: string;
+ serverName: string;
+ serverId: string;
+ credentials: CredentialRequirement[];
+ }[] {
+ // Restored credential requirements functionality
+ console.log(`ToolRegistry: getToolsWithCredentialRequirements: checking for tools with credential requirements`);
+
+ const toolsWithCredentials = Array.from(this.tools.values())
+ .filter((info) => info.credentialRequirements && info.credentialRequirements.length > 0)
+ .map((info) => ({
+ toolName: info.tool.name,
+ serverName: info.serverName,
+ serverId: info.serverId,
+ credentials: info.credentialRequirements || []
+ }));
+
+ console.log(`ToolRegistry: Found ${toolsWithCredentials.length} tools requiring credentials`);
+ return toolsWithCredentials;
}
removeToolsByServerId(serverId: string): void {
diff --git a/server/src/mcp/types.ts b/server/src/mcp/types.ts
new file mode 100644
index 0000000..275026f
--- /dev/null
+++ b/server/src/mcp/types.ts
@@ -0,0 +1,70 @@
+// server/src/mcp/types.ts
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import { Tool } from "@modelcontextprotocol/sdk/types.js";
+
+export interface ToolInfo {
+ name: string;
+ description?: string;
+ inputSchema: any;
+ credentialRequirements?: CredentialRequirement[];
+ serverId?: string;
+ rating?: number;
+}
+
+export interface CredentialRequirement {
+ id: string;
+ name: string;
+ description?: string;
+ acquisition?: {
+ url?: string;
+ instructions?: string;
+ };
+}
+
+export interface ToolCredentialInfo {
+ toolName: string;
+ serverName: string;
+ serverId: string;
+ credentials: CredentialRequirement[];
+}
+
+export interface ToolCall {
+ name: string;
+ arguments: any;
+}
+
+export interface ToolResult {
+ content: Array<{
+ type: string;
+ text?: string;
+ [key: string]: any;
+ }>;
+}
+
+export interface McpManager {
+ discoverTools: (sessionId: string) => Promise;
+ executeToolCall: (
+ sessionId: string,
+ toolName: string,
+ args: any
+ ) => Promise;
+ registerServer: (serverConfig: ServerConfig) => Promise;
+ getAvailableServers: () => ServerConfig[];
+ getToolsWithCredentialRequirements: (sessionId: string) => ToolCredentialInfo[];
+ setToolCredentials: (
+ sessionId: string,
+ toolName: string,
+ serverId: string,
+ credentials: Record
+ ) => Promise;
+ cleanup: () => Promise;
+ getSessionManager: () => any;
+ fetchRegistryServers: () => Promise;
+}
+
+export interface ServerConfig {
+ id: string;
+ name: string;
+ url: string;
+ rating?: number;
+}
\ No newline at end of file
diff --git a/server/src/registry/client.ts b/server/src/registry/client.ts
index e3cf94d..2bf728a 100644
--- a/server/src/registry/client.ts
+++ b/server/src/registry/client.ts
@@ -1,6 +1,6 @@
// server/src/registry/client.ts
import axios from "axios";
-import { ServerConfig } from "../mcp/manager.js";
+import { ServerConfig } from "../mcp/types.js";
interface RegistryConfig {
url: string;
@@ -20,74 +20,311 @@ interface RegistryServerResponse {
logo_url: string;
}
+// Interface for server configurations
+export interface RegistryServer {
+ id: string;
+ name: string;
+ url: string;
+ description?: string;
+ types?: string[];
+ tags?: string[];
+ verified?: boolean;
+ rating?: number;
+}
+
+// Global rate limit state
+const globalRateLimitState = {
+ isRateLimited: false,
+ rateLimitResetTime: 0,
+ rateLimitBackoff: 5 * 60 * 1000, // 5 minutes initial backoff
+ consecutiveErrors: 0
+};
+
+// Helper function to implement retry with exponential backoff
+async function retryWithBackoff(
+ fn: () => Promise,
+ retries = 3,
+ initialDelayMs = 1000
+): Promise {
+ // Check if we're globally rate limited
+ if (globalRateLimitState.isRateLimited && Date.now() < globalRateLimitState.rateLimitResetTime) {
+ console.warn(`NANDA Registry API is currently rate limited. Waiting until ${new Date(globalRateLimitState.rateLimitResetTime).toISOString()}`);
+ throw new Error(`Rate limited by NANDA Registry API until ${new Date(globalRateLimitState.rateLimitResetTime).toISOString()}`);
+ }
+
+ let currentDelay = initialDelayMs;
+ let lastError: any;
+
+ for (let attempt = 0; attempt <= retries; attempt++) {
+ try {
+ // If not the first attempt, log the retry
+ if (attempt > 0) {
+ console.log(`Retry attempt ${attempt}/${retries} after ${currentDelay}ms delay...`);
+ }
+
+ const result = await fn();
+
+ // Reset consecutive errors on success
+ if (globalRateLimitState.consecutiveErrors > 0) {
+ globalRateLimitState.consecutiveErrors = 0;
+ }
+
+ return result;
+ } catch (error) {
+ lastError = error;
+
+ // If this is a rate limit error (429)
+ if (axios.isAxiosError(error) && error.response?.status === 429) {
+ // Set global rate limit with exponential backoff
+ globalRateLimitState.isRateLimited = true;
+ globalRateLimitState.consecutiveErrors++;
+
+ // Get retry-after header or use exponential backoff
+ let retryAfter = 0;
+ if (error.response.headers['retry-after']) {
+ retryAfter = parseInt(error.response.headers['retry-after']) * 1000;
+ } else {
+ // Increase backoff time exponentially with consecutive errors (max 1 hour)
+ retryAfter = Math.min(
+ globalRateLimitState.rateLimitBackoff * Math.pow(2, globalRateLimitState.consecutiveErrors - 1),
+ 60 * 60 * 1000
+ );
+ }
+
+ globalRateLimitState.rateLimitResetTime = Date.now() + retryAfter;
+ console.warn(`Rate limited by NANDA Registry API. Backing off for ${retryAfter/1000} seconds until ${new Date(globalRateLimitState.rateLimitResetTime).toISOString()}`);
+
+ // If we have retries left
+ if (attempt < retries) {
+ // Wait for the current delay
+ await new Promise(resolve => setTimeout(resolve, currentDelay));
+
+ // Exponential backoff - double the delay for next attempt
+ currentDelay *= 2;
+
+ continue;
+ }
+ }
+ // If this is a server error (5xx)
+ else if (axios.isAxiosError(error) && error.response?.status && error.response.status >= 500) {
+ // If we have retries left
+ if (attempt < retries) {
+ console.warn(`Request failed with status ${error.response.status}, retrying...`);
+
+ // Wait for the current delay
+ await new Promise(resolve => setTimeout(resolve, currentDelay));
+
+ // Exponential backoff - double the delay for next attempt
+ currentDelay *= 2;
+
+ continue;
+ }
+ }
+
+ // If we're out of retries or it's not a retryable error, throw
+ throw lastError;
+ }
+ }
+
+ // This should never happen, but TypeScript wants a return value
+ throw lastError;
+}
+
export class RegistryClient {
private baseUrl: string;
private apiKey?: string;
- constructor(config: RegistryConfig) {
- this.baseUrl = config.url;
- this.apiKey = config.apiKey;
+ constructor(baseUrl: string = 'https://nanda-registry.com', apiKey?: string) {
+ this.baseUrl = baseUrl;
+ this.apiKey = apiKey;
}
/**
- * Fetches available MCP servers from the registry
+ * Get popular servers from the registry
*/
- async getServers(): Promise {
+ async getPopularServers(limit: number = 50): Promise {
+ try {
+ console.log(`Fetching popular servers from registry with limit ${limit}`);
+
+ const fetchPopular = async () => {
+ const response = await axios.get(`${this.baseUrl}/api/v1/discovery/popular/`, {
+ params: { limit },
+ headers: this.apiKey ? { 'Authorization': `Bearer ${this.apiKey}` } : {}
+ });
+ return response.data;
+ };
+
+ // Use retry with backoff
+ const data = await retryWithBackoff(fetchPopular, 3, 1000);
+
+ console.log(`Registry returned data structure:`, Object.keys(data));
+ return this.processServerResponse(data);
+ } catch (error) {
+ console.error('Error fetching popular servers from registry:', error);
+ if (axios.isAxiosError(error)) {
+ console.error('Response status:', error.response?.status);
+ console.error('Response headers:', error.response?.headers);
+ if (error.response?.data) {
+ console.error('Response data:', JSON.stringify(error.response.data).substring(0, 500));
+ }
+ }
+ return [];
+ }
+ }
+
+ async getAllServers(limit: number = 100): Promise {
try {
+ console.log("📡 Fetching all servers from /api/v1/servers/");
+
const headers: Record = {};
if (this.apiKey) {
headers["Authorization"] = `Bearer ${this.apiKey}`;
}
+
+ const fetchServers = async () => {
+ const response = await axios.get(`${this.baseUrl}/api/v1/servers/`, {
+ headers,
+ params: { limit },
+ });
+ return response;
+ };
+
+ // Use retry with backoff
+ const response = await retryWithBackoff(fetchServers, 3, 1000);
+
+ const servers = response.data?.data || [];
+ console.log(`/servers/ returned ${servers.length} servers`);
+ return this.processServerResponse(servers);
+ } catch (error) {
+ console.error("Error fetching from /servers/ endpoint, falling back to /popular:", error);
+ return [];
+ }
+ }
- const response = await axios.get(`${this.baseUrl}/api/v1/servers/`, {
- headers,
+ /**
+ * Search for servers in the registry
+ */
+ async searchServers(query: string, options: {
+ limit?: number,
+ page?: number,
+ tags?: string,
+ type?: string,
+ verified?: boolean
+ } = {}): Promise {
+ try {
+ console.log(`Searching registry for "${query}" with options:`, options);
+ const response = await axios.get(`${this.baseUrl}/api/v1/discovery/search/`, {
params: {
- // We can add params here for filtering if needed
- // verified: true,
- // types: 'tool',
+ q: query,
+ limit: options.limit || 50,
+ page: options.page || 1,
+ tags: options.tags,
+ type: options.type,
+ verified: options.verified
},
+ headers: this.apiKey ? { 'Authorization': `Bearer ${this.apiKey}` } : {}
});
+
+ console.log(`Registry search returned data structure:`, Object.keys(response.data));
+ return this.processServerResponse(response.data);
+ } catch (error) {
+ console.error('Error searching servers in registry:', error);
+ if (axios.isAxiosError(error)) {
+ console.error('Response status:', error.response?.status);
+ console.error('Response headers:', error.response?.headers);
+ if (error.response?.data) {
+ console.error('Response data:', JSON.stringify(error.response.data).substring(0, 500));
+ }
+ }
+ return [];
+ }
+ }
- console.log("Response from registry:", response.data.data);
- // Extract server information from the response
- const servers = response.data.data || [];
+ //alyssa
+ async getServers(query?: string, options: any = {}): Promise {
+ if (query) {
+ const results = await this.searchServers(query, options);
+ if (results.length > 0) {
+ return results;
+ }
+ }
+
+ // Try full /servers endpoint first
+ const all = await this.getAllServers(options.limit);
+ if (all.length > 0) return all;
+
+ // Fall back to /popular if needed
+ return this.getPopularServers(options.limit);
+ }
- // Filter to include only servers with valid SSE URLs
- // Exclude GitHub URLs as they're likely just source code repos
- return servers
- .filter((server: RegistryServerResponse) => {
- return (
- server.url &&
- server.url.trim() !== "" &&
- !server.url.includes("github.com") &&
- !server.url.includes("gitlab.com")
- );
- })
- .map((server: RegistryServerResponse) => {
- let url = server.url;
- if (url.endsWith("/")) {
- url = url.slice(0, -1);
- }
- // remove sse if it is present in the url
- if (url.endsWith("/sse")) {
- url = url.slice(0, -4);
- }
- return {
- id: server.id,
- name: server.name,
- url: url + "/sse",
- description: server.description,
- types: server.types,
- tags: server.tags,
- verified: server.verified,
- rating: server.rating,
- };
- });
- } catch (error) {
- console.error("Error fetching servers from registry:", error);
+ /**
+ * Process and format server response from registry
+ */
+ private processServerResponse(data: any): RegistryServer[] {
+ console.log("Processing server response");
+
+ // Check if data is empty
+ if (!data) {
+ console.warn('Empty response from registry');
return [];
}
+
+ // Check if the response has a data property (actual response format)
+ if (data.data && Array.isArray(data.data)) {
+ console.log(`Found ${data.data.length} servers in data property`);
+ return data.data
+ .filter(server => server && server.id && server.name && server.url)
+ .map(this.formatServerData);
+ }
+
+ // Check for pagination structure in search results
+ if (data.pagination && data.data && Array.isArray(data.data)) {
+ console.log(`Found ${data.data.length} servers in paginated data`);
+ return data.data
+ .filter(server => server && server.id && server.name && server.url)
+ .map(this.formatServerData);
+ }
+
+ // Check if the response is already an array
+ if (Array.isArray(data)) {
+ console.log(`Found ${data.length} servers in direct array`);
+ return data
+ .filter(server => server && server.id && server.name && server.url)
+ .map(this.formatServerData);
+ }
+
+ // If we can't identify the structure, log and return empty array
+ console.warn('Unknown response format from registry:', JSON.stringify(data).substring(0, 200));
+ return [];
+ }
+
+ /**
+ * Format server data consistently
+ */
+ private formatServerData = (server: any): RegistryServer => {
+ let url = server.url;
+
+ // Clean URL (remove trailing slashes)
+ if (url && url.endsWith('/')) {
+ url = url.slice(0, -1);
+ }
+
+ // Ensure the URL has /sse suffix for MCP compatibility
+ if (url && !url.endsWith('/sse')) {
+ url = `${url}/sse`;
+ }
+
+ return {
+ id: server.id,
+ name: server.name,
+ url,
+ description: server.description,
+ types: server.types,
+ tags: server.tags,
+ verified: server.verified,
+ rating: server.rating
+ };
}
/**
diff --git a/server/src/routes.ts b/server/src/routes.ts
index 23e9939..64f6d19 100644
--- a/server/src/routes.ts
+++ b/server/src/routes.ts
@@ -1,18 +1,158 @@
// server/src/routes.ts
+import axios from "axios";
import { Express, Request, Response } from "express";
import Anthropic from "@anthropic-ai/sdk";
import { McpManager } from "./mcp/manager.js";
+import { RegistryClient } from "./registry/client.js";
+
+// Add a helper function to sanitize input schemas for Anthropic
+function sanitizeInputSchema(schema: any): any {
+ if (!schema || typeof schema !== 'object') {
+ return schema;
+ }
+
+ // Create a copy of the schema
+ const sanitizedSchema = { ...schema };
+
+ // Remove oneOf, allOf, anyOf at the top level
+ delete sanitizedSchema.oneOf;
+ delete sanitizedSchema.allOf;
+ delete sanitizedSchema.anyOf;
+
+ // If we removed these operators, provide a basic schema structure
+ // This ensures we don't send an empty schema
+ if (Object.keys(sanitizedSchema).length === 0 ||
+ (schema.oneOf !== undefined || schema.allOf !== undefined || schema.anyOf !== undefined)) {
+ return {
+ type: "object",
+ properties: {},
+ description: schema.description || "Input for this tool"
+ };
+ }
+
+ return sanitizedSchema;
+}
+
+// Cache for server ratings
+const ratingsCache = new Map();
+const RATINGS_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours (increased from 30 minutes)
+
+// Track rate limit status to avoid hitting limits repeatedly
+const ratingApiState = {
+ isRateLimited: false,
+ rateLimitResetTime: 0,
+ rateLimitBackoff: 5 * 60 * 1000, // 5 minutes initial backoff
+ consecutiveErrors: 0
+};
+
+async function getWeightedRatingScore(serverId: string): Promise<{ average: number, count: number, score: number }> {
+ try {
+ // Check if we're currently rate limited
+ if (ratingApiState.isRateLimited && Date.now() < ratingApiState.rateLimitResetTime) {
+ console.log(`Rating API is rate limited, waiting until ${new Date(ratingApiState.rateLimitResetTime).toISOString()}`);
+
+ // Use cached data if available
+ const cached = ratingsCache.get(serverId);
+ if (cached) {
+ console.log(`Using cached ratings for server ${serverId} due to rate limiting`);
+ return cached.data;
+ }
+
+ // If no cache, return default values during rate limit
+ return { average: 0, count: 0, score: 0 };
+ }
+
+ // Check if we have cached data that's not expired
+ const cached = ratingsCache.get(serverId);
+ const now = Date.now();
+
+ if (cached && (now - cached.timestamp) < RATINGS_CACHE_TTL) {
+ console.log(`Using cached ratings for server ${serverId}`);
+ return cached.data;
+ }
+
+ // If not in cache or expired, fetch from API
+ console.log(`Fetching ratings for server ${serverId}`);
+ const response = await axios.get(`https://nanda-registry.com/api/v1/servers/${serverId}/ratings`);
+
+ // Reset rate limit state on successful request
+ ratingApiState.isRateLimited = false;
+ ratingApiState.consecutiveErrors = 0;
+
+ const ratings = response.data?.data || [];
+
+ const count = ratings.length;
+ const total = ratings.reduce((sum: number, r: any) => sum + r.rating, 0);
+ const average = count > 0 ? total / count : 0;
+ const score = average * count;
+
+ const result = { average, count, score };
+
+ // Cache the result
+ ratingsCache.set(serverId, {
+ data: result,
+ timestamp: now
+ });
+
+ return result;
+ } catch (error) {
+ console.error(`Failed to fetch ratings for server ${serverId}:`, error);
+
+ // Check for rate limit error (429)
+ if (axios.isAxiosError(error) && error.response?.status === 429) {
+ // Set rate limit state with exponential backoff
+ ratingApiState.isRateLimited = true;
+ ratingApiState.consecutiveErrors++;
+
+ // Increase backoff time exponentially with consecutive errors (max 1 hour)
+ const backoffTime = Math.min(
+ ratingApiState.rateLimitBackoff * Math.pow(2, ratingApiState.consecutiveErrors - 1),
+ 60 * 60 * 1000
+ );
+
+ ratingApiState.rateLimitResetTime = Date.now() + backoffTime;
+ console.warn(`Rate limited by ratings API. Backing off for ${backoffTime/1000} seconds until ${new Date(ratingApiState.rateLimitResetTime).toISOString()}`);
+ }
+
+ // If we have cached data, use it even if expired
+ const cached = ratingsCache.get(serverId);
+ if (cached) {
+ console.log(`Using expired cached ratings for server ${serverId} due to fetch error`);
+ return cached.data;
+ }
+
+ // Default values if no cached data available
+ return { average: 0, count: 0, score: 0 };
+ }
+}
export function setupRoutes(app: Express, mcpManager: McpManager): void {
+ // Health check endpoint for deployment
+ app.get("/api/healthcheck", (req: Request, res: Response) => {
+ res.status(200).json({ status: "ok" });
+ });
+
// Session endpoint
app.post("/api/session", (req: Request, res: Response) => {
- // We would use the session manager here to create a new session
- // For simplicity, we're using a dummy session ID
- res.json({ sessionId: "12345" });
+ console.log("API: /api/session called");
+ // Create a real session using sessionManager
+ const sessionManager = mcpManager.getSessionManager();
+ if (!sessionManager) {
+ console.error("Cannot create session: SessionManager not available");
+ return res.status(500).json({ error: "Session manager not available" });
+ }
+
+ const sessionId = sessionManager.createSession();
+ console.log(`Created new session with ID: ${sessionId}`);
+ res.json({ sessionId });
});
// API key endpoint
app.post("/api/settings/apikey", (req: Request, res: Response) => {
+ console.log("API: /api/settings/apikey called");
const { apiKey } = req.body;
if (!apiKey) {
@@ -24,10 +164,32 @@ export function setupRoutes(app: Express, mcpManager: McpManager): void {
res.json({ success: true });
});
- // Chat completion endpoint
+ // Helper function to ensure session exists
+ const ensureSession = (sessionId: string): string => {
+ if (!sessionId) {
+ console.log("No session ID provided, creating new session");
+ return mcpManager.getSessionManager().createSession();
+ }
+
+ // Use getOrCreateSession to handle the session
+ mcpManager.getSessionManager().getOrCreateSession(sessionId);
+ return sessionId;
+ };
+
+ // Update the chat completion endpoint to ensure session
app.post("/api/chat/completions", async (req: Request, res: Response) => {
- const { messages, tools = true } = req.body;
+ console.log("API: /api/chat/completions called");
+ const { messages, tools = true, auto_proceed = true } = req.body;
const apiKey = req.headers["x-api-key"] as string;
+ const rawSessionId = (req.headers["x-session-id"] as string) || "";
+
+ // Ensure we have a valid session
+ const sessionId = ensureSession(rawSessionId);
+
+ // If the session ID changed, let the client know
+ if (sessionId !== rawSessionId) {
+ console.log(`Using new session ID: ${sessionId} (original was: ${rawSessionId || "empty"})`);
+ }
if (!apiKey) {
return res.status(401).json({ error: "API key is required" });
@@ -38,18 +200,58 @@ export function setupRoutes(app: Express, mcpManager: McpManager): void {
apiKey,
});
+ //Mapping ratings to natural langauge
+ const ratingTextMap = {
+ 1: "terrible",
+ 2: "poorly rated",
+ 3: "average",
+ 4: "good",
+ 5: "excellent",
+ };
+
// Fetch available tools if enabled
let availableTools = [];
if (tools) {
try {
- const sessionId = (req.headers["x-session-id"] as string) || "12345";
const discoveredTools = await mcpManager.discoverTools(sessionId);
- availableTools = discoveredTools.map((tool) => ({
- name: tool.name,
- description: tool.description || "",
- input_schema: tool.inputSchema,
- }));
+ // Use existing rating from tool info instead of fetching for every tool
+ availableTools = discoveredTools.map((tool) => {
+ // Use the server rating that's already included in the tool info
+ const rating = tool.rating || 0;
+ const ratingLabel = ratingTextMap[Math.round(rating) || 0] || "unrated";
+
+ const enhancedDescription = `${tool.description || ""}
+ (This tool runs on a ${ratingLabel} server with a ${rating.toFixed(1)}/5 rating.)`;
+
+ // Sanitize the input schema before passing it to Anthropic
+ const sanitizedInputSchema = sanitizeInputSchema(tool.inputSchema);
+
+ return {
+ name: tool.name,
+ description: enhancedDescription,
+ input_schema: sanitizedInputSchema,
+ score: rating, // Use simple rating for sorting
+ };
+ });
+
+ // Sort by rating descending
+ availableTools.sort((a, b) => (b.score || 0) - (a.score || 0));
+
+ // Remove score field before sending to Claude
+ availableTools = availableTools.map(({ score, ...tool }) => tool);
+
+ // Preparing Claude to prefer higher rated tools
+ messages.unshift({
+ role: "user",
+ content: [
+ {
+ type: "text",
+ text: "Hello! I'm here to help you with tools from various servers. When suggesting tools, I'll consider their ratings to provide you with the most reliable options. Tools with higher ratings are generally more trusted by the community.",
+ },
+ ],
+ });
+
} catch (error) {
console.error("Error discovering tools:", error);
// Continue without tools if there's an error
@@ -58,8 +260,8 @@ export function setupRoutes(app: Express, mcpManager: McpManager): void {
// Create completion request
const completion = await anthropic.messages.create({
- model: "claude-3-7-sonnet-20250219",
- max_tokens: 10000,
+ model: "claude-3-5-sonnet-20241022",
+ max_tokens: 4000,
messages,
tools: availableTools.length > 0 ? availableTools : undefined,
});
@@ -67,28 +269,45 @@ export function setupRoutes(app: Express, mcpManager: McpManager): void {
// Process tool calls if present
const finalMessages = [...messages];
let finalResponse = completion;
+ let serverUsed = null;
+ let intermediateResponses = [];
// Check if there are any tool calls in the response
const toolUses = completion.content.filter((c) => c.type === "tool_use");
- if (toolUses.length > 0) {
- // Add the assistant's response with tool calls
+ if (toolUses.length > 0 && auto_proceed) {
+ // Add the assistant's initial response with tool calls
finalMessages.push({
role: "assistant",
- content: completion.content,
+ content: completion.content
+ });
+
+ // Add the initial response to intermediate responses
+ intermediateResponses.push({
+ role: "assistant",
+ content: completion.content.filter(c => c.type === "text"),
+ timestamp: new Date(),
});
// Process each tool call
for (const toolUse of toolUses) {
try {
- const sessionId =
- (req.headers["x-session-id"] as string) || "12345";
+ console.log(`Executing tool call: ${toolUse.name} with input:`, JSON.stringify(toolUse.input));
+
+ // Execute the tool and get the server that was used
const result = await mcpManager.executeToolCall(
sessionId,
toolUse.name,
toolUse.input
);
+
+ // Capture server info if available
+ if (result.serverInfo) {
+ serverUsed = result.serverInfo;
+ }
+ console.log(`Tool result for ${toolUse.name}:`, JSON.stringify(result.content));
+
// Add the tool result to the messages
finalMessages.push({
role: "user",
@@ -98,6 +317,7 @@ export function setupRoutes(app: Express, mcpManager: McpManager): void {
tool_use_id: toolUse.id,
content: result.content.map((c: any) => {
if (c.type === "text") {
+ console.log(`Tool ${toolUse.name} text response:`, c.text);
return {
type: "text",
text: c.text,
@@ -108,6 +328,21 @@ export function setupRoutes(app: Express, mcpManager: McpManager): void {
},
],
});
+
+ // Get an intermediate response after each tool execution
+ const intermediateCompletion = await anthropic.messages.create({
+ model: "claude-3-5-sonnet-20241022",
+ max_tokens: 4000,
+ messages: finalMessages,
+ });
+
+ // Add intermediate response
+ intermediateResponses.push({
+ role: "assistant",
+ content: intermediateCompletion.content,
+ timestamp: new Date(),
+ });
+
} catch (error) {
console.error(`Error executing tool ${toolUse.name}:`, error);
@@ -132,15 +367,43 @@ export function setupRoutes(app: Express, mcpManager: McpManager): void {
}
}
- // Get a new completion with all the tool results
- finalResponse = await anthropic.messages.create({
- model: "claude-3-7-sonnet-20250219",
- max_tokens: 10000,
- messages: finalMessages,
- });
+ try {
+ // Get a final completion with all the tool results
+ finalResponse = await anthropic.messages.create({
+ model: "claude-3-5-sonnet-20241022",
+ max_tokens: 4000,
+ messages: finalMessages,
+ });
+ } catch (error) {
+ console.error("Error creating final response:", error);
+ // If we can't get a final response, use the last intermediate response
+ if (intermediateResponses.length > 0) {
+ const lastIntermediate = intermediateResponses[intermediateResponses.length - 1];
+ // Create a response with the same structure as what Anthropic would return
+ finalResponse = {
+ id: completion.id,
+ content: lastIntermediate.content,
+ model: completion.model,
+ role: "assistant",
+ stop_reason: completion.stop_reason,
+ stop_sequence: completion.stop_sequence,
+ type: completion.type,
+ usage: completion.usage
+ };
+ }
+ }
}
- res.json(finalResponse);
+ // Add server info to the response
+ const responseWithServerInfo = {
+ ...finalResponse,
+ serverInfo: serverUsed,
+ requires_confirmation: !auto_proceed && toolUses.length > 0,
+ intermediateResponses: intermediateResponses,
+ toolsUsed: toolUses.length > 0
+ };
+
+ res.json(responseWithServerInfo);
} catch (error) {
console.error("Error creating chat completion:", error);
res.status(500).json({
@@ -152,8 +415,10 @@ export function setupRoutes(app: Express, mcpManager: McpManager): void {
// Tool discovery endpoint
app.get("/api/tools", async (req: Request, res: Response) => {
+ console.log("API: /api/tools called");
try {
- const sessionId = (req.headers["x-session-id"] as string) || "12345";
+ const rawSessionId = (req.headers["x-session-id"] as string) || "";
+ const sessionId = ensureSession(rawSessionId);
const tools = await mcpManager.discoverTools(sessionId);
res.json({ tools });
} catch (error) {
@@ -166,31 +431,150 @@ export function setupRoutes(app: Express, mcpManager: McpManager): void {
// Tool execution endpoint
app.post("/api/tools/execute", async (req: Request, res: Response) => {
+ console.log("API: /api/tools/execute called");
const { toolName, args } = req.body;
- const sessionId = (req.headers["x-session-id"] as string) || "12345";
+ const rawSessionId = (req.headers["x-session-id"] as string) || "";
+ const sessionId = ensureSession(rawSessionId);
if (!toolName) {
return res.status(400).json({ error: "Tool name is required" });
}
+ // Add enhanced logging
+ console.log(`🛠️ Executing tool: ${toolName}`);
+ console.log(`📋 Session ID: ${sessionId}`);
+ console.log(`📝 Args: ${JSON.stringify(args, (key, value) => {
+ // Don't log credentials in full
+ if (key === "__credentials") {
+ return "[CREDENTIALS REDACTED]";
+ }
+ return value;
+ })}`);
+
try {
const result = await mcpManager.executeToolCall(
sessionId,
toolName,
args || {}
);
+
+ // Get server info for the socket event
+ const serverInfo = result.serverInfo || {};
+
+ // Log server info
+ console.log(`Server info for tool ${toolName}:`, serverInfo);
+
+ // Emit a socket event with the tool execution result
+ if (req.app.get('io')) {
+ const io = req.app.get('io');
+ console.log('Emitting tool_executed event via socket.io');
+
+ const eventData = {
+ toolName,
+ serverId: serverInfo.id || 'unknown',
+ serverName: serverInfo.name || 'Unknown Server',
+ result: {
+ content: result.content || [],
+ isError: false
+ }
+ };
+
+ console.log('Event data:', JSON.stringify(eventData));
+ io.emit('tool_executed', eventData);
+ console.log(`Socket event emitted for tool: ${toolName}`);
+ } else {
+ console.warn('Socket.io not available for emitting events');
+ }
+
+ console.log(`✅ Tool ${toolName} executed successfully`);
res.json(result);
} catch (error) {
console.error(`Error executing tool ${toolName}:`, error);
+
+ // Emit error event via socket
+ if (req.app.get('io')) {
+ const io = req.app.get('io');
+ console.log('Emitting tool_executed error event via socket.io');
+
+ const errorEventData = {
+ toolName,
+ serverId: 'unknown',
+ serverName: 'Error',
+ result: {
+ content: [{ type: 'text', text: `Error: ${error.message || 'Unknown error'}` }],
+ isError: true
+ }
+ };
+
+ console.log('Error event data:', JSON.stringify(errorEventData));
+ io.emit('tool_executed', errorEventData);
+ console.log(`Socket error event emitted for tool: ${toolName}`);
+ }
+
res.status(500).json({
error: error.message || "An error occurred while executing the tool",
});
}
});
+ // Get tools that require credentials
+ app.get("/api/tools/credentials", (req: Request, res: Response) => {
+ console.log("API: /api/tools/credentials called with headers:", JSON.stringify(req.headers));
+ try {
+ const rawSessionId = (req.headers["x-session-id"] as string) || "";
+ const sessionId = ensureSession(rawSessionId);
+ console.log(`API: /api/tools/credentials using sessionId: ${sessionId}`);
+ const tools = mcpManager.getToolsWithCredentialRequirements(sessionId);
+ console.log(`API: /api/tools/credentials found ${tools.length} tools requiring credentials`);
+ res.json({ tools });
+ } catch (error) {
+ console.error("Error getting tools with credential requirements:", error);
+ res.status(500).json({
+ error: error.message || "An error occurred while fetching tools",
+ });
+ }
+ });
+
+ // Set credentials for a tool
+ app.post("/api/tools/credentials", async (req: Request, res: Response) => {
+ console.log("API: /api/tools/credentials POST called");
+ const { toolName, serverId, credentials } = req.body;
+ const rawSessionId = (req.headers["x-session-id"] as string) || "";
+ const sessionId = ensureSession(rawSessionId);
+
+ if (!toolName || !serverId || !credentials) {
+ return res.status(400).json({
+ error: "Missing required fields. toolName, serverId, and credentials are required"
+ });
+ }
+
+ try {
+ const success = await mcpManager.setToolCredentials(
+ sessionId,
+ toolName,
+ serverId,
+ credentials
+ );
+
+ if (success) {
+ res.json({ success: true });
+ } else {
+ res.status(500).json({
+ error: "Failed to set credentials for the tool"
+ });
+ }
+ } catch (error) {
+ console.error(`Error setting credentials for tool ${toolName}:`, error);
+ res.status(500).json({
+ error: error.message || "An error occurred while setting credentials",
+ });
+ }
+ });
+
// Server registration endpoint
app.post("/api/servers", async (req: Request, res: Response) => {
- const { id, name, url } = req.body;
+ console.log("API: /api/servers POST called with body:", JSON.stringify(req.body));
+ const { id, name, url, description, types, tags, verified, rating = 0 } = req.body;
if (!id || !name || !url) {
return res
@@ -199,8 +583,35 @@ export function setupRoutes(app: Express, mcpManager: McpManager): void {
}
try {
- await mcpManager.registerServer({ id, name, url });
- res.json({ success: true });
+ // Try to get detailed rating, but don't fail if we can't
+ let ratingInfo = { average: rating, count: 0, score: 0 };
+
+ try {
+ ratingInfo = await getWeightedRatingScore(id);
+ console.log(`📊 Server rating summary for ${name}: avg=${ratingInfo.average}, votes=${ratingInfo.count}, score=${ratingInfo.score}`);
+ } catch (ratingError) {
+ console.warn(`Unable to fetch rating for ${name}, using provided rating ${rating}:`, ratingError);
+ }
+
+ // Use the server rating we just got or fall back to the provided rating
+ const serverConfig = {
+ id,
+ name,
+ url,
+ description,
+ types,
+ tags,
+ verified,
+ rating: ratingInfo.average || rating
+ };
+
+ const success = await mcpManager.registerServer(serverConfig);
+
+ if (success) {
+ res.json({ success: true });
+ } else {
+ res.status(400).json({ success: false, message: "Failed to connect to server or discover tools" });
+ }
} catch (error) {
console.error("Error registering server:", error);
res.status(500).json({
@@ -212,24 +623,68 @@ export function setupRoutes(app: Express, mcpManager: McpManager): void {
// Get available servers endpoint
app.get("/api/servers", (req: Request, res: Response) => {
+ console.log("API: /api/servers GET called");
const servers = mcpManager.getAvailableServers();
res.json({ servers });
});
// Registry refresh endpoint
app.post("/api/registry/refresh", async (req: Request, res: Response) => {
+ console.log("API: /api/registry/refresh called");
try {
- const registryServers = await mcpManager.fetchRegistryServers();
- res.json({
+ // Create registry client and fetch popular servers
+ const registryClient = new RegistryClient();
+ const servers = await registryClient.getPopularServers();
+
+ console.log(`Fetched ${servers.length} popular servers from Nanda Registry`);
+
+ res.json({
success: true,
- servers: registryServers,
+ servers,
+ message: `Found ${servers.length} servers in the registry`
});
} catch (error) {
- console.error("Error refreshing servers from registry:", error);
+ console.error("Error refreshing registry servers:", error);
res.status(500).json({
- error:
- error.message ||
- "An error occurred while refreshing registry servers",
+ error: error.message || "An error occurred while refreshing registry servers"
+ });
+ }
+ });
+
+ // Registry search endpoint
+ app.get("/api/registry/search", async (req: Request, res: Response) => {
+ console.log("API: /api/registry/search called with query:", req.query);
+ try {
+ const query = req.query.q as string;
+
+ if (!query) {
+ return res.status(400).json({
+ error: "Search query is required"
+ });
+ }
+
+ // Create registry client and search for servers
+ const registryClient = new RegistryClient();
+ const servers = await registryClient.searchServers(query, {
+ limit: req.query.limit ? parseInt(req.query.limit as string) : undefined,
+ page: req.query.page ? parseInt(req.query.page as string) : undefined,
+ tags: req.query.tags as string,
+ type: req.query.type as string,
+ verified: req.query.verified ? req.query.verified === 'true' : undefined
+ });
+
+ console.log(`Found ${servers.length} servers matching query "${query}"`);
+
+ res.json({
+ success: true,
+ servers,
+ query,
+ message: `Found ${servers.length} servers matching "${query}"`
+ });
+ } catch (error) {
+ console.error("Error searching registry servers:", error);
+ res.status(500).json({
+ error: error.message || "An error occurred while searching registry servers"
});
}
});
diff --git a/server/storage/servers.json b/server/storage/servers.json
new file mode 100644
index 0000000..b73dd31
--- /dev/null
+++ b/server/storage/servers.json
@@ -0,0 +1,274 @@
+[
+ {
+ "id": "bakery ",
+ "name": "Bakery",
+ "url": "https://web-production-091a3.up.railway.app/sse",
+ "description": "An MCP-compatible server for bakery operations that handles menu queries, order processing, and real-time ingredient availability. Responds to natural language prompts like “What pastries are fresh today?” or “Order a dozen croissants for pickup.”",
+ "types": [
+ "agent"
+ ],
+ "tags": [
+ "agent"
+ ],
+ "verified": false,
+ "rating": 0
+ },
+ {
+ "id": "Spotify",
+ "name": "Spotify ",
+ "url": "https://tjdgmjqend.us-east-1.awsapprunner.com"
+ },
+ {
+ "id": "Spotify1",
+ "name": "Spotify1",
+ "url": "https://tjdgmjqend.us-east-1.awsapprunner.com/sse"
+ },
+ {
+ "id": "2871c512-7765-498c-831e-0b1a09be5336",
+ "name": "ConstructionAI",
+ "url": "https://smort-digital-tejas45.replit.app/mcp/sse"
+ },
+ {
+ "id": "d0accff0-f994-49fa-aa37-c2b76ee59b3d",
+ "name": "Toxicology AI",
+ "url": "https://agjz7p7vza.us-east-1.awsapprunner.com/sse",
+ "description": "Toxicology AI MCP delivers insights from toxicology data related to medicolegal death investigations (MDI). It streams live responses from an AI system that analyzes trends, identifies substances, and surfaces patterns across toxicology reports — helping investigators, researchers, and public health teams get immediate, interpretable feedback from complex MDI datasets.",
+ "types": [
+ "tool"
+ ],
+ "tags": [
+ "toxicology",
+ "medicolegal-death-investigation",
+ "public-health-insights",
+ "forensic-data",
+ "forensic-toxicology",
+ "mdi-data",
+ "health"
+ ],
+ "verified": false,
+ "rating": 5
+ },
+ {
+ "id": "aa9262ad-6cd8-4d4d-9bf0-013517ecac08",
+ "name": "Search Spotify Songs",
+ "url": "https://mppfwfdsnu.us-east-1.awsapprunner.com/sse"
+ },
+ {
+ "id": "97f3f10d-7c5e-4343-ad98-8b863121eafa",
+ "name": "Currency Converter MCP Server",
+ "url": "http://3.133.113.164:8080/sse",
+ "description": "Converts amounts from one currency to another using live exchange rates.",
+ "types": [
+ "tool"
+ ],
+ "tags": [
+ "currency",
+ "converter",
+ "finance"
+ ],
+ "verified": false,
+ "rating": 1
+ },
+ {
+ "id": "4e463897-3f21-44af-881e-00a9814a085a",
+ "name": "Synthetic Data generator",
+ "url": "https://gbznaqxwci.us-east-1.awsapprunner.com/sse"
+ },
+ {
+ "id": "767b2a3a-53ac-4dcf-8721-21a7c0584449",
+ "name": "Self Sovereign Identity Tools",
+ "url": "https://mcp-traction-api.xanaducyber.com/sse",
+ "description": "This project establishes a middleware communication layer between an ACA-py Self-Sovereign Identity (SSI) Custodial Wallet and a generative AI agent environment, using the Message-Channel-Protocol (MCP) framework. It integrates:\n\nA reactive client that communicates with a ChatOllama AI model\nMultiple toolchains derived from Traction's Swagger API\nStructured, multi-turn conversation flows\nComprehensive integration testing using a synthetic test tenant (\"Alice\")\nThe client simulates complex workflows by invoking tools in a structured fashion and summarizing outputs clearly. Tests are configured via .example-env and validate real-time interactions against the Traction instance.",
+ "types": [
+ "tool"
+ ],
+ "tags": [
+ "self-sovereign-identity",
+ "ssi",
+ "identity",
+ "decentralised-identity",
+ "zero-knowledge-proofs",
+ "cl-singatures",
+ "verifiable-credentials",
+ "traction",
+ "acapy",
+ "aries",
+ "hyperledger",
+ "identity-wallet"
+ ],
+ "verified": false,
+ "rating": 0
+ },
+ {
+ "id": "dce98a32-70aa-45ad-9266-b7dc8dce442c",
+ "name": "Mara MCP",
+ "url": "https://mcpapi.alys.pro/sse",
+ "description": "A Model Context Protocol (MCP) server that provides real-time Bitcoin blockchain data. This server can be integrated with NANDA Registry to enhance your AI assistants with live Bitcoin blockchain data.",
+ "types": [
+ "agent",
+ "tool"
+ ],
+ "tags": [
+ "mara",
+ "btc",
+ "bitcoin"
+ ],
+ "verified": false,
+ "rating": 0
+ },
+ {
+ "id": "a9ac8350-b64e-4eef-85e1-d1b67f9a3d39",
+ "name": "HCLSoftware Info",
+ "url": "http://nanda.eastus2.cloudapp.azure.com/sse"
+ },
+ {
+ "id": "2e3131c0-65ae-4d84-9621-17a6e8f0e6cd",
+ "name": "Arxiv Paper Helper",
+ "url": "https://mjrupm9g3c.us-east-1.awsapprunner.com/sse"
+ },
+ {
+ "id": "9d0b54be-8a53-49c7-82ac-db8a356c7f21",
+ "name": "Spotify Song Search",
+ "url": "https://tjdgmjqend.us-east-1.awsapprunner.com/sse"
+ },
+ {
+ "id": "a149be9b-d2e6-429e-b9f4-57b168f10b16",
+ "name": "DeepMyst",
+ "url": "https://mcp.deepmyst.com/sse",
+ "description": "DeepMyst is an AI optimization platform designed to supercharge large language model (LLM) interactions through token optimization and intelligent model routing, but its real breakthrough lies in enabling LLM collaboration and Mixture of Agents workflows.\r\n\r\nAt its core, DeepMyst acts as a unified gateway to major LLM providers like OpenAI, Anthropic, Google, and Groq, offering access to top models (e.g., GPT-4o, Claude, Gemini) via a single API. Its Model Context Protocol (MCP) allows these diverse models to collaborate as a team—each agent or model specializing in different tasks such as ideation, reasoning, or data analysis.\r\n\r\nThis Mixture of Agents approach lets organizations orchestrate AI workflows where each LLM contributes based on its strengths. The models share context, build on each other’s outputs, and solve complex problems together—far more effectively than any single model alone. DeepMyst routes each task to the best-suited model, balancing speed, cost, and performance, while optimizing token usage by up to 75%.\r\n\r\nIn short, DeepMyst doesn't just streamline LLM usage—it reimagines how models collaborate, unlocking new frontiers in agent-based AI systems and complex, multi-model workflows.",
+ "types": [
+ "tool"
+ ],
+ "tags": [
+ "mixture-of-agents",
+ "model-router",
+ "token-optimization",
+ "cross-model-reasoning",
+ "llm-collaboration"
+ ],
+ "verified": false,
+ "rating": 5
+ },
+ {
+ "id": "fe9c7d80-7f3f-495f-8406-7ef7a5667495",
+ "name": "AI Coding Project Setup Helper",
+ "url": "https://mcp-cursor-setup-enlighter.replit.app/sse",
+ "description": "This MCP is a part of Enlighter project (https://enlightby.ai). MCP server helps users set up and configure their AI assistant environments with three key components:\n\n1. Memory Bank - A structured documentation system for context preservation (Currently Implemented)\n2. MCP Servers - MCP servers recommender for extending AI capabilities (In progress)\n3. Rules - Rules recommendations based on tech stack and project features (In progress)",
+ "types": [
+ "tool"
+ ],
+ "tags": [
+ "education",
+ "vibe-coding",
+ "ai-coding"
+ ],
+ "verified": false,
+ "rating": 5
+ },
+ {
+ "id": "a3e09952-e1bf-44ed-96cc-65defbf3d8d4",
+ "name": "Hyperskill",
+ "url": "https://mcp-hyperskill.replit.app/sse",
+ "description": "Hyperskill MCP is a server that integrates with Hyperskill's educational resources, enabling AI agents to provide programming topic explanations using Hyperskill's learning materials.\r\n\r\nCore Capabilities\r\n- Code Concept Explanation: Analyzes code to identify and explain programming concepts with direct links to relevant Hyperskill learning resources\r\n- Topic Search: Allows searching for specific programming topics within Hyperskill's knowledge base\r\n\r\nThe server exposes two main tools for AI agents:\r\n1. explain_topics_in_the_code: Identifies key programming concepts in code snippets and provides structured explanations with Hyperskill references\r\n2. find_topics_on_hyperskill: Searches Hyperskill's database for specific programming topics",
+ "types": [
+ "tool"
+ ],
+ "tags": [
+ "education",
+ "programming",
+ "learn-programming"
+ ],
+ "verified": false,
+ "rating": 5
+ },
+ {
+ "id": "c6284608-6bce-4417-a170-da6c1a117616",
+ "name": "Nexonco",
+ "url": "https://mcp.nexonco.nexgene.ai/sse",
+ "description": "Nexonco by Nexgene Research is an advanced MCP Server for accessing and analyzing clinical evidence data, with flexible search across genes, variants, diseases, drugs and phenotypes to support precision oncology research.",
+ "types": [
+ "tool"
+ ],
+ "tags": [
+ "health",
+ "bio",
+ "cancer",
+ "oncology",
+ "variant",
+ "disease",
+ "drug",
+ "therapy",
+ "phenotype",
+ "genomics",
+ "medicine"
+ ],
+ "verified": false,
+ "rating": 5
+ },
+ {
+ "id": "d10a76ec-5182-4570-bf6c-a8b3af099507",
+ "name": "Web Data Scraper 1",
+ "url": "https://9223-2406-7400-56-cb93-20da-c905-3538-629b.ngrok-free.app/sse"
+ },
+ {
+ "id": "7951eac1-57b0-45be-ad75-566eb52edd2a",
+ "name": "chainaim's mcp",
+ "url": "https://5ezxku2qc8.us-east-1.awsapprunner.com/sse",
+ "description": "A simple server for chain aim",
+ "types": [
+ "tool"
+ ],
+ "tags": [
+ "cashflow",
+ "simulation",
+ "financial",
+ "loans"
+ ],
+ "verified": false,
+ "rating": 0
+ },
+ {
+ "id": "04628c42-6c74-4397-a2ad-76218414ffa7",
+ "name": "Office Supplies Inventory",
+ "url": "https://p8x2zwifa5.us-east-2.awsapprunner.com/sse",
+ "description": "An MCP server to query an office supplies inventory: you can get a list of all items in the inventory, or get all the attributes of a particular item.",
+ "types": [
+ "resource"
+ ],
+ "tags": [
+ "office-inventory"
+ ],
+ "verified": false,
+ "rating": 0
+ },
+ {
+ "id": "d9246b27-1367-4f28-989f-ad7081ba922f",
+ "name": "Economic Index Analyzer",
+ "url": "https://econ-agent-cloudflare.zwadia.workers.dev/sse",
+ "description": "Anthropic periodically publishes data on how Claude is being used. These emergent patterns of use can be analyzed by an Agent, and it may elect to collate other tools & resources to then assemble a solution that addresses this emergent use-case. This explorer via MCP SSE is just step one.",
+ "types": [
+ "resource",
+ "tool"
+ ],
+ "tags": [],
+ "verified": false,
+ "rating": 0
+ },
+ {
+ "id": "4d93fb27-26e3-48f2-870d-e8a43ba6d9ec",
+ "name": "Compare Zipcode Demographics",
+ "url": "https://0ce1-18-27-79-169.ngrok-free.app/sse",
+ "description": "A FastMCP-compatible Model Context Protocol (MCP) server for comparing zipcode-level demographic distributions. Includes a tool for detecting statistically significant differences in population segments—based on age, gender, and ethnicity—between two ZIP Code Tabulation Areas (ZCTAs), using z-score analysis over synthetic census data.",
+ "types": [
+ "tool"
+ ],
+ "tags": [],
+ "verified": false,
+ "rating": 0
+ },
+ {
+ "id": "1d72cda4-3bd9-4f18-a71d-957bb60fd342",
+ "name": "Bakery",
+ "url": "https://web-production-091a3.up.railway.app/sse"
+ }
+]
\ No newline at end of file
diff --git a/server/tsconfig.json b/server/tsconfig.json
index ca20f80..0f12fcc 100644
--- a/server/tsconfig.json
+++ b/server/tsconfig.json
@@ -1,19 +1,27 @@
{
"compilerOptions": {
"target": "ES2020",
- "module": "NodeNext",
- "moduleResolution": "NodeNext",
+ "module": "ESNext",
+ "moduleResolution": "Node",
"esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": false,
"noImplicitAny": false,
"skipLibCheck": true,
- "outDir": "dist",
- "rootDir": "src",
+ "outDir": "./dist", // Output relative to this tsconfig.json -> /app/server/dist
+ "rootDir": "..", // Root of source files including parent directory
+ "baseUrl": "..", // Base directory to resolve non-relative module names
"declaration": true,
"resolveJsonModule": true,
- "useUnknownInCatchVariables": false
+ "useUnknownInCatchVariables": false,
+ "paths": {
+ "../../shared/*": ["shared/*"]
+ }
},
- "include": ["src/**/*.ts"],
+ "include": [
+ "src/**/*.ts",
+ "../shared/**/*.ts" // Path to shared from server directory
+ ],
"exclude": ["node_modules", "dist"]
}
diff --git a/shared/types.d.ts b/shared/types.d.ts
new file mode 100644
index 0000000..1090bab
--- /dev/null
+++ b/shared/types.d.ts
@@ -0,0 +1,67 @@
+export interface ServerConfig {
+ id: string;
+ name: string;
+ url: string;
+}
+export interface MessageContent {
+ type: string;
+ text?: string;
+ data?: string;
+ [key: string]: any;
+}
+export interface Message {
+ role: "user" | "assistant";
+ content: MessageContent | MessageContent[];
+}
+export interface ToolCall {
+ id: string;
+ name: string;
+ input: any;
+ result?: {
+ content: MessageContent[];
+ isError?: boolean;
+ };
+}
+export interface ChatCompletionRequest {
+ messages: Message[];
+ tools?: boolean;
+}
+export interface ChatCompletionResponse {
+ id: string;
+ model: string;
+ content: MessageContent[];
+ usage: {
+ input_tokens: number;
+ output_tokens: number;
+ };
+}
+export interface ToolExecutionRequest {
+ toolName: string;
+ args: any;
+}
+export interface ToolExecutionResponse {
+ content: MessageContent[];
+ isError?: boolean;
+}
+export interface ApiKeyRequest {
+ apiKey: string;
+}
+export interface ApiKeyResponse {
+ success: boolean;
+}
+export interface ToolsListResponse {
+ tools: Array<{
+ name: string;
+ description?: string;
+ inputSchema: any;
+ }>;
+}
+export interface ServersListResponse {
+ servers: ServerConfig[];
+}
+export interface SessionResponse {
+ sessionId: string;
+}
+export interface ErrorResponse {
+ error: string;
+}
diff --git a/shared/types.js b/shared/types.js
new file mode 100644
index 0000000..c8ad2e5
--- /dev/null
+++ b/shared/types.js
@@ -0,0 +1,2 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
diff --git a/shared/types.ts b/shared/types.ts
index 1b92539..9d7e995 100644
--- a/shared/types.ts
+++ b/shared/types.ts
@@ -60,12 +60,44 @@ export interface ApiKeyResponse {
success: boolean;
}
+// New interfaces for tool credential management
+export interface CredentialRequirement {
+ id: string;
+ name: string;
+ description?: string;
+ acquisition?: {
+ url: string;
+ instructions?: string;
+ };
+}
+
+export interface ToolCredentialInfo {
+ toolName: string;
+ serverName: string;
+ serverId: string;
+ credentials: CredentialRequirement[];
+}
+
+export interface ToolCredentialRequest {
+ toolName: string;
+ serverId: string;
+ credentials: Record;
+}
+
+export interface ToolCredentialResponse {
+ success: boolean;
+ error?: string;
+}
+
+export interface ToolInfo {
+ name: string;
+ description?: string;
+ inputSchema: any;
+ credentialRequirements?: CredentialRequirement[];
+}
+
export interface ToolsListResponse {
- tools: Array<{
- name: string;
- description?: string;
- inputSchema: any;
- }>;
+ tools: ToolInfo[];
}
export interface ServersListResponse {