├── public ├── .nojekyll └── favicon.ico ├── src ├── vite-env.d.ts ├── lib │ ├── utils.ts │ ├── messages.ts │ ├── presets.json │ ├── models.ts │ └── models.json ├── types │ ├── toolCall.ts │ ├── llmModel.ts │ ├── message.ts │ └── mcpServer.ts ├── assets │ └── thirdparty │ │ └── logos │ │ ├── anthropic.svg │ │ ├── groq.svg │ │ ├── xai.svg │ │ ├── togetherai.svg │ │ ├── google.svg │ │ ├── fireworks.svg │ │ ├── vertex.svg │ │ ├── fireworksai.svg │ │ ├── openai.svg │ │ ├── deepseek.svg │ │ ├── mistral.svg │ │ └── ollama.svg ├── main.tsx ├── components │ ├── ui │ │ ├── Label.tsx │ │ ├── Textarea.tsx │ │ ├── Input.tsx │ │ ├── Tooltip.tsx │ │ ├── Spinner.tsx │ │ ├── Accordion.tsx │ │ ├── Button.tsx │ │ ├── Dialog.tsx │ │ ├── Select.tsx │ │ └── DropdownMenu.tsx │ ├── ThemeToggle.tsx │ ├── Footer.tsx │ ├── ErrorMessage.tsx │ ├── ThemeProvider.tsx │ ├── ModelPicker.tsx │ ├── SetupGuide.tsx │ ├── EmptyState.tsx │ ├── NavBar.tsx │ ├── ChatInput.tsx │ ├── ApiKeySettings.tsx │ ├── TutorialCard.tsx │ ├── Chat.tsx │ └── ToolSettings.tsx ├── globals.css ├── hooks │ ├── useChat.ts │ └── useMcpTools.ts └── App.tsx ├── postcss.config.js ├── .prettierrc ├── tsconfig.json ├── vite.config.ts ├── .gitignore ├── components.json ├── index.html ├── tsconfig.node.json ├── tsconfig.app.json ├── eslint.config.js ├── package.json ├── README.md ├── tailwind.config.ts └── .github └── workflows └── deploy.yml /public/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netglade/mcp-chat/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/types/toolCall.ts: -------------------------------------------------------------------------------- 1 | export type ToolCallArgument = { 2 | name: string 3 | value: string 4 | } 5 | 6 | export type ToolCall = { 7 | name: string 8 | arguments: ToolCallArgument[] 9 | result: string, 10 | id: string, 11 | } 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "semi": false, 4 | "tabWidth": 4, 5 | "singleQuote": true, 6 | "jsxSingleQuote": true, 7 | "plugins": [ 8 | "prettier-plugin-tailwindcss", 9 | "@trivago/prettier-plugin-sort-imports" 10 | ] 11 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["./src/*"] 11 | } 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/assets/thirdparty/logos/anthropic.svg: -------------------------------------------------------------------------------- 1 | Anthropic 2 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import path from "path" 4 | 5 | // https://vite.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | base: '/mcp-chat', 9 | resolve: { 10 | alias: { 11 | "@": path.resolve(__dirname, "./src"), 12 | }, 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | api-keys.txt 26 | -------------------------------------------------------------------------------- /src/types/llmModel.ts: -------------------------------------------------------------------------------- 1 | export type LLMModel = { 2 | id: string 3 | name: string 4 | provider: string 5 | providerId: string 6 | } 7 | 8 | export type LLMModelConfig = { 9 | model?: string 10 | apiKey?: string 11 | baseURL?: string 12 | temperature?: number 13 | topP?: number 14 | topK?: number 15 | frequencyPenalty?: number 16 | presencePenalty?: number 17 | maxTokens?: number 18 | } 19 | -------------------------------------------------------------------------------- /src/assets/thirdparty/logos/groq.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/types/message.ts: -------------------------------------------------------------------------------- 1 | import { ToolCall } from '@/types/toolCall' 2 | 3 | export type MessageText = { 4 | type: 'text' 5 | text: string 6 | } 7 | 8 | export type MessageCode = { 9 | type: 'code' 10 | text: string 11 | } 12 | 13 | export type MessageImage = { 14 | type: 'image' 15 | image: string 16 | } 17 | 18 | export type Message = { 19 | role: 'assistant' | 'user' 20 | content: Array 21 | toolCalls?: ToolCall[] 22 | } 23 | -------------------------------------------------------------------------------- /src/assets/thirdparty/logos/xai.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/index.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | MCP Chat 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/lib/messages.ts: -------------------------------------------------------------------------------- 1 | import { Message } from '@/types/message' 2 | import { CoreMessage } from 'ai' 3 | 4 | export function toAISDKMessages(messages: Message[]) { 5 | return messages.map((message) => ({ 6 | role: message.role, 7 | content: message.content.map((content) => { 8 | if (content.type === 'code') { 9 | return { 10 | type: 'text', 11 | text: content.text, 12 | } 13 | } 14 | 15 | return content 16 | }), 17 | } as CoreMessage)) 18 | } 19 | -------------------------------------------------------------------------------- /src/assets/thirdparty/logos/togetherai.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | import './globals.css' 3 | import App from './App.tsx' 4 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 5 | import { ThemeProvider } from '@/components/ThemeProvider.tsx' 6 | 7 | const queryClient = new QueryClient({ 8 | defaultOptions: { 9 | queries: { 10 | staleTime: 60 * 1000, 11 | }, 12 | }, 13 | }) 14 | 15 | createRoot(document.getElementById('root')!).render( 16 | 17 | 18 | 19 | 20 | , 21 | ) 22 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /src/types/mcpServer.ts: -------------------------------------------------------------------------------- 1 | import { McpSandbox } from '@netglade/mcp-sandbox' 2 | import { experimental_createMCPClient } from 'ai' 3 | 4 | export type McpServerState = 'starting' | 'running' | 'error' 5 | 6 | export type McpServerConfiguration = { 7 | name: string 8 | command: string 9 | envs: Record 10 | id: string 11 | } 12 | 13 | export type McpServerClient = { 14 | id: string 15 | configuration: McpServerConfiguration 16 | state: McpServerState 17 | sandbox?: McpSandbox 18 | url?: string 19 | client?: Awaited> 20 | tools?: Awaited>['tools']>> 21 | } 22 | -------------------------------------------------------------------------------- /src/assets/thirdparty/logos/google.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/lib/presets.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | { 4 | "name": "Brave Search", 5 | "command": "npx -y @modelcontextprotocol/server-brave-search", 6 | "envs": { 7 | "BRAVE_API_KEY": "" 8 | } 9 | }, 10 | { 11 | "name": "E2B Interpreter", 12 | "command": "npx -y @e2b/mcp-server", 13 | "envs": { 14 | "E2B_API_KEY": "" 15 | } 16 | }, 17 | { 18 | "name": "GitHub", 19 | "command": "npx -y @modelcontextprotocol/server-github", 20 | "envs": { 21 | "GITHUB_PERSONAL_ACCESS_TOKEN": "" 22 | } 23 | }, 24 | { 25 | "name": "PostgreSQL", 26 | "command": "npx -y @modelcontextprotocol/server-postgres postgresql://localhost/mydb", 27 | "envs": {} 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/assets/thirdparty/logos/fireworks.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/ui/Label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as LabelPrimitive from '@radix-ui/react-label' 3 | import { cva, type VariantProps } from 'class-variance-authority' 4 | 5 | import { cn } from '@/lib/utils' 6 | 7 | const labelVariants = cva( 8 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /src/lib/models.ts: -------------------------------------------------------------------------------- 1 | import { createAnthropic } from '@ai-sdk/anthropic' 2 | import { createOpenAI } from '@ai-sdk/openai' 3 | import { LLMModel, LLMModelConfig } from '@/types/llmModel' 4 | 5 | export function getModelClient(model: LLMModel, config: LLMModelConfig) { 6 | const { id: modelNameString, providerId } = model 7 | const { apiKey, baseURL } = config 8 | 9 | const providerConfigs = { 10 | anthropic: () => createAnthropic({ apiKey, baseURL })(modelNameString), 11 | openai: () => createOpenAI({ apiKey, baseURL })(modelNameString, {structuredOutputs: false}), //INFO: Workaround for https://github.com/vercel/ai/issues/4662 12 | } 13 | 14 | const createClient = 15 | providerConfigs[providerId as keyof typeof providerConfigs] 16 | 17 | if (!createClient) { 18 | throw new Error(`Unsupported provider: ${providerId}`) 19 | } 20 | 21 | return createClient() 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2024", 5 | "useDefineForClassFields": true, 6 | "lib": [ 7 | "ES2024", 8 | "DOM", 9 | "DOM.Iterable" 10 | ], 11 | "module": "ESNext", 12 | "skipLibCheck": true, 13 | /* Bundler mode */ 14 | "moduleResolution": "bundler", 15 | "allowImportingTsExtensions": true, 16 | "isolatedModules": true, 17 | "moduleDetection": "force", 18 | "noEmit": true, 19 | "jsx": "react-jsx", 20 | /* Linting */ 21 | "strict": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noFallthroughCasesInSwitch": true, 25 | "noUncheckedSideEffectImports": true, 26 | "baseUrl": ".", 27 | "paths": { 28 | "@/*": [ 29 | "./src/*" 30 | ] 31 | } 32 | }, 33 | "include": [ 34 | "src" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | import eslintConfigPrettier from 'eslint-config-prettier/flat' 7 | 8 | export default tseslint.config( 9 | { ignores: ['dist'] }, 10 | { 11 | extends: [js.configs.recommended, ...tseslint.configs.recommended, eslintConfigPrettier], 12 | files: ['**/*.{ts,tsx}'], 13 | languageOptions: { 14 | ecmaVersion: 2020, 15 | globals: globals.browser, 16 | }, 17 | plugins: { 18 | 'react-hooks': reactHooks, 19 | 'react-refresh': reactRefresh, 20 | }, 21 | rules: { 22 | ...reactHooks.configs.recommended.rules, 23 | 'react-refresh/only-export-components': [ 24 | 'warn', 25 | { allowConstantExport: true }, 26 | ], 27 | }, 28 | }, 29 | ) 30 | -------------------------------------------------------------------------------- /src/components/ui/Textarea.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils' 2 | import * as React from 'react' 3 | 4 | export interface TextareaProps 5 | extends React.TextareaHTMLAttributes { 6 | } 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |