├── .dockerignore ├── .env.example ├── .eslintrc.json ├── .github ├── .dangerfile.ts ├── release-please-config.json └── workflows │ ├── lint-and-type-check.yml │ ├── pr-check.yml │ └── release.yml ├── .gitignore ├── .husky └── pre-commit ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── biome.json ├── components.json ├── custom-mcp-server └── index.ts ├── docker ├── Dockerfile └── compose.yml ├── docs └── tips-guides │ ├── docker.md │ ├── mcp-server-setup-and-tool-testing.md │ ├── oauth.md │ ├── project_with_mcp.md │ ├── system-prompts-and-customization.md │ ├── temporary_chat.md │ └── vercel.md ├── drizzle.config.ts ├── messages ├── en.json ├── es.json ├── fr.json ├── ja.json ├── ko.json ├── language.md └── zh.json ├── next.config.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public ├── file.svg ├── globe.svg ├── next.svg ├── pf.png ├── sounds │ └── start_voice.ogg ├── vercel.svg └── window.svg ├── scripts ├── clean.ts ├── db-migrate.ts ├── initial-env.ts └── postinstall.ts ├── src ├── app │ ├── (auth) │ │ ├── layout.tsx │ │ ├── sign-in │ │ │ └── page.tsx │ │ └── sign-up │ │ │ └── page.tsx │ ├── (chat) │ │ ├── chat │ │ │ └── [thread] │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── mcp │ │ │ ├── create │ │ │ │ └── page.tsx │ │ │ ├── modify │ │ │ │ └── [id] │ │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ └── test │ │ │ │ └── [id] │ │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── project │ │ │ └── [id] │ │ │ └── page.tsx │ ├── api │ │ ├── auth │ │ │ ├── [...all] │ │ │ │ └── route.ts │ │ │ └── actions.ts │ │ ├── chat │ │ │ ├── [threadId] │ │ │ │ └── route.ts │ │ │ ├── actions.ts │ │ │ ├── helper.ts │ │ │ ├── openai-realtime │ │ │ │ └── route.ts │ │ │ ├── route.ts │ │ │ ├── summarize │ │ │ │ └── route.ts │ │ │ └── temporary │ │ │ │ └── route.ts │ │ ├── mcp │ │ │ ├── actions.ts │ │ │ ├── server-customizations │ │ │ │ └── [server] │ │ │ │ │ └── route.ts │ │ │ └── tool-customizations │ │ │ │ └── [server] │ │ │ │ ├── [tool] │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ ├── thread │ │ │ └── route.ts │ │ └── user │ │ │ └── preferences │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── loading.tsx │ └── store.ts ├── components │ ├── chat-bot-temporary.tsx │ ├── chat-bot-voice.tsx │ ├── chat-bot.tsx │ ├── chat-greeting.tsx │ ├── chat-preferences-content.tsx │ ├── chat-preferences-popup.tsx │ ├── create-project-popup.tsx │ ├── create-project-with-thread-popup.tsx │ ├── enabled-mcp-tools-dropdown.tsx │ ├── json-view-popup.tsx │ ├── keyboard-shortcuts-popup.tsx │ ├── layouts │ │ ├── app-header.tsx │ │ ├── app-popup-provider.tsx │ │ ├── app-sidebar-menus.tsx │ │ ├── app-sidebar-projects.tsx │ │ ├── app-sidebar-threads.tsx │ │ ├── app-sidebar-user.tsx │ │ ├── app-sidebar.tsx │ │ └── theme-provider.tsx │ ├── markdown.tsx │ ├── mcp-card.tsx │ ├── mcp-customization-popup.tsx │ ├── mcp-editor.tsx │ ├── mcp-overview.tsx │ ├── mention-input.tsx │ ├── mermaid-diagram.tsx │ ├── message-editor.tsx │ ├── message-parts.tsx │ ├── message-pasts-content.tsx │ ├── message.tsx │ ├── pre-block.tsx │ ├── project-dropdown.tsx │ ├── project-system-message-popup.tsx │ ├── prompt-input.tsx │ ├── select-model.tsx │ ├── thread-dropdown.tsx │ ├── tool-detail-popup.tsx │ ├── tool-invocation │ │ ├── bar-chart.tsx │ │ ├── line-chart.tsx │ │ ├── pie-chart.tsx │ │ └── utils.ts │ ├── tool-mode-dropdown.tsx │ ├── tool-select-dropdown.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── alert.tsx │ │ ├── auto-height.tsx │ │ ├── avatar.tsx │ │ ├── background-paths.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── command.tsx │ │ ├── dialog.tsx │ │ ├── discord-icon.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── example-placeholder.tsx │ │ ├── flip-words.tsx │ │ ├── gemini-icon.tsx │ │ ├── github-icon.tsx │ │ ├── google-icon.tsx │ │ ├── hover-card.tsx │ │ ├── input.tsx │ │ ├── json-view.tsx │ │ ├── label.tsx │ │ ├── mcp-icon.tsx │ │ ├── message-loading.tsx │ │ ├── openai-icon.tsx │ │ ├── popover.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── shared-toast.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── think.tsx │ │ ├── toggle.tsx │ │ ├── tooltip.tsx │ │ └── write-icon.tsx ├── hooks │ ├── use-copy.ts │ ├── use-latest.ts │ ├── use-mobile.ts │ ├── use-mounted.ts │ ├── use-object-state.ts │ └── use-state-with-browserstorage.ts ├── i18n │ ├── get-locale.ts │ └── request.ts ├── instrumentation.ts ├── lib │ ├── ai │ │ ├── mcp │ │ │ ├── config-path.ts │ │ │ ├── create-mcp-client.ts │ │ │ ├── create-mcp-clients-manager.test.ts │ │ │ ├── create-mcp-clients-manager.ts │ │ │ ├── db-mcp-config-storage.test.ts │ │ │ ├── db-mcp-config-storage.ts │ │ │ ├── fb-mcp-config-storage.test.ts │ │ │ ├── fb-mcp-config-storage.ts │ │ │ ├── is-mcp-config.ts │ │ │ ├── mcp-config-diff.test.ts │ │ │ ├── mcp-config-diff.ts │ │ │ ├── mcp-manager.ts │ │ │ ├── mcp-tool-id.test.ts │ │ │ └── mcp-tool-id.ts │ │ ├── models.ts │ │ ├── prompts.ts │ │ ├── speech │ │ │ ├── index.ts │ │ │ └── open-ai │ │ │ │ ├── openai-realtime-event.ts │ │ │ │ └── use-voice-chat.openai.ts │ │ └── tools │ │ │ ├── create-bar-chart.ts │ │ │ ├── create-line-chart.ts │ │ │ ├── create-pie-chart.ts │ │ │ ├── index.ts │ │ │ └── utils.ts │ ├── auth │ │ ├── client.ts │ │ └── server.ts │ ├── browser-stroage.ts │ ├── cache │ │ ├── cache-keys.ts │ │ ├── cache.interface.ts │ │ ├── index.ts │ │ ├── memory-cache.test.ts │ │ ├── memory-cache.ts │ │ └── redis-cache.ts │ ├── const.ts │ ├── db │ │ ├── migrations │ │ │ └── pg │ │ │ │ ├── 0000_past_nebula.sql │ │ │ │ ├── 0001_slimy_tarot.sql │ │ │ │ ├── 0002_numerous_power_man.sql │ │ │ │ ├── 0003_hesitant_firedrake.sql │ │ │ │ ├── 0004_oval_silverclaw.sql │ │ │ │ ├── 0005_mushy_harpoon.sql │ │ │ │ ├── 0006_married_marvel_boy.sql │ │ │ │ └── meta │ │ │ │ ├── 0000_snapshot.json │ │ │ │ ├── 0001_snapshot.json │ │ │ │ ├── 0002_snapshot.json │ │ │ │ ├── 0003_snapshot.json │ │ │ │ ├── 0004_snapshot.json │ │ │ │ ├── 0005_snapshot.json │ │ │ │ ├── 0006_snapshot.json │ │ │ │ └── _journal.json │ │ ├── pg │ │ │ ├── db.pg.ts │ │ │ ├── migrate.pg.ts │ │ │ ├── repositories │ │ │ │ ├── chat-repository.pg.ts │ │ │ │ ├── mcp-repository.pg.ts │ │ │ │ ├── mcp-server-customization-repository.pg.ts │ │ │ │ ├── mcp-tool-customization-repository.pg.ts │ │ │ │ └── user-repository.pg.ts │ │ │ └── schema.pg.ts │ │ ├── repository.ts │ │ └── utils.ts │ ├── errors.ts │ ├── fuzzy-search.test.ts │ ├── fuzzy-search.ts │ ├── keyboard-shortcuts.ts │ ├── load-env.ts │ ├── logger.ts │ ├── utils.test.ts │ └── utils.ts ├── middleware.ts └── types │ ├── chat.ts │ ├── global.d.ts │ ├── mcp.ts │ └── user.ts ├── tsconfig.json ├── vitest.config.ts └── vitest.setup.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | Dockerfile 5 | compose.yml 6 | /node_modules 7 | /.pnp 8 | .pnp.* 9 | .yarn/* 10 | !.yarn/patches 11 | !.yarn/plugins 12 | !.yarn/releases 13 | !.yarn/versions 14 | 15 | 16 | # testing 17 | /coverage 18 | 19 | # next.js 20 | /.next/ 21 | /out/ 22 | 23 | # production 24 | /build 25 | 26 | # misc 27 | .DS_Store 28 | *.pem 29 | *.local.* 30 | 31 | # debug 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | .pnpm-debug.log* 36 | 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | next-env.d.ts 44 | .local-cache 45 | 46 | # memory-bank 47 | /memory-bank 48 | 49 | local-data 50 | 51 | .cursorrules 52 | .cursor 53 | *.ignore 54 | .mcp-config.json 55 | .next 56 | .changeset -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # === LLM Provider API Keys === 2 | # You only need to enter the keys for the providers you plan to use 3 | GOOGLE_GENERATIVE_AI_API_KEY=**** 4 | OPENAI_API_KEY=**** 5 | XAI_API_KEY=**** 6 | ANTHROPIC_API_KEY=**** 7 | OPENROUTER_API_KEY=**** 8 | OLLAMA_BASE_URL=http://localhost:11434/api 9 | 10 | 11 | # Secret for Better Auth (generate with: npx @better-auth/cli@latest secret) 12 | BETTER_AUTH_SECRET=**** 13 | 14 | # (Optional) 15 | # URL for Better Auth (the URL you access the app from) 16 | BETTER_AUTH_URL= 17 | 18 | # === Database === 19 | # If you don't have PostgreSQL running locally, start it with: pnpm docker:pg 20 | POSTGRES_URL=postgres://your_username:your_password@localhost:5432/your_database_name 21 | 22 | # Whether to use file-based MCP config (default: false) 23 | FILE_BASED_MCP_CONFIG=false 24 | 25 | # (Optional) 26 | # === OAuth Settings === 27 | # Fill in these values only if you want to enable Google/GitHub login 28 | GOOGLE_CLIENT_ID= 29 | GOOGLE_CLIENT_SECRET= 30 | GITHUB_CLIENT_ID= 31 | GITHUB_CLIENT_SECRET= 32 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "rules": { 4 | "@typescript-eslint/no-explicit-any": "off", 5 | "@typescript-eslint/no-unused-expressions": "off", 6 | "@typescript-eslint/no-unused-vars": "off", 7 | "react-hooks/exhaustive-deps": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/.dangerfile.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { danger, fail, warn, message } from "danger"; 3 | 4 | const prTitle = danger.github.pr.title; 5 | const conventionalRegex = 6 | /^(feat|fix|chore|docs|style|refactor|test|perf|build|ci|revert)(\(.+\))?!?: .+/; 7 | 8 | if (!conventionalRegex.test(prTitle)) { 9 | fail( 10 | `❌ The PR title does not follow the Conventional Commit format. 11 | 12 | **Current title:** "${prTitle}" 13 | 14 | **Expected formats:** 15 | - \`feat: add login functionality\` 16 | - \`fix: correct redirect bug\` 17 | - \`chore: update dependency xyz\` 18 | - \`feat(auth): add OAuth integration\` 19 | 20 | **Supported prefixes:** 21 | - \`feat\` - new features 22 | - \`fix\` - bug fixes 23 | - \`chore\` - maintenance tasks 24 | - \`docs\` - documentation changes 25 | - \`style\` - formatting changes 26 | - \`refactor\` - code refactoring 27 | - \`test\` - test additions/changes 28 | - \`perf\` - performance improvements 29 | - \`build\` - build system changes 30 | - \`ci\` - CI configuration changes 31 | - \`revert\` - reverting changes 32 | 33 | Please update your PR title to match one of these formats.`, 34 | ); 35 | } else { 36 | message("✅ PR title follows Conventional Commit format!"); 37 | } 38 | 39 | if (prTitle.length > 100) { 40 | warn("⚠️ PR title is quite long. Consider keeping it under 100 characters."); 41 | } 42 | 43 | if (prTitle.length < 10) { 44 | warn("⚠️ PR title seems too short. Consider being more descriptive."); 45 | } 46 | -------------------------------------------------------------------------------- /.github/release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "changelog-type": "github" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/lint-and-type-check.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Type Check 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | lint_and_type_check: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | 15 | - name: Set up Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: '20' 19 | 20 | - name: Install pnpm 21 | uses: pnpm/action-setup@v2 22 | with: 23 | version: 8 24 | run_install: false 25 | 26 | - name: Setup pnpm cache 27 | uses: actions/cache@v3 28 | with: 29 | path: ~/.pnpm-store 30 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 31 | restore-keys: | 32 | ${{ runner.os }}-pnpm-store- 33 | 34 | - name: Install dependencies 35 | run: pnpm install 36 | 37 | - name: Run Lint 38 | run: pnpm lint 39 | 40 | - name: Run Type Check 41 | run: pnpm check-types 42 | 43 | - name: Run Tests 44 | run: pnpm test 45 | -------------------------------------------------------------------------------- /.github/workflows/pr-check.yml: -------------------------------------------------------------------------------- 1 | name: PR Title Check 2 | permissions: 3 | contents: write 4 | pull-requests: write 5 | issues: write 6 | statuses: write 7 | checks: write 8 | on: 9 | pull_request: 10 | types: [opened, synchronize, reopened, edited] 11 | 12 | jobs: 13 | danger: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Install Node.js 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: 20 24 | 25 | - name: Run Danger 26 | run: npx danger ci --dangerfile=./.github/.dangerfile.ts 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | permissions: 6 | contents: write 7 | pull-requests: write 8 | 9 | name: release-please 10 | 11 | jobs: 12 | release-please: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: googleapis/release-please-action@v4 16 | with: 17 | # this assumes that you have created a personal access token 18 | # (PAT) and configured it as a GitHub action secret named 19 | # `MY_RELEASE_PLEASE_TOKEN` (this secret name is not important). 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | # this is a built-in strategy in release-please, see "Action Inputs" 22 | # for more options 23 | release-type: node 24 | config-file: ./.github/release-please-config.json 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | *.local.* 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | .pnpm-debug.log* 33 | 34 | # env files (can opt-in for committing if needed) 35 | .env 36 | .env.* 37 | !.env.example 38 | 39 | # vercel 40 | .vercel 41 | 42 | # typescript 43 | *.tsbuildinfo 44 | next-env.d.ts 45 | .local-cache 46 | 47 | # memory-bank 48 | /memory-bank 49 | 50 | local-data 51 | 52 | .cursorrules 53 | .cursor 54 | *.ignore 55 | .mcp-config.json -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm exec lint-staged 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["biomejs.biome"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "[javascript]": { 4 | "editor.defaultFormatter": "biomejs.biome" 5 | }, 6 | "[typescript]": { 7 | "editor.defaultFormatter": "biomejs.biome" 8 | }, 9 | "[typescriptreact]": { 10 | "editor.defaultFormatter": "biomejs.biome" 11 | }, 12 | "typescript.tsdk": "node_modules/typescript/lib", 13 | "eslint.workingDirectories": [ 14 | { "pattern": "app/*" }, 15 | { "pattern": "packages/*" } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to MCP Client Chatbot 2 | 3 | Thank you for your interest in contributing to MCP Client Chatbot! We welcome contributions from the community and truly appreciate your effort to improve the project. 4 | 5 | --- 6 | 7 | ## Getting Started 8 | 9 | 1. **Fork this repository** on GitHub. 10 | 11 | 2. **Clone your fork** locally: 12 | 13 | ```bash 14 | git clone https://github.com/YOUR_USERNAME/mcp-client-chatbot.git 15 | cd mcp-client-chatbot 16 | ``` 17 | 18 | 3. **Create a new branch** for your changes: 19 | 20 | ```bash 21 | git checkout -b feature/your-feature-name 22 | # or 23 | git checkout -b fix/your-bug-fix 24 | ``` 25 | 26 | 4. **Implement your changes**, following the existing code style and structure. 27 | 28 | 5. **Test your changes thoroughly**: 29 | 30 | ```bash 31 | pnpm dev 32 | pnpm test 33 | ``` 34 | 35 | --- 36 | 37 | ## Releasing and PR Title Rules 38 | 39 | We use [Release Please](https://github.com/googleapis/release-please) to automate GitHub releases. 40 | **Only the Pull Request title** needs to follow the [Conventional Commits](https://www.conventionalcommits.org/) format. Commit messages can be written freely. 41 | 42 | ### ✅ PR Title Examples 43 | 44 | * `fix: voice chat audio not initializing` 45 | * `feat: support multi-language UI toggle` 46 | * `chore: update dependencies` 47 | 48 | ### ⚠️ Important Notes 49 | 50 | * PR **titles must start** with one of the following prefixes: 51 | 52 | ``` 53 | feat: ... 54 | fix: ... 55 | chore: ... 56 | docs: ... 57 | style: ... 58 | refactor: ... 59 | test: ... 60 | perf: ... 61 | build: ... 62 | ``` 63 | 64 | * Only the PR title is used for changelog and versioning 65 | 66 | * We use **squash merge** to keep the history clean 67 | 68 | * Changelog entries and GitHub Releases are **automatically generated** after merging 69 | 70 | --- 71 | 72 | ## Submitting a Pull Request 73 | 74 | 1. **Format your code**: 75 | 76 | ```bash 77 | pnpm lint:fix 78 | ``` 79 | 80 | 2. **Commit and push**: 81 | 82 | ```bash 83 | git add . 84 | git commit -m "your internal message" 85 | git push origin your-branch-name 86 | ``` 87 | 88 | 3. **Open a Pull Request**: 89 | 90 | * **Title**: Must follow the Conventional Commit format 91 | * **Description**: Explain what you changed and why 92 | * Link to related issues, if any 93 | * Include **screenshots or demos** for any UI changes 94 | 95 | --- 96 | 97 | ## Thank You 98 | 99 | We sincerely appreciate your contribution to MCP Client Chatbot. 100 | Let’s build a powerful and lightweight AI experience together! 🚀 101 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 mcp-client-chatbot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": true, 5 | "clientKind": "git", 6 | "useIgnoreFile": true, 7 | "defaultBranch": "main" 8 | }, 9 | "files": { 10 | "ignoreUnknown": false, 11 | "ignore": ["node_modules", ".next", "public", "docker", "dist/**", "*.d.ts"] 12 | }, 13 | "formatter": { 14 | "enabled": true, 15 | "formatWithErrors": false, 16 | "indentStyle": "space", 17 | "indentWidth": 2, 18 | "lineEnding": "lf", 19 | "lineWidth": 80, 20 | "attributePosition": "auto" 21 | }, 22 | "organizeImports": { "enabled": true }, 23 | "linter": { 24 | "enabled": true, 25 | "rules": { 26 | "recommended": false, 27 | "complexity": { "noUselessTypeConstraint": "error" }, 28 | "correctness": { 29 | "noUnusedVariables": "error", 30 | "useArrayLiterals": "off", 31 | "useExhaustiveDependencies": "off" 32 | }, 33 | "style": { "noNamespace": "error", "useAsConstAssertion": "error" }, 34 | "suspicious": { 35 | "noExplicitAny": "off", 36 | "noExtraNonNullAssertion": "error", 37 | "noMisleadingInstantiator": "error", 38 | "noUnsafeDeclarationMerging": "error", 39 | "useNamespaceKeyword": "error" 40 | } 41 | } 42 | }, 43 | "javascript": { "formatter": { "quoteStyle": "double" } }, 44 | "overrides": [ 45 | { 46 | "include": ["*.ts", "*.tsx", "*.mts", "*.cts"], 47 | "linter": { 48 | "rules": { 49 | "complexity": { "noWith": "off" }, 50 | "correctness": { 51 | "noConstAssign": "off", 52 | "noGlobalObjectCalls": "off", 53 | "noInvalidBuiltinInstantiation": "off", 54 | "noInvalidConstructorSuper": "off", 55 | "noNewSymbol": "off", 56 | "noSetterReturn": "off", 57 | "noUndeclaredVariables": "off", 58 | "noUnreachable": "off", 59 | "noUnreachableSuper": "off" 60 | }, 61 | "style": { 62 | "noArguments": "error", 63 | "noVar": "error", 64 | "useConst": "error" 65 | }, 66 | "suspicious": { 67 | "noClassAssign": "off", 68 | "noDuplicateClassMembers": "off", 69 | "noDuplicateObjectKeys": "off", 70 | "noDuplicateParameters": "off", 71 | "noFunctionAssign": "off", 72 | "noImportAssign": "off", 73 | "noRedeclare": "off", 74 | "noUnsafeNegation": "off", 75 | "useGetterReturn": "off" 76 | } 77 | } 78 | } 79 | } 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /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": "", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 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 | } 22 | -------------------------------------------------------------------------------- /custom-mcp-server/index.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { z } from "zod"; 4 | 5 | const server = new McpServer({ 6 | name: "custom-mcp-server", 7 | version: "0.0.1", 8 | }); 9 | 10 | server.tool( 11 | "get_weather", 12 | "Get the current weather at a location.", 13 | { 14 | latitude: z.number(), 15 | longitude: z.number(), 16 | }, 17 | async ({ latitude, longitude }) => { 18 | const response = await fetch( 19 | `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m&hourly=temperature_2m&daily=sunrise,sunset&timezone=auto`, 20 | ); 21 | const data = await response.json(); 22 | return { 23 | content: [ 24 | { 25 | type: "text", 26 | text: `The current temperature in ${latitude}, ${longitude} is ${data.current.temperature_2m}°C.`, 27 | }, 28 | { 29 | type: "text", 30 | text: `The sunrise in ${latitude}, ${longitude} is ${data.daily.sunrise[0]} and the sunset is ${data.daily.sunset[0]}.`, 31 | }, 32 | ], 33 | }; 34 | }, 35 | ); 36 | 37 | const transport = new StdioServerTransport(); 38 | 39 | await server.connect(transport); 40 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:23-alpine AS builder 2 | ARG DOCKER_BUILD="1" 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | # Install pnpm 8 | RUN npm install -g pnpm 9 | 10 | RUN pnpm install --frozen-lockfile 11 | 12 | RUN pnpm build 13 | 14 | FROM node:23-alpine AS runner 15 | 16 | WORKDIR /app 17 | 18 | RUN npm install -g pnpm 19 | 20 | RUN apk add --no-cache curl bash \ 21 | && curl -fsSL https://bun.sh/install | bash \ 22 | && ln -s /root/.bun/bin/bun /usr/local/bin/bun 23 | 24 | # Add the UV installation steps 25 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ 26 | 27 | # Copy the build output from the builder stage 28 | COPY --from=builder /app/.next ./.next 29 | COPY --from=builder /app/node_modules ./node_modules 30 | COPY --from=builder /app/package.json ./package.json 31 | COPY --from=builder /app/public ./public/ 32 | COPY --from=builder /app/tsconfig.json ./tsconfig.json 33 | COPY --from=builder /app/scripts/db-migrate.ts ./scripts/db-migrate.ts 34 | COPY --from=builder /app/src/lib/db/pg/migrate.pg.ts ./src/lib/db/pg/migrate.pg.ts 35 | COPY --from=builder /app/src/lib/utils.ts ./src/lib/utils.ts 36 | COPY --from=builder /app/src/lib/load-env.ts ./src/lib/load-env.ts 37 | COPY --from=builder /app/src/lib/db/migrations ./src/lib/db/migrations 38 | COPY --from=builder /app/src/types/chat.ts ./src/types/chat.ts 39 | COPY --from=builder /app/.env ./.env 40 | COPY --from=builder /app/messages ./messages 41 | EXPOSE 3000 42 | 43 | CMD pnpm db:migrate && pnpm start -------------------------------------------------------------------------------- /docker/compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | mcp-client-chatbot: 3 | build: 4 | context: .. 5 | dockerfile: ./docker/Dockerfile 6 | ports: 7 | - '3000:3000' 8 | environment: 9 | - NO_HTTPS=1 10 | env_file: 11 | - .env.docker # docker .env 12 | - .env 13 | dns: 14 | - 8.8.8.8 # Google's public DNS server 15 | - 8.8.4.4 # Google's public DNS server 16 | networks: 17 | - mcp_client_network 18 | depends_on: 19 | - postgres 20 | restart: unless-stopped 21 | 22 | postgres: 23 | image: postgres:17 24 | env_file: 25 | - .env.docker # docker .env 26 | - .env 27 | networks: 28 | - mcp_client_network 29 | volumes: 30 | - postgres_data:/var/lib/postgresql/data 31 | restart: unless-stopped 32 | 33 | volumes: 34 | postgres_data: 35 | 36 | networks: 37 | mcp_client_network: 38 | driver: bridge 39 | -------------------------------------------------------------------------------- /docs/tips-guides/docker.md: -------------------------------------------------------------------------------- 1 | # Using Docker 2 | 3 | Docker provides a streamlined and efficient method for managing containerized applications, making it an ideal choice for deploying this project. 4 | 5 | ## Requirements 6 | 7 | - **Architecture:** An x86-64 or ARM(64) based computer. 8 | - **Operating System:** Linux, macOS (with Docker Desktop or equivalent), or Windows (with WSL). 9 | - **Software:** Docker and Docker Compose installed and configured. 10 | 11 | ## Steps 12 | 13 | 1. **Clone the Repository:** 14 | Navigate to the desired directory in your terminal and clone the project repository. If you're not already in the project directory after cloning, change into it: 15 | 16 | ```sh 17 | git clone https://github.com/cgoinglove/mcp-client-chatbot 18 | cd mcp-client-chatbot 19 | ``` 20 | 21 | 2. **Set up Environment Variables:** 22 | Run `pnpm initial:env` to generate the `.env` file. 23 | Then, enter the API keys only for the LLM providers you plan to use. 24 | 25 | You can generate an authentication secret (`BETTER_AUTH_SECRET`) with the command: 26 | `pnpx auth secret` 27 | 28 | For the database, Docker will handle all necessary configuration automatically, 29 | so the default `docker/.env` file is sufficient. 30 | 31 | 32 | 33 | 1. **Build and Start the Container:** 34 | From the project's root directory, build the Docker image and start the container in detached mode (running in the background): 35 | 36 | ```sh 37 | pnpm docker-compose:up 38 | ``` 39 | 40 | Your application should now be running. You can access it by visiting `http://:3000/` in your web browser. Replace `` with the IP address of the server where Docker is running (this will likely be `localhost` if you're running it on your local machine). 41 | 42 | ## Using your own database 43 | 44 | If you don't want to host your own db, here are some steps 45 | 46 | 1. Open up your docker compose file. `docker/compose.yml` 47 | Comment out the postgres section and the volume 48 | 2. Update `.env` change your DB url 49 | 3. Migrate the DB 50 | 51 | ```sh 52 | pnpm db:migrate 53 | ``` 54 | 55 | 4. Run the app 56 | 57 | ```sh 58 | pnpm docker-compose:up 59 | ``` 60 | 61 | ## What is possible in docker and what is not 62 | 63 | - Full support for MCP stdio servers that work with bunx, uvx and npx. 64 | - Full support for SSE,Streamable Remote servers. 65 | - And everything else as you would expect. 66 | 67 | ## Managing the Container 68 | 69 | ### Stopping the Container 70 | 71 | To stop the running container, ensure you are in the project's root directory and execute: 72 | 73 | ```sh 74 | pnpm docker-compose:down 75 | ``` 76 | 77 | ### Updating the Application 78 | 79 | To update the application to the latest version: 80 | 81 | ```sh 82 | pnpm docker-compose:update 83 | ``` 84 | -------------------------------------------------------------------------------- /docs/tips-guides/mcp-server-setup-and-tool-testing.md: -------------------------------------------------------------------------------- 1 | # 🔧 MCP Server Configuration Guide 2 | 3 | > This guide explains how to add MCP servers by defining their configuration in JSON format. Each MCP server entry is stored in the database and supports different transport types: `stdio`, `SSE`, and `StreamableHTTP`. 4 | 5 | You can add new MCP servers effortlessly through the UI — no need to restart the app. Each tool is available instantly and can be tested independently outside of chat. This is perfect for quick debugging and reliable development workflows. 6 | 7 | ![add-mcp-server](https://github.com/user-attachments/assets/f66ae118-883e-4638-b4fc-9f9849566da2) 8 | 9 |
10 | 11 | ## 🖥️ Stdio Type 12 | 13 | Used for locally executed tools that run via a command-line interface. 14 | 15 | **Example:** 16 | 17 | ```json 18 | { 19 | "command": "npx", 20 | "args": ["@playwright/mcp@latest"] 21 | } 22 | ``` 23 | 24 | - `command`: Required. The CLI command to launch the server. 25 | - `args`: Optional. A list of arguments to pass to the command. 26 | 27 | ## 🌐 SSE / StreamableHTTP Type 28 | 29 | Used for remote servers that communicate via HTTP (SSE or streaming). 30 | 31 | **Example:** 32 | 33 | ```json 34 | { 35 | "url": "https://api.example.com", 36 | "headers": { 37 | "Authorization": "Bearer sk-..." 38 | } 39 | } 40 | ``` 41 | 42 | - `url`: Required. The endpoint to connect to. 43 | - `headers`: Optional. HTTP headers such as authorization tokens. 44 | 45 | You don't need to specify the transport type manually — it is inferred based on the structure: 46 | 47 | - If `command` is present → it's a `stdio` config 48 | - If `url` is present → it's a `SSE` or `StreamableHTTP` config 49 | 50 | ## 💾 File-based Configuration (for local dev) 51 | 52 | By default, MCP server configs are stored in the database. 53 | However, for local development, you can also use a file-based approach by enabling the following setting: 54 | 55 | ```env 56 | # Whether to use file-based MCP config (default: false) 57 | FILE_BASED_MCP_CONFIG=true 58 | ``` 59 | 60 | Then, create a `.mcp-config.json` file in the project root and define your servers there. Example: 61 | 62 | ```jsonc 63 | // .mcp-config.json 64 | { 65 | "playwright": { 66 | "command": "npx", 67 | "args": ["@playwright/mcp@latest"] 68 | } 69 | } 70 | ``` 71 | 72 | Simply paste your configuration in the MCP Configuration form (or .mcp-config.json) to register a new tool. 73 | -------------------------------------------------------------------------------- /docs/tips-guides/oauth.md: -------------------------------------------------------------------------------- 1 | ## Social Login Setup (Google & GitHub, English) 2 | 3 | ### Get your Google credentials 4 | To use Google as a social provider, you need to get your Google credentials. You can get them by creating a new project in the [Google Cloud Console](https://console.cloud.google.com/apis/dashboard). 5 | 6 | - In the Google Cloud Console, go to **APIs & Services > Credentials**. 7 | - Click **Create Credentials** and select **OAuth client ID**. 8 | - Choose **Web application** as the application type. 9 | - In **Authorized redirect URIs**, set: 10 | - For local development: `http://localhost:3000/api/auth/callback/google` 11 | - For production: your deployed application's URL, e.g. `https://example.com/api/auth/callback/google` 12 | - If you change the base path of your authentication routes, update the redirect URL accordingly. 13 | - After creation, copy your **Client ID** and **Client Secret** and add them to your `.env` file: 14 | ``` 15 | GOOGLE_CLIENT_ID=your_client_id 16 | GOOGLE_CLIENT_SECRET=your_client_secret 17 | ``` 18 | 19 | ### Get your GitHub credentials 20 | To use GitHub sign in, you need a client ID and client secret. You can get them from the [GitHub Developer Portal](https://github.com/settings/developers). 21 | 22 | - For local development, set the redirect URL to: 23 | - `http://localhost:3000/api/auth/callback/github` 24 | - For production, set it to your deployed application's URL, e.g.: 25 | - `https://your-domain.com/api/auth/callback/github` 26 | - If you change the base path of your authentication routes, make sure to update the redirect URL accordingly. 27 | - **Important:** You MUST include the `user:email` scope in your GitHub app to ensure the application can access the user's email address. 28 | - Add your credentials to your `.env` file: 29 | ``` 30 | GITHUB_CLIENT_ID=your_client_id 31 | GITHUB_CLIENT_SECRET=your_client_secret 32 | ``` 33 | 34 | ## Environment Variable Check 35 | 36 | Make sure your `.env` file contains the following variables: 37 | 38 | ``` 39 | GOOGLE_CLIENT_ID=your_google_client_id 40 | GOOGLE_CLIENT_SECRET=your_google_client_secret 41 | GITHUB_CLIENT_ID=your_github_client_id 42 | GITHUB_CLIENT_SECRET=your_github_client_secret 43 | ``` 44 | 45 | ## Done 46 | 47 | You can now sign in to MCP Client Chatbot using your Google or GitHub account. Restart the application to apply the changes. -------------------------------------------------------------------------------- /docs/tips-guides/project_with_mcp.md: -------------------------------------------------------------------------------- 1 | # 🧠 Building a Project Agent with MCP Client Chatbot 2 | 3 | You can turn the MCP Client Chatbot into a powerful agent by combining a **Project**, a custom **System Prompt**, and the **Tool Preset** feature. This is similar to how OpenAI and Claude structure their Project features — a way to start conversations with context tailored to a specific task or domain. 4 | 5 | --- 6 | 7 | ## 🛠️ How It Works 8 | 9 | * A **Project** stores reusable instructions (system prompt) that are injected into every chat started within that project. 10 | * A **Tool Preset** can be used independently to scope the available tools, but it is not directly bound to a project. 11 | 12 | By using both together, you can create an effective workflow-specific assistant. 13 | 14 | --- 15 | 16 | ## 📦 Example: Managing the `mcp-client-chatbot` Repository 17 | 18 | Let’s say you want to build an assistant for managing the `mcp-client-chatbot` GitHub repository: 19 | 20 | 1. **Create a Project** named `mcp-client-chatbot` 21 | 2. In the system prompt, include: 22 | 23 | * A description of the project 24 | * Technologies used 25 | * That the assistant should behave like a contributor 26 | * A brief instruction on how to use the GitHub MCP server 27 | 3. Separately, create a **Tool Preset** including 10–15 GitHub tools like `list_issues`, `comment_on_issue`, `merge_pr`, etc. 28 | 29 | Now, any chat created under the `mcp-client-chatbot` project will always start with that system prompt. If the user enables the corresponding Tool Preset, the assistant becomes a specialized project agent. 30 | 31 | --- 32 | 33 | ## 💬 Example Interaction 34 | 35 | **User:** Check recent issues 36 | **Agent:** (retrieves and summarizes latest issues in the GitHub repository) 37 | 38 | **User:** Write a comment explaining the benefit 39 | **Agent:** (generates and posts a comment via GitHub MCP tool) 40 | 41 | --- 42 | 43 | This setup lets you: 44 | 45 | * Reuse task-specific context across chats 46 | * Focus tools and behavior for a given project 47 | * Reduce repetitive setup and instructions 48 | 49 | > 💡 Great for workflows like translation, repo management, documentation review, or anything with clear structure and goals. 50 | -------------------------------------------------------------------------------- /docs/tips-guides/temporary_chat.md: -------------------------------------------------------------------------------- 1 | # 💬 Temporary Chat Guide 2 | 3 | Temporary Chat allows you to interact with the assistant in a lightweight, popup-style chat — without creating a new thread or saving any messages. It's perfect for side questions, quick tests, or one-off tasks. 4 | 5 | ![temporarily](https://github.com/user-attachments/assets/e0c9874c-e06a-4d2b-a630-1871c6fe3a69) 6 | 7 | 8 | ## 🔄 How It Works 9 | 10 | * Open or close the temporary chat anytime using the shortcut (`⌘K`) or the button in the top right corner. 11 | * You can reset the temporary chat with a shortcut (`⌘E`). 12 | * It appears as a **right-side sliding panel** so you can keep your main chat open. 13 | * Messages sent here are **not saved** to history — once closed, they're gone. 14 | * Temporary chat supports its own **System Prompt**, allowing you to customize behavior. 15 | 16 | systemprompt 17 | 18 | 19 | ## 🧠 Why It's Useful 20 | 21 | Imagine you're in the middle of an important discussion and you suddenly need a quick translation or to test a tool — but don't want to clutter the current chat. Temporary Chat is built exactly for that. 22 | 23 | **Use cases include:** 24 | 25 | * Asking a quick side question without breaking the main conversation flow 26 | * Testing a prompt or tool before using it in your main chat 27 | * Setting up a dedicated system prompt for focused tasks (e.g. translation, formatting) 28 | 29 | ## 📝 Real-World Example 30 | 31 | For example, if you are not fluent in English, you can set a translation-focused system prompt in the temporary chat (e.g. "Translate everything naturally between Korean and English without extra explanation"). While having a main conversation, whenever you need a translation, you can quickly open the temporary chat using the shortcut, get the translation you need to understand the context, and then return to your main chat to continue the discussion. 32 | 33 | --- 34 | 35 | Temporary Chat helps keep your main conversations clean, while still giving you flexibility and speed for all your side tasks. 36 | -------------------------------------------------------------------------------- /docs/tips-guides/vercel.md: -------------------------------------------------------------------------------- 1 | # Vercel Deployment Guide 2 | 3 | 1. **Click this button** to start the deployment process: 4 | 5 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/cgoinglove/mcp-client-chatbot&env=OPENAI_API_KEY&env=BETTER_AUTH_SECRET&envDescription=Learn+more+about+how+to+get+the+API+Keys+for+the+application&envLink=https://github.com/cgoinglove/mcp-client-chatbot/blob/main/.env.example&demo-title=MCP+Client+Chatbot&demo-description=An+Open-Source+MCP+Chatbot+Template+Built+With+Next.js+and+the+AI+SDK+by+Vercel.&products=[{"type":"integration","protocol":"storage","productSlug":"neon","integrationSlug":"neon"}]) 6 | 7 | 2. **Click the "Create" button** on Vercel to begin setting up your project. 8 | 9 | step2 10 | 11 | 12 | 3. **Add Neon Postgres and create a database.** 13 | Click the "Neon Postgres Add" button to create a database. This is available even on the free plan. 14 | 15 | step3 16 | 17 | 18 | 4. **Add Environment Variables.** 19 | You can enter placeholder values for the environment variables at this stage, or use the actual values if you have them ready. If you prefer, you can enter temporary values now and update them later in the project's settings after deployment is complete. However, **BETTER_AUTH_SECRET** must be set at this stage. 20 | 21 | - You can generate an BETTER_AUTH_SECRET [here](https://auth-secret-gen.vercel.app/). 22 | 23 | step4 24 | 25 | 26 | 5. **Deployment and Project Creation.** 27 | Once the above steps are complete, deployment will begin automatically and your project will be created. 28 | 29 | 6. **Enter LLM Provider API Keys in Project Settings.** 30 | After deployment, go to your project's **Settings > Environments** tab. Here, enter the API keys for the LLM providers you wish to use. You only need to enter the keys for the providers you actually plan to use—other fields can be left blank or filled with dummy values. 31 | 32 | - Example environment file: [example.env](https://github.com/cgoinglove/mcp-client-chatbot/blob/main/.env.example) 33 | 34 | step6 35 | 36 | 37 | ## Notes 38 | 39 | - Only Remote(sse, streamable) MCP servers are supported (STDIO-based servers are not). 40 | - If you need STDIO support, consider using Docker or Render. 41 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit"; 2 | import "load-env"; 3 | 4 | const dialect = "postgresql"; 5 | 6 | const url = process.env.POSTGRES_URL!; 7 | 8 | const schema = "./src/lib/db/pg/schema.pg.ts"; 9 | 10 | const out = "./src/lib/db/migrations/pg"; 11 | 12 | export default defineConfig({ 13 | schema, 14 | out, 15 | dialect, 16 | migrations: {}, 17 | dbCredentials: { 18 | url, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /messages/language.md: -------------------------------------------------------------------------------- 1 | # Adding a New Language 2 | 3 | To add a new language to the application, follow these simple steps: 4 | 5 | 1. Copy the `messages/en.json` file and rename it to match your language code (e.g., `fr.json` for French). 6 | 7 | 2. Translate all content in the file to your target language while maintaining the same JSON structure and keys. 8 | 9 | 3. Add your language to the `SUPPORTED_LOCALES` array in `src/lib/const.ts` file: 10 | 11 | ```typescript 12 | export const SUPPORTED_LOCALES = [ 13 | { 14 | code: "en", 15 | name: "English 🇺🇸", 16 | }, 17 | { 18 | code: "ko", 19 | name: "Korean 🇰🇷", 20 | }, 21 | { 22 | code: "your-language-code", 23 | name: "Your Language Name 🏴", 24 | }, 25 | ]; 26 | ``` 27 | 28 | The language will then be available in the language selector of the application. 29 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | import createNextIntlPlugin from "next-intl/plugin"; 3 | export default () => { 4 | const nextConfig: NextConfig = { 5 | cleanDistDir: true, 6 | devIndicators: { 7 | position: "bottom-right", 8 | }, 9 | env: { 10 | NO_HTTPS: process.env.NO_HTTPS, 11 | NEXT_PUBLIC_GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, 12 | NEXT_PUBLIC_GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID, 13 | }, 14 | }; 15 | const withNextIntl = createNextIntlPlugin(); 16 | return withNextIntl(nextConfig); 17 | }; 18 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/pf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgoinglove/mcp-client-chatbot/176ff5d7fe8d3d4571dc6ffc2143491c4d2f36de/public/pf.png -------------------------------------------------------------------------------- /public/sounds/start_voice.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgoinglove/mcp-client-chatbot/176ff5d7fe8d3d4571dc6ffc2143491c4d2f36de/public/sounds/start_voice.ogg -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/clean.ts: -------------------------------------------------------------------------------- 1 | import { rimraf } from "rimraf"; 2 | 3 | async function clean(dirsToClean?: string[]) { 4 | try { 5 | console.log("🧹 Cleaning up..."); 6 | 7 | // Default directories to clean if none provided 8 | const defaultDirs = [".next", "node_modules"]; 9 | const dirs = 10 | dirsToClean && dirsToClean.length > 0 ? dirsToClean : defaultDirs; 11 | 12 | // Remove each specified directory 13 | for (const dir of dirs) { 14 | await rimraf(dir); 15 | console.log(`✅ Removed ${dir} directory`); 16 | } 17 | console.log("✨ Cleanup completed successfully!"); 18 | } catch (error) { 19 | console.error("❌ Error during cleanup:", error); 20 | process.exit(1); 21 | } 22 | } 23 | 24 | // Parse command line arguments, skip the first two (node and script path) 25 | const args = process.argv.slice(2); 26 | clean(args); 27 | -------------------------------------------------------------------------------- /scripts/db-migrate.ts: -------------------------------------------------------------------------------- 1 | import { colorize } from "consola/utils"; 2 | import "load-env"; 3 | 4 | const promise = import("lib/db/pg/migrate.pg"); 5 | 6 | await promise 7 | .then(() => { 8 | console.info("🚀 DB Migration completed"); 9 | }) 10 | .catch((err) => { 11 | console.error(err); 12 | 13 | console.warn( 14 | ` 15 | ${colorize("red", "🚨 Migration failed due to incompatible schema.")} 16 | 17 | ❗️DB Migration failed – incompatible schema detected. 18 | 19 | This version introduces a complete rework of the database schema. 20 | As a result, your existing database structure may no longer be compatible. 21 | 22 | **To resolve this:** 23 | 24 | 1. Drop all existing tables in your database. 25 | 2. Then run the following command to apply the latest schema: 26 | 27 | 28 | ${colorize("green", "pnpm db:migrate")} 29 | 30 | **Note:** This schema overhaul lays the foundation for more stable updates moving forward. 31 | You shouldn’t have to do this kind of reset again in future releases. 32 | 33 | Need help? Open an issue on GitHub 🙏 34 | `.trim(), 35 | ); 36 | 37 | process.exit(1); 38 | }); 39 | -------------------------------------------------------------------------------- /scripts/initial-env.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | 4 | // Get current directory path 5 | const ROOT = process.cwd(); 6 | 7 | const DOCKER_ENV_PATH = path.join(ROOT, "docker", ".env.docker"); 8 | 9 | const DOCKER_ENV_CONTENT = 10 | [ 11 | "POSTGRES_URL=postgres://your_username:your_password@postgres:5432/your_database_name", 12 | "POSTGRES_DB=your_database_name", 13 | "POSTGRES_USER=your_username", 14 | "POSTGRES_PASSWORD=your_password", 15 | ].join("\n") + "\n"; 16 | 17 | /** 18 | * Copy .env.example to .env if .env doesn't exist 19 | */ 20 | function copyEnvFile() { 21 | const envPath = path.join(ROOT, ".env"); 22 | const envExamplePath = path.join(ROOT, ".env.example"); 23 | 24 | if (!fs.existsSync(envPath)) { 25 | try { 26 | console.warn(".env file not found. Copying from .env.example..."); 27 | fs.copyFileSync(envExamplePath, envPath); 28 | console.log(".env file has been created."); 29 | console.warn( 30 | "Important: You may need to edit the .env file to set your API keys.", 31 | ); 32 | } catch (error) { 33 | console.error("Error occurred while creating .env file."); 34 | console.error(error); 35 | return false; 36 | } 37 | } else { 38 | console.info(".env file already exists. Skipping..."); 39 | } 40 | 41 | if (!fs.existsSync(DOCKER_ENV_PATH)) { 42 | try { 43 | fs.writeFileSync(DOCKER_ENV_PATH, DOCKER_ENV_CONTENT, "utf-8"); 44 | console.log("/docker/.env file has been created."); 45 | } catch (error) { 46 | console.error("Error occurred while creating /docker/.env file."); 47 | console.error(error); 48 | return false; 49 | } 50 | } else { 51 | console.info("/docker/.env file already exists. Skipping..."); 52 | } 53 | 54 | return true; 55 | } 56 | 57 | // Execute copy operation 58 | const result = copyEnvFile(); 59 | process.exit(result ? 0 : 1); 60 | -------------------------------------------------------------------------------- /scripts/postinstall.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process"; 2 | import { IS_VERCEL_ENV, IS_DOCKER_ENV, FILE_BASED_MCP_CONFIG } from "lib/const"; 3 | import { promisify } from "util"; 4 | import "load-env"; 5 | const execPromise = promisify(exec); 6 | 7 | async function runCommand(command: string, description: string) { 8 | console.log(`Starting: ${description}`); 9 | try { 10 | const { stdout, stderr } = await execPromise(command, { 11 | cwd: process.cwd(), 12 | env: process.env, 13 | }); 14 | 15 | console.log(`${description} output:`); 16 | console.log(stdout); 17 | 18 | if (stderr) { 19 | console.error(`${description} stderr:`); 20 | console.error(stderr); 21 | } 22 | console.log(`${description} finished successfully.`); 23 | } catch (error: any) { 24 | console.error(`${description} error:`, error); 25 | process.exit(1); 26 | } 27 | } 28 | 29 | async function main() { 30 | if (IS_VERCEL_ENV) { 31 | if (FILE_BASED_MCP_CONFIG) { 32 | console.error("File based MCP config is not supported on Vercel."); 33 | process.exit(1); 34 | } 35 | console.log("Running on Vercel, performing database migration."); 36 | await runCommand("pnpm db:migrate", "Database migration"); 37 | } else if (IS_DOCKER_ENV) { 38 | if (FILE_BASED_MCP_CONFIG) { 39 | console.error("File based MCP config is not supported in Docker."); 40 | process.exit(1); 41 | } 42 | console.log("Running in Docker, nothing to do."); 43 | } else { 44 | console.log( 45 | "Running in a normal environment, performing initial environment setup.", 46 | ); 47 | await runCommand("pnpm initial:env", "Initial environment setup"); 48 | } 49 | } 50 | 51 | main(); 52 | -------------------------------------------------------------------------------- /src/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Think } from "ui/think"; 2 | import { getTranslations } from "next-intl/server"; 3 | import { FlipWords } from "ui/flip-words"; 4 | import { BackgroundPaths } from "ui/background-paths"; 5 | 6 | export default async function AuthLayout({ 7 | children, 8 | }: { children: React.ReactNode }) { 9 | const t = await getTranslations("Auth.Intro"); 10 | return ( 11 |
12 |
13 |
14 |
15 |
16 | 17 |
18 |

