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. -![MCP Host Screenshot](/path/to/screenshot.png) +![MCP Host Screenshot](host_screenshot.png) ## 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} + + + + + + + + + + + {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 - - - - ) : ( - 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 ( - - Image - - ); - } 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 ( - + +