diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..780c92d --- /dev/null +++ b/client/README.md @@ -0,0 +1,50 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default tseslint.config({ + languageOptions: { + // other options... + parserOptions: { + project: ["./tsconfig.node.json", "./tsconfig.app.json"], + tsconfigRootDir: import.meta.dirname, + }, + }, +}); +``` + +- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` +- Optionally add `...tseslint.configs.stylisticTypeChecked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: + +```js +// eslint.config.js +import react from "eslint-plugin-react"; + +export default tseslint.config({ + // Set the react version + settings: { react: { version: "18.3" } }, + plugins: { + // Add the react plugin + react, + }, + rules: { + // other rules... + // Enable its recommended rules + ...react.configs.recommended.rules, + ...react.configs["jsx-runtime"].rules, + }, +}); +``` diff --git a/client/components.json b/client/components.json new file mode 100644 index 0000000..cd5af28 --- /dev/null +++ b/client/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} \ No newline at end of file diff --git a/client/eslint.config.js b/client/eslint.config.js new file mode 100644 index 0000000..79a552e --- /dev/null +++ b/client/eslint.config.js @@ -0,0 +1,28 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist"] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + }, + }, +); diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..e4b78ea --- /dev/null +++ b/client/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..7b020ec --- /dev/null +++ b/client/package.json @@ -0,0 +1,41 @@ +{ + "name": "client", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.1", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "lucide-react": "^0.447.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "tailwind-merge": "^2.5.3", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@eslint/js": "^9.11.1", + "@types/node": "^22.7.5", + "@types/react": "^18.3.10", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.2", + "autoprefixer": "^10.4.20", + "eslint": "^9.11.1", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.12", + "globals": "^15.9.0", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.13", + "typescript": "^5.5.3", + "typescript-eslint": "^8.7.0", + "vite": "^5.4.8" + } +} diff --git a/client/postcss.config.js b/client/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/client/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/client/public/vite.svg b/client/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/client/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/App.css b/client/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/client/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 0000000..693f015 --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,113 @@ +import { useState, useEffect } from "react"; +import { Send, Bell, Terminal, Files } from "lucide-react"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; + +import ConsoleTab from "./components/ConsoleTab"; +import Sidebar from "./components/Sidebar"; +import RequestsTab from "./components/RequestsTabs"; +import ResourcesTab, { Resource } from "./components/ResourcesTab"; +import NotificationsTab from "./components/NotificationsTab"; + +const App = () => { + const [socket, setSocket] = useState(null); + const [resources, setResources] = useState([]); + const [selectedResource, setSelectedResource] = useState( + null, + ); + const [resourceContent, setResourceContent] = useState(""); + const [connectionStatus, setConnectionStatus] = useState< + "disconnected" | "connected" | "error" + >("disconnected"); + const [error, setError] = useState(null); + + useEffect(() => { + const ws = new WebSocket("ws://localhost:3000"); + + ws.onopen = () => { + console.log("Connected to WebSocket server"); + setConnectionStatus("connected"); + setSocket(ws); + }; + + ws.onmessage = (event) => { + const message = JSON.parse(event.data); + console.log("Received message:", message); + if (message.type === "resources") { + setResources(message.data.resources); + setError(null); + } else if (message.type === "resource") { + setResourceContent(JSON.stringify(message.data, null, 2)); + setError(null); + } else if (message.type === "error") { + setError(message.message); + } + }; + + ws.onerror = () => { + setConnectionStatus("error"); + }; + + ws.onclose = () => { + setConnectionStatus("disconnected"); + }; + + return () => ws.close(); + }, []); + + const listResources = () => { + if (socket) { + socket.send(JSON.stringify({ type: "listResources" })); + } + }; + + const readResource = (uri: string) => { + if (socket) { + socket.send(JSON.stringify({ type: "readResource", uri })); + } + }; + + return ( +
+ +
+
+ + + + + Requests + + + + Notifications + + + + Resources + + + + Console + + + + + + + + +
+
+
+ ); +}; + +export default App; diff --git a/client/src/assets/react.svg b/client/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/client/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/components/ConsoleTab.tsx b/client/src/components/ConsoleTab.tsx new file mode 100644 index 0000000..8f05f70 --- /dev/null +++ b/client/src/components/ConsoleTab.tsx @@ -0,0 +1,12 @@ +import { TabsContent } from "@/components/ui/tabs"; + +const ConsoleTab = () => ( + +
+
Welcome to MCP Client Console
+ {/* Console output would go here */} +
+
+); + +export default ConsoleTab; diff --git a/client/src/components/NotificationsTab.tsx b/client/src/components/NotificationsTab.tsx new file mode 100644 index 0000000..7e23b99 --- /dev/null +++ b/client/src/components/NotificationsTab.tsx @@ -0,0 +1,33 @@ +import { Bell } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { TabsContent } from "@/components/ui/tabs"; + +const NotificationsTab = () => ( + +
+
+
+ + +
+