19 | 20 | 21 | Chat Bot 22 |

23 |
24 | 28 |
29 | 30 |
{children}
31 |
32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/app/(chat)/chat/[thread]/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return
; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/(chat)/chat/[thread]/page.tsx: -------------------------------------------------------------------------------- 1 | import { selectThreadWithMessagesAction } from "@/app/api/chat/actions"; 2 | import ChatBot from "@/components/chat-bot"; 3 | 4 | import { ChatMessage, ChatThread } from "app-types/chat"; 5 | import { convertToUIMessage } from "lib/utils"; 6 | import { redirect } from "next/navigation"; 7 | 8 | const fetchThread = async ( 9 | threadId: string, 10 | ): Promise<(ChatThread & { messages: ChatMessage[] }) | null> => { 11 | const response = await selectThreadWithMessagesAction(threadId); 12 | if (!response) return null; 13 | return response; 14 | }; 15 | 16 | export default async function Page({ 17 | params, 18 | }: { params: Promise<{ thread: string }> }) { 19 | const { thread: threadId } = await params; 20 | 21 | const thread = await fetchThread(threadId); 22 | 23 | if (!thread) redirect("/"); 24 | 25 | const initialMessages = thread.messages.map(convertToUIMessage); 26 | 27 | return ( 28 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/app/(chat)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { SidebarProvider } from "ui/sidebar"; 2 | import { AppSidebar } from "@/components/layouts/app-sidebar"; 3 | import { AppHeader } from "@/components/layouts/app-header"; 4 | import { cookies } from "next/headers"; 5 | 6 | import { getSession } from "auth/server"; 7 | import { redirect } from "next/navigation"; 8 | import { COOKIE_KEY_SIDEBAR_STATE } from "lib/const"; 9 | import { AppPopupProvider } from "@/components/layouts/app-popup-provider"; 10 | 11 | export default async function ChatLayout({ 12 | children, 13 | }: { children: React.ReactNode }) { 14 | const [session, cookieStore] = await Promise.all([getSession(), cookies()]); 15 | if (!session) { 16 | return redirect("/sign-in"); 17 | } 18 | const isCollapsed = 19 | cookieStore.get(COOKIE_KEY_SIDEBAR_STATE)?.value !== "true"; 20 | return ( 21 | 22 | 23 | 24 |
25 | 26 |
{children}
27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/app/(chat)/mcp/create/page.tsx: -------------------------------------------------------------------------------- 1 | import MCPEditor from "@/components/mcp-editor"; 2 | import { ArrowLeft } from "lucide-react"; 3 | import Link from "next/link"; 4 | import { getTranslations } from "next-intl/server"; 5 | 6 | export default async function Page() { 7 | const t = await getTranslations(); 8 | return ( 9 |
10 |
11 | 15 | 16 | {t("Common.back")} 17 | 18 |
19 |

20 | {t("MCP.mcpConfiguration")} 21 |

22 |

23 | {t("MCP.configureYourMcpServerConnectionSettings")} 24 |

25 |
26 | 27 |
28 | 29 |
30 |
31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/app/(chat)/mcp/modify/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import MCPEditor from "@/components/mcp-editor"; 2 | import { Alert } from "ui/alert"; 3 | import Link from "next/link"; 4 | import { ArrowLeft } from "lucide-react"; 5 | import { getTranslations } from "next-intl/server"; 6 | import { mcpRepository } from "lib/db/repository"; 7 | import { redirect } from "next/navigation"; 8 | 9 | export default async function Page({ 10 | params, 11 | }: { params: Promise<{ id: string }> }) { 12 | const { id } = await params; 13 | const t = await getTranslations(); 14 | const mcpClient = await mcpRepository.selectById(id); 15 | 16 | if (!mcpClient) { 17 | return redirect("/mcp"); 18 | } 19 | 20 | return ( 21 |
22 |
23 | 27 | 28 | {t("Common.back")} 29 | 30 |
31 |

32 | {t("MCP.mcpConfiguration")} 33 |

34 |

35 | {t("MCP.configureYourMcpServerConnectionSettings")} 36 |

37 |
38 | 39 |
40 | {mcpClient ? ( 41 | 46 | ) : ( 47 | MCP client not found 48 | )} 49 |
50 |
51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/app/(chat)/mcp/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { MCPCard } from "@/components/mcp-card"; 3 | import { appStore } from "@/app/store"; 4 | import { Button } from "@/components/ui/button"; 5 | import Link from "next/link"; 6 | import { MCPOverview } from "@/components/mcp-overview"; 7 | import { selectMcpClientsAction } from "@/app/api/mcp/actions"; 8 | import useSWR from "swr"; 9 | import { Skeleton } from "ui/skeleton"; 10 | 11 | import { handleErrorWithToast } from "ui/shared-toast"; 12 | import { ScrollArea } from "ui/scroll-area"; 13 | import { useTranslations } from "next-intl"; 14 | import { MCPIcon } from "ui/mcp-icon"; 15 | 16 | export default function Page() { 17 | const appStoreMutate = appStore((state) => state.mutate); 18 | const t = useTranslations("MCP"); 19 | const { data: mcpList, isLoading } = useSWR( 20 | "mcp-list", 21 | selectMcpClientsAction, 22 | { 23 | refreshInterval: 10000, 24 | fallbackData: [], 25 | onError: handleErrorWithToast, 26 | onSuccess: (data) => appStoreMutate({ mcpList: data }), 27 | }, 28 | ); 29 | 30 | return ( 31 | 32 |
33 |
34 |

MCP Servers

35 |
36 | 37 |
38 | 43 | 46 | 47 | 48 | 52 | 53 |
54 |
55 | {isLoading ? ( 56 |
57 | 58 | 59 | 60 |
61 | ) : mcpList?.length ? ( 62 |
63 | {mcpList.map((mcp) => ( 64 | 65 | ))} 66 |
67 | ) : ( 68 | // When MCP list is empty 69 | 70 | )} 71 |
72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /src/app/(chat)/page.tsx: -------------------------------------------------------------------------------- 1 | import ChatBot from "@/components/chat-bot"; 2 | import { generateUUID } from "lib/utils"; 3 | 4 | export const dynamic = "force-dynamic"; 5 | 6 | export default function HomePage() { 7 | const id = generateUUID(); 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/api/auth/[...all]/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "auth/server"; 2 | import { toNextJsHandler } from "better-auth/next-js"; 3 | 4 | export const { GET, POST } = toNextJsHandler(auth.handler); 5 | -------------------------------------------------------------------------------- /src/app/api/auth/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { userRepository } from "lib/db/repository"; 4 | 5 | export async function existsByEmailAction(email: string) { 6 | const exists = await userRepository.existsByEmail(email); 7 | return exists; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/api/chat/[threadId]/route.ts: -------------------------------------------------------------------------------- 1 | import { chatRepository } from "lib/db/repository"; 2 | import { NextRequest } from "next/server"; 3 | import { generateTitleFromUserMessageAction } from "../actions"; 4 | 5 | import { customModelProvider } from "lib/ai/models"; 6 | import { getSession } from "auth/server"; 7 | 8 | export async function POST( 9 | request: NextRequest, 10 | { params }: { params: Promise<{ threadId: string }> }, 11 | ) { 12 | const session = await getSession(); 13 | if (!session) { 14 | return new Response("Unauthorized", { status: 401 }); 15 | } 16 | const { threadId } = await params; 17 | const { messages, model, projectId } = await request.json(); 18 | 19 | let thread = await chatRepository.selectThread(threadId); 20 | if (!thread) { 21 | const title = await generateTitleFromUserMessageAction({ 22 | message: messages[0], 23 | model: customModelProvider.getModel(model), 24 | }); 25 | thread = await chatRepository.insertThread({ 26 | id: threadId, 27 | projectId: projectId ?? null, 28 | title, 29 | userId: session.user.id, 30 | }); 31 | } 32 | if (thread.userId !== session.user.id) { 33 | return new Response("Forbidden", { status: 403 }); 34 | } 35 | await chatRepository.insertMessages( 36 | messages.map((message) => ({ 37 | ...message, 38 | threadId: thread.id, 39 | createdAt: message.createdAt ? new Date(message.createdAt) : undefined, 40 | })), 41 | ); 42 | return new Response( 43 | JSON.stringify({ 44 | success: true, 45 | }), 46 | { 47 | status: 200, 48 | }, 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/app/api/chat/summarize/route.ts: -------------------------------------------------------------------------------- 1 | import { convertToCoreMessages, smoothStream, streamText } from "ai"; 2 | import { selectThreadWithMessagesAction } from "../actions"; 3 | import { customModelProvider } from "lib/ai/models"; 4 | import { SUMMARIZE_PROMPT } from "lib/ai/prompts"; 5 | import logger from "logger"; 6 | 7 | export async function POST(request: Request) { 8 | try { 9 | const json = await request.json(); 10 | const { threadId, model: modelName } = json as { 11 | threadId: string; 12 | model: string; 13 | }; 14 | 15 | const thread = await selectThreadWithMessagesAction(threadId); 16 | 17 | if (!thread) { 18 | return new Response("Thread not found", { status: 404 }); 19 | } 20 | 21 | const messages = convertToCoreMessages( 22 | thread.messages 23 | .map((v) => ({ 24 | content: "", 25 | role: v.role, 26 | parts: v.parts, 27 | })) 28 | .concat({ 29 | content: "", 30 | parts: [ 31 | { 32 | type: "text", 33 | text: "Generate a system prompt based on the conversation so far according to the rules.", 34 | }, 35 | ], 36 | role: "user", 37 | }), 38 | ); 39 | 40 | const result = streamText({ 41 | model: customModelProvider.getModel(modelName), 42 | system: SUMMARIZE_PROMPT, 43 | experimental_transform: smoothStream({ chunking: "word" }), 44 | messages, 45 | }); 46 | 47 | return result.toDataStreamResponse(); 48 | } catch (error) { 49 | logger.error(error); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/api/chat/temporary/route.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { getSession } from "auth/server"; 3 | import { Message, smoothStream, streamText } from "ai"; 4 | import { customModelProvider } from "lib/ai/models"; 5 | import logger from "logger"; 6 | import { buildUserSystemPrompt } from "lib/ai/prompts"; 7 | import { userRepository } from "lib/db/repository"; 8 | 9 | export async function POST(request: Request) { 10 | try { 11 | const json = await request.json(); 12 | 13 | const session = await getSession(); 14 | 15 | if (!session?.user.id) { 16 | return redirect("/sign-in"); 17 | } 18 | 19 | const { 20 | messages, 21 | model: modelName, 22 | instructions, 23 | } = json as { 24 | messages: Message[]; 25 | model: string; 26 | instructions?: string; 27 | }; 28 | 29 | const model = customModelProvider.getModel(modelName); 30 | 31 | const userPreferences = 32 | (await userRepository.getPreferences(session.user.id)) || undefined; 33 | 34 | return streamText({ 35 | model, 36 | system: `${buildUserSystemPrompt(session.user, userPreferences)} ${ 37 | instructions ? `\n\n${instructions}` : "" 38 | }`.trim(), 39 | messages, 40 | maxSteps: 10, 41 | experimental_continueSteps: true, 42 | experimental_transform: smoothStream({ chunking: "word" }), 43 | }).toDataStreamResponse(); 44 | } catch (error: any) { 45 | logger.error(error); 46 | return new Response(error.message || "Oops, an error occured!", { 47 | status: 500, 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/api/mcp/server-customizations/[server]/route.ts: -------------------------------------------------------------------------------- 1 | import { McpServerCustomizationZodSchema } from "app-types/mcp"; 2 | import { getSession } from "auth/server"; 3 | import { serverCache } from "lib/cache"; 4 | import { CacheKeys } from "lib/cache/cache-keys"; 5 | import { mcpServerCustomizationRepository } from "lib/db/repository"; 6 | 7 | import { NextResponse } from "next/server"; 8 | 9 | export async function GET( 10 | _: Request, 11 | { params }: { params: Promise<{ server: string }> }, 12 | ) { 13 | const { server } = await params; 14 | const session = await getSession(); 15 | if (!session) { 16 | return new Response("Unauthorized", { status: 401 }); 17 | } 18 | const mcpServerCustomization = 19 | await mcpServerCustomizationRepository.selectByUserIdAndMcpServerId({ 20 | mcpServerId: server, 21 | userId: session.user.id, 22 | }); 23 | 24 | return NextResponse.json(mcpServerCustomization ?? {}); 25 | } 26 | 27 | export async function POST( 28 | request: Request, 29 | { params }: { params: Promise<{ server: string }> }, 30 | ) { 31 | const { server } = await params; 32 | const session = await getSession(); 33 | if (!session) { 34 | return new Response("Unauthorized", { status: 401 }); 35 | } 36 | 37 | const body = await request.json(); 38 | const { mcpServerId, prompt } = McpServerCustomizationZodSchema.parse({ 39 | ...body, 40 | mcpServerId: server, 41 | }); 42 | 43 | const result = 44 | await mcpServerCustomizationRepository.upsertMcpServerCustomization({ 45 | userId: session.user.id, 46 | mcpServerId, 47 | prompt, 48 | }); 49 | const key = CacheKeys.mcpServerCustomizations(session.user.id); 50 | void serverCache.delete(key); 51 | 52 | return NextResponse.json(result); 53 | } 54 | 55 | export async function DELETE( 56 | _: Request, 57 | { params }: { params: Promise<{ server: string }> }, 58 | ) { 59 | const { server } = await params; 60 | const session = await getSession(); 61 | if (!session) { 62 | return new Response("Unauthorized", { status: 401 }); 63 | } 64 | 65 | await mcpServerCustomizationRepository.deleteMcpServerCustomizationByMcpServerIdAndUserId( 66 | { 67 | mcpServerId: server, 68 | userId: session.user.id, 69 | }, 70 | ); 71 | const key = CacheKeys.mcpServerCustomizations(session.user.id); 72 | void serverCache.delete(key); 73 | 74 | return NextResponse.json({ success: true }); 75 | } 76 | -------------------------------------------------------------------------------- /src/app/api/mcp/tool-customizations/[server]/[tool]/route.ts: -------------------------------------------------------------------------------- 1 | import { McpToolCustomizationZodSchema } from "app-types/mcp"; 2 | import { getSession } from "auth/server"; 3 | import { serverCache } from "lib/cache"; 4 | import { CacheKeys } from "lib/cache/cache-keys"; 5 | import { mcpMcpToolCustomizationRepository } from "lib/db/repository"; 6 | 7 | export async function GET( 8 | _: Request, 9 | { params }: { params: Promise<{ server: string; tool: string }> }, 10 | ) { 11 | const { server, tool } = await params; 12 | const session = await getSession(); 13 | if (!session) { 14 | return new Response("Unauthorized", { status: 401 }); 15 | } 16 | 17 | const result = await mcpMcpToolCustomizationRepository.select({ 18 | mcpServerId: server, 19 | userId: session.user.id, 20 | toolName: tool, 21 | }); 22 | return Response.json(result ?? {}); 23 | } 24 | 25 | export async function POST( 26 | request: Request, 27 | { params }: { params: Promise<{ server: string; tool: string }> }, 28 | ) { 29 | const { server, tool } = await params; 30 | const session = await getSession(); 31 | if (!session) { 32 | return new Response("Unauthorized", { status: 401 }); 33 | } 34 | 35 | const body = await request.json(); 36 | 37 | const { mcpServerId, toolName, prompt } = McpToolCustomizationZodSchema.parse( 38 | { 39 | ...body, 40 | mcpServerId: server, 41 | toolName: tool, 42 | }, 43 | ); 44 | 45 | const result = 46 | await mcpMcpToolCustomizationRepository.upsertToolCustomization({ 47 | userId: session.user.id, 48 | mcpServerId, 49 | toolName, 50 | prompt, 51 | }); 52 | const key = CacheKeys.mcpServerCustomizations(session.user.id); 53 | void serverCache.delete(key); 54 | 55 | return Response.json(result); 56 | } 57 | 58 | export async function DELETE( 59 | _: Request, 60 | { params }: { params: Promise<{ server: string; tool: string }> }, 61 | ) { 62 | const { server, tool } = await params; 63 | const session = await getSession(); 64 | if (!session) { 65 | return new Response("Unauthorized", { status: 401 }); 66 | } 67 | 68 | await mcpMcpToolCustomizationRepository.deleteToolCustomization({ 69 | mcpServerId: server, 70 | userId: session.user.id, 71 | toolName: tool, 72 | }); 73 | const key = CacheKeys.mcpServerCustomizations(session.user.id); 74 | void serverCache.delete(key); 75 | 76 | return Response.json({ success: true }); 77 | } 78 | -------------------------------------------------------------------------------- /src/app/api/mcp/tool-customizations/[server]/route.ts: -------------------------------------------------------------------------------- 1 | import { getSession } from "auth/server"; 2 | import { mcpMcpToolCustomizationRepository } from "lib/db/repository"; 3 | 4 | import { NextResponse } from "next/server"; 5 | 6 | export async function GET( 7 | _: Request, 8 | { params }: { params: Promise<{ server: string }> }, 9 | ) { 10 | const { server } = await params; 11 | const session = await getSession(); 12 | if (!session) { 13 | return new Response("Unauthorized", { status: 401 }); 14 | } 15 | const mcpServerCustomization = 16 | await mcpMcpToolCustomizationRepository.selectByUserIdAndMcpServerId({ 17 | mcpServerId: server, 18 | userId: session.user.id, 19 | }); 20 | 21 | return NextResponse.json(mcpServerCustomization); 22 | } 23 | -------------------------------------------------------------------------------- /src/app/api/thread/route.ts: -------------------------------------------------------------------------------- 1 | import { chatRepository } from "lib/db/repository"; 2 | import { getSession } from "auth/server"; 3 | import { redirect } from "next/navigation"; 4 | import { generateUUID } from "lib/utils"; 5 | import { generateTitleFromUserMessageAction } from "../chat/actions"; 6 | 7 | export async function POST(request: Request) { 8 | const { id, projectId, message, model } = await request.json(); 9 | 10 | const session = await getSession(); 11 | 12 | if (!session?.user.id) { 13 | return redirect("/sign-in"); 14 | } 15 | 16 | const title = await generateTitleFromUserMessageAction({ 17 | message, 18 | model, 19 | }); 20 | 21 | const newThread = await chatRepository.insertThread({ 22 | id: id ?? generateUUID(), 23 | projectId, 24 | title, 25 | userId: session.user.id, 26 | }); 27 | 28 | return Response.json({ 29 | threadId: newThread.id, 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /src/app/api/user/preferences/route.ts: -------------------------------------------------------------------------------- 1 | import { getSession } from "auth/server"; 2 | import { UserPreferencesZodSchema } from "app-types/user"; 3 | import { userRepository } from "lib/db/repository"; 4 | import { NextResponse } from "next/server"; 5 | 6 | export async function GET() { 7 | try { 8 | const session = await getSession(); 9 | 10 | if (!session?.user?.id) { 11 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 12 | } 13 | const preferences = await userRepository.getPreferences(session.user.id); 14 | return NextResponse.json(preferences ?? {}); 15 | } catch (error: any) { 16 | return NextResponse.json( 17 | { error: error.message || "Failed to get preferences" }, 18 | { status: 500 }, 19 | ); 20 | } 21 | } 22 | 23 | export async function PUT(request: Request) { 24 | try { 25 | const session = await getSession(); 26 | if (!session?.user?.id) { 27 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 28 | } 29 | const json = await request.json(); 30 | const preferences = UserPreferencesZodSchema.parse(json); 31 | const updatedUser = await userRepository.updatePreferences( 32 | session.user.id, 33 | preferences, 34 | ); 35 | return NextResponse.json({ 36 | success: true, 37 | preferences: updatedUser.preferences, 38 | }); 39 | } catch (error: any) { 40 | return NextResponse.json( 41 | { error: error.message || "Failed to update preferences" }, 42 | { status: 500 }, 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgoinglove/mcp-client-chatbot/176ff5d7fe8d3d4571dc6ffc2143491c4d2f36de/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | import { ThemeProvider } from "@/components/layouts/theme-provider"; 5 | import { Toaster } from "ui/sonner"; 6 | import { BASE_THEMES } from "lib/const"; 7 | import { NextIntlClientProvider } from "next-intl"; 8 | import { getLocale } from "next-intl/server"; 9 | const geistSans = Geist({ 10 | variable: "--font-geist-sans", 11 | subsets: ["latin"], 12 | }); 13 | const geistMono = Geist_Mono({ 14 | variable: "--font-geist-mono", 15 | subsets: ["latin"], 16 | }); 17 | 18 | export const metadata: Metadata = { 19 | title: "MCP Chat", 20 | description: 21 | "MCP Chat is a chatbot that uses the MCP Tools to answer questions.", 22 | }; 23 | 24 | const themes = BASE_THEMES.flatMap((t) => [t, `${t}-dark`]); 25 | 26 | export default async function RootLayout({ 27 | children, 28 | }: Readonly<{ 29 | children: React.ReactNode; 30 | }>) { 31 | const locale = await getLocale(); 32 | return ( 33 | 34 | 37 | 43 | 44 |
45 | {children} 46 | 47 |
48 |
49 |
50 | 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/app/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return
; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/chat-greeting.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion } from "framer-motion"; 4 | import { authClient } from "auth/client"; 5 | import { useMemo } from "react"; 6 | import { FlipWords } from "ui/flip-words"; 7 | import { useTranslations } from "next-intl"; 8 | 9 | function getGreetingByTime() { 10 | const hour = new Date().getHours(); 11 | if (hour < 12) return "goodMorning"; 12 | if (hour < 18) return "goodAfternoon"; 13 | return "goodEvening"; 14 | } 15 | 16 | export const ChatGreeting = () => { 17 | const { data: session } = authClient.useSession(); 18 | 19 | const t = useTranslations("Chat.Greeting"); 20 | 21 | const user = session?.user; 22 | 23 | const word = useMemo(() => { 24 | if (!user?.name) return ""; 25 | const words = [ 26 | t(getGreetingByTime(), { name: user.name }), 27 | t("niceToSeeYouAgain", { name: user.name }), 28 | t("whatAreYouWorkingOnToday", { name: user.name }), 29 | t("letMeKnowWhenYoureReadyToBegin"), 30 | t("whatAreYourThoughtsToday"), 31 | t("whereWouldYouLikeToStart"), 32 | t("whatAreYouThinking", { name: user.name }), 33 | ]; 34 | return words[Math.floor(Math.random() * words.length)]; 35 | }, [user?.name]); 36 | 37 | return ( 38 | 46 |
47 |

48 | {word ? : ""} 49 |

50 |
51 |
52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /src/components/json-view-popup.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useCopy } from "@/hooks/use-copy"; 3 | import { cn, isString } from "lib/utils"; 4 | import { Check, Copy } from "lucide-react"; 5 | import { ReactNode } from "react"; 6 | import { Button } from "ui/button"; 7 | import { 8 | Dialog, 9 | DialogContent, 10 | DialogHeader, 11 | DialogTitle, 12 | DialogTrigger, 13 | } from "ui/dialog"; 14 | import JsonView from "ui/json-view"; 15 | 16 | export function JsonViewPopup({ 17 | data, 18 | open, 19 | onOpenChange, 20 | children, 21 | }: { 22 | data?: any; 23 | open?: boolean; 24 | onOpenChange?: (open: boolean) => void; 25 | children?: ReactNode; 26 | }) { 27 | const { copied, copy } = useCopy(); 28 | return ( 29 | 30 | 31 | {children || ( 32 | 39 | )} 40 | 41 | 42 | 43 | JSON 44 | 45 | 46 |
47 | 55 | 56 |
57 |
58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/components/keyboard-shortcuts-popup.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | getShortcutKeyList, 5 | isShortcutEvent, 6 | Shortcuts, 7 | } from "lib/keyboard-shortcuts"; 8 | 9 | import { 10 | Dialog, 11 | DialogContent, 12 | DialogDescription, 13 | DialogTitle, 14 | } from "ui/dialog"; 15 | import { useTranslations } from "next-intl"; 16 | import { useShallow } from "zustand/shallow"; 17 | import { appStore } from "@/app/store"; 18 | import { useEffect } from "react"; 19 | 20 | export function KeyboardShortcutsPopup({}) { 21 | const [openShortcutsPopup, appStoreMutate] = appStore( 22 | useShallow((state) => [state.openShortcutsPopup, state.mutate]), 23 | ); 24 | const t = useTranslations("KeyboardShortcuts"); 25 | 26 | useEffect(() => { 27 | const handleKeyDown = (e: KeyboardEvent) => { 28 | if (isShortcutEvent(e, Shortcuts.openShortcutsPopup)) { 29 | e.preventDefault(); 30 | e.stopPropagation(); 31 | appStoreMutate((prev) => ({ 32 | openShortcutsPopup: !prev.openShortcutsPopup, 33 | })); 34 | } 35 | }; 36 | window.addEventListener("keydown", handleKeyDown); 37 | return () => window.removeEventListener("keydown", handleKeyDown); 38 | }, []); 39 | 40 | return ( 41 | 44 | appStoreMutate({ openShortcutsPopup: !openShortcutsPopup }) 45 | } 46 | > 47 | 48 | {t("title")} 49 | 50 |
51 | {Object.entries(Shortcuts).map(([key, shortcut]) => ( 52 |
56 |

{t(shortcut.description ?? "")}

57 |
58 | {getShortcutKeyList(shortcut).map((key) => { 59 | return ( 60 |
64 | {key} 65 |
66 | ); 67 | })} 68 |
69 | ))} 70 |
71 | 72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /src/components/layouts/app-popup-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import dynamic from "next/dynamic"; 4 | 5 | const KeyboardShortcutsPopup = dynamic( 6 | () => 7 | import("@/components/keyboard-shortcuts-popup").then( 8 | (mod) => mod.KeyboardShortcutsPopup, 9 | ), 10 | { 11 | ssr: false, 12 | }, 13 | ); 14 | 15 | const ChatPreferencesPopup = dynamic( 16 | () => 17 | import("@/components/chat-preferences-popup").then( 18 | (mod) => mod.ChatPreferencesPopup, 19 | ), 20 | { 21 | ssr: false, 22 | }, 23 | ); 24 | 25 | const ChatBotVoice = dynamic( 26 | () => import("@/components/chat-bot-voice").then((mod) => mod.ChatBotVoice), 27 | { 28 | ssr: false, 29 | }, 30 | ); 31 | 32 | const ChatBotTemporary = dynamic( 33 | () => 34 | import("@/components/chat-bot-temporary").then( 35 | (mod) => mod.ChatBotTemporary, 36 | ), 37 | { 38 | ssr: false, 39 | }, 40 | ); 41 | 42 | const McpCustomizationPopup = dynamic( 43 | () => 44 | import("@/components/mcp-customization-popup").then( 45 | (mod) => mod.McpCustomizationPopup, 46 | ), 47 | { 48 | ssr: false, 49 | }, 50 | ); 51 | export function AppPopupProvider() { 52 | return ( 53 | <> 54 | 55 | 56 | 57 | 58 | 59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/components/layouts/app-sidebar-menus.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { SidebarMenuButton, useSidebar } from "ui/sidebar"; 3 | import { Tooltip } from "ui/tooltip"; 4 | import { SidebarMenu, SidebarMenuItem } from "ui/sidebar"; 5 | import { SidebarGroupContent } from "ui/sidebar"; 6 | 7 | import { SidebarGroup } from "ui/sidebar"; 8 | import { TooltipProvider } from "ui/tooltip"; 9 | import Link from "next/link"; 10 | import { getShortcutKeyList, Shortcuts } from "lib/keyboard-shortcuts"; 11 | import { useRouter } from "next/navigation"; 12 | import { useTranslations } from "next-intl"; 13 | import { MCPIcon } from "ui/mcp-icon"; 14 | import { WriteIcon } from "ui/write-icon"; 15 | 16 | export function AppSidebarMenus() { 17 | const router = useRouter(); 18 | const t = useTranslations("Layout"); 19 | const { setOpenMobile } = useSidebar(); 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | { 30 | e.preventDefault(); 31 | setOpenMobile(false); 32 | router.push(`/`); 33 | router.refresh(); 34 | }} 35 | > 36 | 37 | 38 | {t("newChat")} 39 |
40 | {getShortcutKeyList(Shortcuts.openNewChat).map((key) => ( 41 | 45 | {key} 46 | 47 | ))} 48 |
49 |
50 | 51 |
52 |
53 |
54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | {t("mcpConfiguration")} 63 | 64 | 65 | 66 | 67 | 68 | 69 |
70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/components/layouts/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | 6 | export function ThemeProvider({ 7 | children, 8 | ...props 9 | }: React.ComponentProps) { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/mcp-overview.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowUpRight } from "lucide-react"; 2 | import Link from "next/link"; 3 | import { useTranslations } from "next-intl"; 4 | import { MCPIcon } from "ui/mcp-icon"; 5 | 6 | export function MCPOverview() { 7 | const t = useTranslations("MCP"); 8 | 9 | return ( 10 | 14 | 15 |
16 |

17 | 18 | {t("overviewTitle")} 19 |

20 | 21 |

22 | {t("overviewDescription")} 23 |

24 | 25 |
26 | {t("addMcpServer")} 27 | 28 |
29 |
30 | 31 | ); 32 | } 33 | 34 | const calculateHeight = (index: number, total: number) => { 35 | const position = index / (total - 1); 36 | const maxHeight = 100; 37 | const minHeight = 30; 38 | 39 | const center = 0.5; 40 | const distanceFromCenter = Math.abs(position - center); 41 | const heightPercentage = Math.pow(distanceFromCenter * 2, 1.2); 42 | 43 | return minHeight + (maxHeight - minHeight) * heightPercentage; 44 | }; 45 | 46 | const GradientBars: React.FC = () => { 47 | const length = 15; 48 | return ( 49 |
50 |
59 | {Array.from({ length }).map((_, index) => { 60 | const height = calculateHeight(index, length); 61 | return ( 62 |
78 | ); 79 | })} 80 |
81 |
82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /src/components/select-model.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Fragment, PropsWithChildren, useState } from "react"; 4 | 5 | import { 6 | Command, 7 | CommandEmpty, 8 | CommandGroup, 9 | CommandInput, 10 | CommandItem, 11 | CommandList, 12 | CommandSeparator, 13 | } from "ui/command"; 14 | import { Popover, PopoverContent, PopoverTrigger } from "ui/popover"; 15 | 16 | interface SelectModelProps { 17 | onSelect: (model: string) => void; 18 | align?: "start" | "end"; 19 | providers: { 20 | provider: string; 21 | models: { name: string; isToolCallUnsupported: boolean }[]; 22 | }[]; 23 | model: string; 24 | } 25 | 26 | export const SelectModel = (props: PropsWithChildren) => { 27 | const [open, setOpen] = useState(false); 28 | 29 | return ( 30 | 31 | {props.children} 32 | 33 | e.stopPropagation()} 37 | > 38 | 39 | 40 | No results found. 41 | {props.providers.map((provider, i) => ( 42 | 43 | { 47 | e.stopPropagation(); 48 | }} 49 | > 50 | {provider.models.map((model) => ( 51 | { 55 | props.onSelect(model.name); 56 | setOpen(false); 57 | }} 58 | value={model.name} 59 | > 60 | {model.name} 61 | {model.isToolCallUnsupported && ( 62 |
63 | No tools 64 |
65 | )} 66 |
67 | ))} 68 |
69 | {i < props.providers.length - 1 && } 70 |
71 | ))} 72 |
73 |
74 |
75 |
76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /src/components/tool-invocation/utils.ts: -------------------------------------------------------------------------------- 1 | export const sanitizeCssVariableName = (label: string) => { 2 | return label 3 | .replaceAll(" ", "") 4 | .toLowerCase() 5 | .replace(/[^a-z0-9\-_]/g, "_"); 6 | }; 7 | -------------------------------------------------------------------------------- /src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"; 5 | import { ChevronDownIcon } from "lucide-react"; 6 | 7 | import { cn } from "lib/utils"; 8 | 9 | function Accordion({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return ; 13 | } 14 | 15 | function AccordionItem({ 16 | className, 17 | ...props 18 | }: React.ComponentProps) { 19 | return ( 20 | 25 | ); 26 | } 27 | 28 | function AccordionTrigger({ 29 | className, 30 | children, 31 | ...props 32 | }: React.ComponentProps) { 33 | return ( 34 | 35 | svg]:rotate-180", 39 | className, 40 | )} 41 | {...props} 42 | > 43 | {children} 44 | 45 | 46 | 47 | ); 48 | } 49 | 50 | function AccordionContent({ 51 | className, 52 | children, 53 | ...props 54 | }: React.ComponentProps) { 55 | return ( 56 | 61 |
{children}
62 |
63 | ); 64 | } 65 | 66 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; 67 | -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import type * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "lib/utils"; 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-card text-card-foreground", 12 | destructive: 13 | "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | }, 20 | ); 21 | 22 | function Alert({ 23 | className, 24 | variant, 25 | ...props 26 | }: React.ComponentProps<"div"> & VariantProps) { 27 | return ( 28 |
34 | ); 35 | } 36 | 37 | function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { 38 | return ( 39 |
47 | ); 48 | } 49 | 50 | function AlertDescription({ 51 | className, 52 | ...props 53 | }: React.ComponentProps<"div">) { 54 | return ( 55 |
63 | ); 64 | } 65 | 66 | export { Alert, AlertTitle, AlertDescription }; 67 | -------------------------------------------------------------------------------- /src/components/ui/auto-height.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion"; 2 | import { ReactNode, useRef, useLayoutEffect, useState } from "react"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | interface AutoHeightProps { 6 | children: ReactNode; 7 | className?: string; 8 | duration?: number; 9 | ease?: string; 10 | } 11 | 12 | export function AutoHeight({ 13 | children, 14 | className, 15 | duration = 0.2, 16 | ease = "easeInOut", 17 | }: AutoHeightProps) { 18 | const [height, setHeight] = useState("auto"); 19 | const contentRef = useRef(null); 20 | 21 | useLayoutEffect(() => { 22 | if (contentRef.current) { 23 | const resizeObserver = new ResizeObserver(() => { 24 | if (contentRef.current) { 25 | const newHeight = contentRef.current.scrollHeight; 26 | setHeight(newHeight); 27 | } 28 | }); 29 | 30 | resizeObserver.observe(contentRef.current); 31 | 32 | // Initial height measurement 33 | const initialHeight = contentRef.current.scrollHeight; 34 | setHeight(initialHeight); 35 | 36 | return () => { 37 | resizeObserver.disconnect(); 38 | }; 39 | } 40 | }, [children]); 41 | 42 | return ( 43 | 51 |
{children}
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type * as React from "react"; 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 5 | 6 | import { cn } from "lib/utils"; 7 | 8 | function Avatar({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ); 22 | } 23 | 24 | function AvatarImage({ 25 | className, 26 | ...props 27 | }: React.ComponentProps) { 28 | return ( 29 | 34 | ); 35 | } 36 | 37 | function AvatarFallback({ 38 | className, 39 | ...props 40 | }: React.ComponentProps) { 41 | return ( 42 | 50 | ); 51 | } 52 | 53 | export { Avatar, AvatarImage, AvatarFallback }; 54 | -------------------------------------------------------------------------------- /src/components/ui/background-paths.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion } from "framer-motion"; 4 | 5 | function FloatingPaths({ position }: { position: number }) { 6 | const paths = Array.from({ length: 36 }, (_, i) => ({ 7 | id: i, 8 | d: `M-${380 - i * 5 * position} -${189 + i * 6}C-${ 9 | 380 - i * 5 * position 10 | } -${189 + i * 6} -${312 - i * 5 * position} ${216 - i * 6} ${ 11 | 152 - i * 5 * position 12 | } ${343 - i * 6}C${616 - i * 5 * position} ${470 - i * 6} ${ 13 | 684 - i * 5 * position 14 | } ${875 - i * 6} ${684 - i * 5 * position} ${875 - i * 6}`, 15 | color: `rgba(15,23,42,${0.1 + i * 0.03})`, 16 | width: 0.5 + i * 0.03, 17 | })); 18 | 19 | return ( 20 |
21 | 26 | Background Paths 27 | {paths.map((path) => ( 28 | 46 | ))} 47 | 48 |
49 | ); 50 | } 51 | 52 | export function BackgroundPaths() { 53 | return ( 54 |
55 |
56 | 57 | 58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "lib/utils"; 6 | 7 | const badgeVariants = cva( 8 | "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", 14 | secondary: 15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", 16 | destructive: 17 | "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 18 | outline: 19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", 20 | }, 21 | }, 22 | defaultVariants: { 23 | variant: "default", 24 | }, 25 | }, 26 | ); 27 | 28 | function Badge({ 29 | className, 30 | variant, 31 | asChild = false, 32 | ...props 33 | }: React.ComponentProps<"span"> & 34 | VariantProps & { asChild?: boolean }) { 35 | const Comp = asChild ? Slot : "span"; 36 | 37 | return ( 38 | 43 | ); 44 | } 45 | 46 | export { Badge, badgeVariants }; 47 | -------------------------------------------------------------------------------- /src/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import type * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { ChevronRight, MoreHorizontal } from "lucide-react"; 4 | 5 | import { cn } from "lib/utils"; 6 | 7 | function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { 8 | return