├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── claude.yml │ ├── issue-triage.yml │ ├── release.yml │ └── update-version.yml ├── .gitignore ├── .prettierrc ├── .vscode-test.mjs ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── CLAUDE.md ├── LICENSE ├── README.md ├── WHATS_NEW.md ├── demo.gif ├── esbuild.js ├── eslint.config.mjs ├── logo.png ├── package-lock.json ├── package.json ├── resources ├── dark │ └── server.svg └── light │ └── server.svg ├── src ├── McpAgent.ts ├── extension.ts ├── panels │ └── ExtensionPanel.ts ├── shared │ └── types │ │ └── rpcTypes.ts ├── telemetry │ └── index.ts ├── test │ └── extension.test.ts ├── types │ ├── octokit.d.ts │ └── registry.ts └── utilities │ ├── CopilotChat.ts │ ├── const.ts │ ├── getNonce.ts │ ├── getUri.ts │ ├── logging.ts │ ├── repoSearch.ts │ └── signoz.ts ├── tsconfig.json ├── vsc-extension-quickstart.md └── web ├── .gitignore ├── README.md ├── components.json ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── public └── vite.svg ├── src ├── App.css ├── App.tsx ├── assets │ └── react.svg ├── components │ ├── InstalledMCPServers.tsx │ ├── MCPServers.tsx │ ├── RepoCard.tsx │ ├── SearchMCPServers.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ └── tooltip.tsx ├── contexts │ └── VscodeApiContext.tsx ├── global.d.ts ├── hooks │ ├── use-mobile.ts │ └── useDebounce.ts ├── index.css ├── lib │ └── utils.ts ├── main.tsx └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── vscode-variables.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [VikashLoomba] 4 | buy_me_a_coffee: vikashloomba 5 | thanks_dev: gh/vikashloomba 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/workflows/claude.yml: -------------------------------------------------------------------------------- 1 | name: Claude Code 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | pull_request_review_comment: 7 | types: [created] 8 | issues: 9 | types: [opened, assigned] 10 | pull_request_review: 11 | types: [submitted] 12 | 13 | jobs: 14 | claude: 15 | if: | 16 | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || 17 | (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || 18 | (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || 19 | (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) 20 | runs-on: ubuntu-latest 21 | permissions: 22 | contents: read 23 | pull-requests: read 24 | issues: read 25 | id-token: write 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 1 31 | 32 | - name: Run Claude Code 33 | id: claude 34 | uses: anthropics/claude-code-action@beta 35 | with: 36 | anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} 37 | assignee_trigger: "claude" 38 | 39 | -------------------------------------------------------------------------------- /.github/workflows/issue-triage.yml: -------------------------------------------------------------------------------- 1 | name: Claude Issue Triage Example 2 | description: Run Claude Code for issue triage in GitHub Actions 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | jobs: 8 | triage-issue: 9 | runs-on: ubuntu-latest 10 | timeout-minutes: 10 11 | permissions: 12 | contents: read 13 | issues: write 14 | 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Setup GitHub MCP Server 22 | run: | 23 | mkdir -p /tmp/mcp-config 24 | cat > /tmp/mcp-config/mcp-servers.json << 'EOF' 25 | { 26 | "github": { 27 | "command": "docker", 28 | "args": [ 29 | "run", 30 | "-i", 31 | "--rm", 32 | "-e", 33 | "GITHUB_PERSONAL_ACCESS_TOKEN", 34 | "ghcr.io/github/github-mcp-server:sha-7aced2b" 35 | ], 36 | "env": { 37 | "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}" 38 | } 39 | } 40 | } 41 | EOF 42 | 43 | - name: Create triage prompt 44 | run: | 45 | mkdir -p /tmp/claude-prompts 46 | cat > /tmp/claude-prompts/triage-prompt.txt << 'EOF' 47 | You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list. 48 | 49 | IMPORTANT: Don't post any comments or messages to the issue. Your only action should be to apply labels. 50 | 51 | Issue Information: 52 | - REPO: ${{ github.repository }} 53 | - ISSUE_NUMBER: ${{ github.event.issue.number }} 54 | 55 | TASK OVERVIEW: 56 | 57 | 1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else. 58 | 59 | 2. Next, use the GitHub tools to get context about the issue: 60 | - You have access to these tools: 61 | - mcp__github__get_issue: Use this to retrieve the current issue's details including title, description, and existing labels 62 | - mcp__github__get_issue_comments: Use this to read any discussion or additional context provided in the comments 63 | - mcp__github__update_issue: Use this to apply labels to the issue (do not use this for commenting) 64 | - mcp__github__search_issues: Use this to find similar issues that might provide context for proper categorization and to identify potential duplicate issues 65 | - mcp__github__list_issues: Use this to understand patterns in how other issues are labeled 66 | - Start by using mcp__github__get_issue to get the issue details 67 | 68 | 3. Analyze the issue content, considering: 69 | - The issue title and description 70 | - The type of issue (bug report, feature request, question, etc.) 71 | - Technical areas mentioned 72 | - Severity or priority indicators 73 | - User impact 74 | - Components affected 75 | 76 | 4. Select appropriate labels from the available labels list provided above: 77 | - Choose labels that accurately reflect the issue's nature 78 | - Be specific but comprehensive 79 | - Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority) 80 | - Consider platform labels (android, ios) if applicable 81 | - If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue. 82 | 83 | 5. Apply the selected labels: 84 | - Use mcp__github__update_issue to apply your selected labels 85 | - DO NOT post any comments explaining your decision 86 | - DO NOT communicate directly with users 87 | - If no labels are clearly applicable, do not apply any labels 88 | 89 | IMPORTANT GUIDELINES: 90 | - Be thorough in your analysis 91 | - Only select labels from the provided list above 92 | - DO NOT post any comments to the issue 93 | - Your ONLY action should be to apply labels using mcp__github__update_issue 94 | - It's okay to not add any labels if none are clearly applicable 95 | EOF 96 | 97 | - name: Run Claude Code for Issue Triage 98 | uses: anthropics/claude-code-base-action@beta 99 | with: 100 | prompt_file: /tmp/claude-prompts/triage-prompt.txt 101 | allowed_tools: "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues" 102 | mcp_config: /tmp/mcp-config/mcp-servers.json 103 | timeout_minutes: "5" 104 | anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} 105 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release Extension 2 | 3 | on: 4 | push: 5 | tags: [ 'v*.*.*' ] 6 | workflow_dispatch: 7 | inputs: 8 | version: 9 | description: 'Version to release (without v prefix, e.g. 1.0.0)' 10 | required: true 11 | type: string 12 | 13 | jobs: 14 | release: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: write 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: Setup Node.js 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: '20' 28 | 29 | - name: Install dependencies 30 | run: npm ci 31 | 32 | - name: Install webview dependencies 33 | run: npm ci --prefix web --legacy-peer-deps 34 | 35 | - name: Get version 36 | id: get_version 37 | run: | 38 | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then 39 | echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT 40 | else 41 | echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT 42 | fi 43 | 44 | - name: Configure Git 45 | if: github.event_name == 'workflow_dispatch' 46 | run: | 47 | git config --global user.name 'GitHub Actions' 48 | git config --global user.email 'actions@github.com' 49 | 50 | - name: Delete existing tag if manually triggered 51 | if: github.event_name == 'workflow_dispatch' 52 | run: | 53 | if git rev-parse "v${{ inputs.version }}" >/dev/null 2>&1; then 54 | git tag -d "v${{ inputs.version }}" 55 | git push --delete origin "v${{ inputs.version }}" || true 56 | fi 57 | 58 | - name: Create tag if manually triggered 59 | if: github.event_name == 'workflow_dispatch' 60 | run: | 61 | git tag -a "v${{ inputs.version }}" -m "Release version ${{ inputs.version }}" 62 | git push origin "v${{ inputs.version }}" 63 | 64 | - name: Build & Package VSIX 65 | run: npm run package-extension 66 | env: 67 | VSCE_PAT: ${{ secrets.VSCE_TOKEN }} 68 | 69 | - name: Create Release 70 | id: create_release 71 | uses: actions/create-release@v1 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | with: 75 | tag_name: v${{ steps.get_version.outputs.version }} 76 | release_name: Release v${{ steps.get_version.outputs.version }} 77 | draft: false 78 | prerelease: false 79 | 80 | - name: Upload Release Asset 81 | uses: actions/upload-release-asset@v1 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | with: 85 | upload_url: ${{ steps.create_release.outputs.upload_url }} 86 | asset_path: ./copilot-mcp-${{ steps.get_version.outputs.version }}.vsix 87 | asset_name: copilot-mcp-${{ steps.get_version.outputs.version }}.vsix 88 | asset_content_type: application/octet-stream 89 | 90 | - name: Publish 91 | run: npm run deploy 92 | env: 93 | VSCE_PAT: ${{ secrets.VSCE_TOKEN }} 94 | 95 | -------------------------------------------------------------------------------- /.github/workflows/update-version.yml: -------------------------------------------------------------------------------- 1 | name: Update Version and Create Tag 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'package.json' 7 | - 'src/*' 8 | - '.github/*' 9 | branches: 10 | - main 11 | workflow_dispatch: 12 | inputs: 13 | version: 14 | description: 'Version to update to (e.g. 1.0.0)' 15 | required: true 16 | type: string 17 | 18 | jobs: 19 | update-version: 20 | if: ${{ !startsWith(github.event.head_commit.message, 'chore:') }} 21 | runs-on: ubuntu-latest 22 | permissions: 23 | contents: write 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | 30 | - name: Setup Node.js 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: '20' 34 | 35 | - name: Update version in package.json if manually triggered 36 | if: github.event_name == 'workflow_dispatch' 37 | run: npm version ${{ inputs.version }} --no-git-tag-version --allow-same-version 38 | 39 | - name: Get package version 40 | id: get_version 41 | run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT 42 | 43 | - name: Update README badge 44 | run: | 45 | sed -i "s/version-[0-9]\+\.[0-9]\+\.[0-9]\+-blue/version-${{ steps.get_version.outputs.version }}-blue/" README.md 46 | 47 | - name: Configure Git 48 | run: | 49 | git config --global user.name 'GitHub Actions' 50 | git config --global user.email 'actions@github.com' 51 | 52 | - name: Commit changes 53 | run: | 54 | git add README.md package.json package-lock.json 55 | git diff --staged --quiet || git commit -m "chore: update version to ${{ steps.get_version.outputs.version }}" 56 | git push 57 | 58 | - name: Delete existing tag if present 59 | run: | 60 | if git rev-parse "v${{ steps.get_version.outputs.version }}" >/dev/null 2>&1; then 61 | git tag -d "v${{ steps.get_version.outputs.version }}" 62 | git push --delete origin "v${{ steps.get_version.outputs.version }}" || true 63 | fi 64 | 65 | - name: Create and push tag 66 | run: | 67 | git tag -a "v${{ steps.get_version.outputs.version }}" -m "Release version ${{ steps.get_version.outputs.version }}" 68 | git push origin "v${{ steps.get_version.outputs.version }}" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | .env -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": true, 4 | "arrowParens": "always", 5 | "bracketSpacing": true, 6 | "endOfLine": "lf", 7 | "htmlWhitespaceSensitivity": "css", 8 | "insertPragma": false, 9 | "singleAttributePerLine": false, 10 | "bracketSameLine": false, 11 | "jsxBracketSameLine": false, 12 | "jsxSingleQuote": false, 13 | "printWidth": 80, 14 | "proseWrap": "preserve", 15 | "quoteProps": "as-needed", 16 | "requirePragma": false, 17 | "semi": true, 18 | "singleQuote": false 19 | } 20 | -------------------------------------------------------------------------------- /.vscode-test.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@vscode/test-cli'; 2 | 3 | export default defineConfig({ 4 | files: 'out/test/**/*.test.js', 5 | }); 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["dbaeumer.vscode-eslint", "connor4312.esbuild-problem-matchers", "ms-vscode.extension-test-runner"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/dist/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off" 13 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "watch", 8 | "dependsOn": [ 9 | "npm: watch:tsc", 10 | "npm: watch:esbuild" 11 | ], 12 | "presentation": { 13 | "reveal": "never" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "watch:esbuild", 23 | "group": "build", 24 | "problemMatcher": "$esbuild-watch", 25 | "isBackground": true, 26 | "label": "npm: watch:esbuild", 27 | "presentation": { 28 | "group": "watch", 29 | "reveal": "never" 30 | } 31 | }, 32 | { 33 | "type": "npm", 34 | "script": "watch:tsc", 35 | "group": "build", 36 | "problemMatcher": "$tsc-watch", 37 | "isBackground": true, 38 | "label": "npm: watch:tsc", 39 | "presentation": { 40 | "group": "watch", 41 | "reveal": "never" 42 | } 43 | }, 44 | { 45 | "type": "npm", 46 | "script": "watch-tests", 47 | "problemMatcher": "$tsc-watch", 48 | "isBackground": true, 49 | "presentation": { 50 | "reveal": "never", 51 | "group": "watchers" 52 | }, 53 | "group": "build" 54 | }, 55 | { 56 | "label": "tasks: watch-tests", 57 | "dependsOn": [ 58 | "npm: watch", 59 | "npm: watch-tests" 60 | ], 61 | "problemMatcher": [] 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | out/** 2 | dist/**/bundle.js.map 3 | !dist/extension.js 4 | !web/dist/** 5 | 6 | # Exclude all node_modules directories 7 | **/node_modules/** 8 | 9 | # Exclude source files that are compiled/bundled 10 | src/** 11 | web/src/** 12 | web/public/** 13 | 14 | # Exclude build and config files 15 | esbuild.js 16 | web/vite.config.ts 17 | web/tsconfig.json 18 | web/tsconfig.app.json 19 | web/tsconfig.node.json 20 | web/package.json 21 | web/package-lock.json 22 | web/eslint.config.js 23 | web/components.json 24 | web/.gitignore 25 | web/README.md 26 | tsconfig.json 27 | eslint.config.mjs 28 | 29 | # Exclude test files and VS Code specific dev files 30 | .vscode/** 31 | .vscode-test/** 32 | **/*.test.ts 33 | **/*.spec.ts 34 | 35 | # Other common exclusions 36 | .git/** 37 | .github/** 38 | .gitignore 39 | .yarnrc 40 | vsc-extension-quickstart.md 41 | !CHANGELOG.md 42 | !LICENSE 43 | !README.md 44 | **/*.map 45 | 46 | # Allow specific necessary files if they were excluded by broader rules above 47 | # For example, if you had a top-level asset you needed: 48 | !icon.png 49 | !logo.png -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "copilot-mcp" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [Unreleased] 8 | 9 | - Initial release -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | Copilot MCP is a VSCode extension that enables searching, managing, and installing Model Context Protocol (MCP) servers. It extends GitHub Copilot Chat's capabilities by providing a chat participant interface and a sidebar UI for MCP server discovery. 8 | 9 | ## Development Commands 10 | 11 | ### Root Directory Commands 12 | ```bash 13 | # Install dependencies for both extension and web UI 14 | npm run install:all 15 | 16 | # Build both extension and web UI 17 | npm run build:all 18 | 19 | # Development mode - watch for changes 20 | npm run watch 21 | 22 | # Run tests 23 | npm run test 24 | 25 | # Lint the codebase 26 | npm run lint 27 | 28 | # Package extension for production 29 | npm run package 30 | 31 | # Create VSIX package for distribution 32 | npm run package-extension 33 | ``` 34 | 35 | ### Web UI Commands (from /web directory) 36 | ```bash 37 | # Start development server 38 | npm run start 39 | 40 | # Build for production 41 | npm run build 42 | 43 | # Run linting 44 | npm run lint 45 | ``` 46 | 47 | ## Architecture Overview 48 | 49 | ### Extension Structure 50 | - **Entry Point**: `src/extension.ts` - Activates extension, registers chat participant and webview 51 | - **Chat Participant**: `src/McpAgent.ts` - Handles `@mcp` chat commands (`/search`, `/install`) 52 | - **Webview Panel**: `src/panels/ExtensionPanel.ts` - Manages sidebar UI communication 53 | - **GitHub Integration**: `src/utilities/repoSearch.ts` - Searches GitHub for MCP servers 54 | - **Copilot Integration**: `src/utilities/CopilotChat.ts` - Interfaces with GitHub Copilot 55 | 56 | ### Web UI Structure 57 | - Separate React application in `/web/` directory 58 | - Built with Vite, React 19, TypeScript, and Tailwind CSS 59 | - Uses shadcn/ui component library 60 | - Communicates with extension via VSCode postMessage API 61 | 62 | ### Key Technologies 63 | - **Build**: esbuild for extension bundling, Vite for web UI 64 | - **AI/LLM**: @ax-llm/ax agent framework, AI SDK for model interactions 65 | - **Testing**: VSCode test framework with Mocha 66 | - **Telemetry**: Application Insights and OpenTelemetry integration 67 | 68 | ## Development Guidelines 69 | 70 | ### Testing Changes 71 | 1. Run `npm run watch` in root directory for continuous builds 72 | 2. Press F5 in VSCode to launch Extension Development Host 73 | 3. Test chat participant with `@mcp /search ` or `@mcp /install ` 74 | 4. Test sidebar UI functionality 75 | 76 | ### Code Conventions 77 | - TypeScript strict mode enabled 78 | - ESLint configuration in `eslint.config.mjs` 79 | - React components use function components with TypeScript 80 | - Telemetry events follow consistent naming: `mcp__` 81 | 82 | ### Important Files 83 | - `package.json` - Extension manifest and scripts 84 | - `src/types/registry.ts` - MCP server registry types 85 | - `src/shared/types/rpcTypes.ts` - VSCode messaging types 86 | - `web/src/components/` - React UI components -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Copilot MCP Search for VSCode

3 |
4 |
5 | 6 | 7 | [![](https://dcbadge.limes.pink/api/server/https://discord.gg/copilotmcp)](https://discord.gg/copilotmcp) 8 | 9 |
10 |
11 | image 12 |
13 |
14 | 15 | ![Version](https://img.shields.io/badge/version-0.0.50-blue.svg?cacheSeconds=2592000) 16 | [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) 17 | [![VSCode Extension](https://img.shields.io/badge/VSCode-Extension-blue.svg?logo=visual-studio-code)](https://code.visualstudio.com/api/references/extension-guidelines) 18 | [![MCP Client](https://img.shields.io/badge/MCP-Client-green.svg)](https://modelcontextprotocol.io/clients) 19 | 20 | 21 | 22 |
23 | 24 | > A powerful VSCode extension that allows you to discover and install open-source MCP servers, enabling seamless integration between MCP tool servers and GitHub Copilot Chat. Join the growing ecosystem of interoperable AI applications with flexible integration options. 25 | 26 | ## ✨ Features 27 | 28 | - 🔧 **MCP Server Management**: Connect and manage multiple MCP servers through an intuitive UI 29 | - 🚀 **Copilot Integration**: Expose MCP tools directly to GitHub Copilot Chat 30 | - 🎯 **Server Discovery**: Automatically discover and search for open-source MCP servers 31 | - ⚡ **Server Health Monitoring**: Real-time monitoring of MCP server status and connections 32 | - 🔄 **Automatic Connection Management**: Seamless handling of MCP server connections and reconnections 33 | 34 | 35 | ## 📦 Installation 36 | 37 | 1. Install the [extension](https://marketplace.visualstudio.com/items?itemName=AutomataLabs.copilot-mcp) from the VSCode Marketplace 38 | 2. Configure your MCP servers through the extension settings 39 | 3. Start using GitHub Copilot Chat with your MCP tools! 40 | 41 | ## 🛠️ Configuration 42 | 43 | You can configure your MCP servers in the UI or in VSCode settings. 44 | 45 | In the UI, look for the "MCP Servers" button in the activity bar. 46 | 47 | ## 🚀 Usage 48 | 49 | 1. Open the MCP Servers view from the VSCode activity bar 50 | 2. Manage your configured MCP Servers from the panel 51 | 3. Search and discover new open-source MCP Servers 52 | 53 | ## 🔗 Requirements 54 | 55 | - VSCode 56 | - GitHub Copilot Chat extension 57 | 58 | ## 🌟 Benefits 59 | 60 | - Enable Copilot to use custom context and tools through MCP 61 | - Join the growing ecosystem of interoperable AI applications 62 | - Support local-first AI workflows 63 | - Flexible integration options for your development workflow 64 | 65 | ## 👥 Contributing 66 | 67 | Contributions, issues and feature requests are welcome! 68 | Feel free to check the [issues page](https://github.com/VikashLoomba/copilot-mcp/issues). 69 | 70 | ## ✍️ Author 71 | 72 | **Vikash Loomba** 73 | 74 | * Website: https://automatalabs.io 75 | * Github: [@vikashloomba](https://github.com/vikashloomba) 76 | 77 | ## 📝 License 78 | 79 | Copyright © 2024 [Vikash Loomba](https://automatalabs.io). 80 | 81 | This project is licensed under the [GNU General Public License v3.0](LICENSE). 82 | 83 | --- 84 | 85 | _Part of the [MCP Client Ecosystem](https://modelcontextprotocol.io/clients) - Enabling interoperable AI tools for developers_ ⭐️ 86 | -------------------------------------------------------------------------------- /WHATS_NEW.md: -------------------------------------------------------------------------------- 1 | # What's New in Copilot MCP! 2 | 3 | ## Version 0.0.50 – Smarter Discovery & Better Management 4 | *(June 2025)* 5 | 6 | Our latest release focuses on making MCP servers even easier to find, install and manage. 7 | 8 | **Highlights** 9 | 10 | * **Improved Search Pagination** – Repository search now uses a *next-page cursor* behind the scenes, giving you faster, more accurate results when browsing for MCP servers. 11 | * **Richer Search Intelligence** – A new AI-powered query generator & result filter help surface only *installable* MCP servers (no more clone-and-build surprises!). 12 | * **Enhanced Management Experience** – The extension can now distinguish between *workspace*-specific and *user*-level MCP server configurations, so you have fine-grained control over where servers are installed. 13 | * **Debug-Friendly Mode** – Enable in-depth logs for troubleshooting with a single setting. 14 | * **Update Notifications** – A new in-product notice shows this What's New page automatically after you install or upgrade, so you never miss out on fresh features. 15 | * Plus numerous performance improvements and bug fixes. 16 | 17 | Stay Tuned for more exciting updates! 🚀 (On-demand Cloud MCP's, anyone 👀?) 18 | 19 | --- 20 | 21 | ## Version 0.0.40 - A Whole New Focus! 22 | 23 | We're thrilled to announce a major evolution for **Copilot MCP**! 24 | 25 | Previously, this extension assisted with the manual setup of server tools for GitHub Copilot. Many of these tools extend Copilot's abilities by leveraging the **Model Context Protocol (MCP)**. With recent advancements in Visual Studio Code, which now natively supports the integration of tools via MCP, we saw an opportunity to provide even more value to your Copilot experience. 26 | 27 | **Introducing Your New MCP Server Discovery Hub!** 28 | 29 | **Copilot MCP** has been completely reimagined and refactored. It's now your go-to tool for **finding and installing Model Context Protocol (MCP) servers**. These MCP servers empower GitHub Copilot by connecting it to a vast array of external tools and data sources, significantly expanding its capabilities. 30 | 31 | Here's what's new: 32 | 33 | * **Discover MCP Servers on GitHub**: Seamlessly search and browse a growing catalog of open-source MCP servers. Find tools built by the community that enable Copilot to perform new actions and access diverse information. 34 | * **One-Click Installation**: Gone are the days of manual setup! Leveraging VS Code's native ability to integrate with MCP-compliant server tools, you can now install and configure them with a single click. 35 | * **Streamlined Workflow**: Get from discovery to using new Copilot tools faster than ever before. We handle the complexities so you can focus on enhancing your AI-assisted development. 36 | 37 | We believe this new direction will make it significantly easier for you to unlock the full potential of GitHub Copilot by connecting it to a diverse range of powerful tools and data sources. 38 | 39 | We're excited for you to try out the new **Copilot MCP** and welcome your feedback! 40 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VikashLoomba/copilot-mcp/08e50e061389e4c6c90d357508439082e8be5ee9/demo.gif -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | const esbuild = require("esbuild"); 2 | 3 | const production = process.argv.includes('--production'); 4 | const watch = process.argv.includes('--watch'); 5 | 6 | /** 7 | * @type {import('esbuild').Plugin} 8 | */ 9 | const esbuildProblemMatcherPlugin = { 10 | name: 'esbuild-problem-matcher', 11 | 12 | setup(build) { 13 | build.onStart(() => { 14 | console.log('[watch] build started'); 15 | }); 16 | build.onEnd((result) => { 17 | result.errors.forEach(({ text, location }) => { 18 | console.error(`✘ [ERROR] ${text}`); 19 | console.error(` ${location.file}:${location.line}:${location.column}:`); 20 | }); 21 | console.log('[watch] build finished'); 22 | }); 23 | }, 24 | }; 25 | 26 | async function main() { 27 | const ctx = await esbuild.context({ 28 | entryPoints: [ 29 | 'src/extension.ts' 30 | ], 31 | bundle: true, 32 | format: 'cjs', 33 | minify: production, 34 | sourcemap: !production, 35 | sourcesContent: false, 36 | platform: 'node', 37 | outfile: 'dist/extension.js', 38 | external: ['vscode'], 39 | logLevel: 'silent', 40 | plugins: [ 41 | /* add to the end of plugins array */ 42 | esbuildProblemMatcherPlugin, 43 | ], 44 | }); 45 | if (watch) { 46 | await ctx.watch(); 47 | } else { 48 | await ctx.rebuild(); 49 | await ctx.dispose(); 50 | } 51 | } 52 | 53 | main().catch(e => { 54 | console.error(e); 55 | process.exit(1); 56 | }); 57 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import tsParser from "@typescript-eslint/parser"; 3 | 4 | export default [{ 5 | files: ["**/*.ts"], 6 | }, { 7 | plugins: { 8 | "@typescript-eslint": typescriptEslint, 9 | }, 10 | 11 | languageOptions: { 12 | parser: tsParser, 13 | ecmaVersion: 2022, 14 | sourceType: "module", 15 | }, 16 | 17 | rules: { 18 | "@typescript-eslint/naming-convention": ["warn", { 19 | selector: "import", 20 | format: ["camelCase", "PascalCase"], 21 | }], 22 | 23 | curly: "warn", 24 | eqeqeq: "warn", 25 | "no-throw-literal": "warn", 26 | semi: "warn", 27 | }, 28 | }]; -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VikashLoomba/copilot-mcp/08e50e061389e4c6c90d357508439082e8be5ee9/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "copilot-mcp", 3 | "author": { 4 | "email": "vikash@automatalabs.io", 5 | "name": "Vikash Loomba", 6 | "url": "https://automatalabs.io" 7 | }, 8 | "publisher": "AutomataLabs", 9 | "repository": { 10 | "url": "https://github.com/vikashloomba/copilot-mcp", 11 | "type": "git" 12 | }, 13 | "displayName": "Copilot MCP", 14 | "description": "VSCode extension that allows you to search, manage, and install open-source MCP servers", 15 | "version": "0.0.50", 16 | "icon": "logo.png", 17 | "engines": { 18 | "vscode": "^1.99.0" 19 | }, 20 | "sponsor": { 21 | "url": "https://github.com/sponsors/VikashLoomba" 22 | }, 23 | "categories": [ 24 | "AI", 25 | "Chat", 26 | "Machine Learning" 27 | ], 28 | "keywords": [ 29 | "chat-participant", 30 | "copilot", 31 | "copilot-mcp", 32 | "chat-participant-utils", 33 | "dev", 34 | "mcp", 35 | "openrouter", 36 | "coding", 37 | "agent", 38 | "autonomous", 39 | "chatgpt", 40 | "sonnet", 41 | "ai", 42 | "llama", 43 | "model context protocol" 44 | ], 45 | "activationEvents": [ 46 | "onStartupFinished" 47 | ], 48 | "main": "./dist/extension.js", 49 | "extensionDependencies": [ 50 | "github.copilot-chat" 51 | ], 52 | "contributes": { 53 | "chatParticipants": [ 54 | { 55 | "id": "copilot.mcp-agent", 56 | "name": "mcp", 57 | "fullName": "MCP Finder", 58 | "description": "Find and install MCP servers for VSCode", 59 | "isSticky": true, 60 | "commands": [ 61 | { 62 | "name": "search", 63 | "description": "Search for MCP servers to install", 64 | "disambiguation": [ 65 | { 66 | "category": "mcp_search", 67 | "description": "The user wants to search for Model Context Protocol (MCP) servers", 68 | "examples": [ 69 | "What mcp servers are there for figma?", 70 | "Can you find a browser mcp server to use?", 71 | "Can find an mcp server for X?" 72 | ] 73 | } 74 | ] 75 | }, 76 | { 77 | "name": "install", 78 | "description": "Install an MCP server", 79 | "disambiguation": [ 80 | { 81 | "category": "mcp_install", 82 | "description": "The user wants to install a Model Context Protocol (MCP) server to VSCode.", 83 | "examples": [ 84 | "Can you install the firecrawl mcp server for me?", 85 | "Add the github mcp server to vscode" 86 | ] 87 | } 88 | ] 89 | } 90 | ], 91 | "disambiguation": [ 92 | { 93 | "category": "mcp_search", 94 | "description": "The user is asking about finding Model Context Protocol (MCP) servers", 95 | "examples": [ 96 | "Can you help me find mcp servers for interacting with my local database?", 97 | "Can you help me install the mcp memory server?", 98 | "How do I install the firecrawl MCP server?", 99 | "Can you help me add the figma MCP server?", 100 | "What mcp servers are there for task management?" 101 | ] 102 | } 103 | ] 104 | } 105 | ], 106 | "commands": [ 107 | { 108 | "command": "copilot-mcp.helloWorld", 109 | "title": "Hello World" 110 | } 111 | ], 112 | "viewsContainers": { 113 | "activitybar": [ 114 | { 115 | "id": "copilotMcpSidebar", 116 | "title": "Copilot MCP", 117 | "icon": "resources/light/server.svg" 118 | } 119 | ] 120 | }, 121 | "views": { 122 | "copilotMcpSidebar": [ 123 | { 124 | "type": "webview", 125 | "id": "copilotMcpView", 126 | "name": "Copilot MCP Panel", 127 | "icon": "resources/light/server.svg" 128 | } 129 | ] 130 | } 131 | }, 132 | "scripts": { 133 | "install:all": "npm install && cd web && npm install --legacy-peer-deps", 134 | "start:webview": "cd web && npm run start", 135 | "build:webview": "npm --prefix web run build", 136 | "vscode:prepublish": "npm run package", 137 | "compile": "npm run check-types && npm run lint && node esbuild.js", 138 | "watch": "npm-run-all -p watch:*", 139 | "watch:esbuild": "node esbuild.js --watch", 140 | "watch:tsc": "tsc --noEmit --watch --project tsconfig.json", 141 | "package": "npm run build:all", 142 | "build:all": "npm run compile && npm run build:webview", 143 | "package-extension": "npm run package && npx vsce package -o copilot-mcp-${npm_package_version}.vsix", 144 | "compile-tests": "tsc -p . --outDir out", 145 | "watch-tests": "tsc -p . -w --outDir out", 146 | "pretest": "npm run compile-tests && npm run compile && npm run lint", 147 | "check-types": "tsc --noEmit", 148 | "lint": "eslint src", 149 | "test": "vscode-test", 150 | "deploy": "npx vsce publish -p $VSCE_PAT" 151 | }, 152 | "devDependencies": { 153 | "@types/mocha": "^10.0.10", 154 | "@types/node": "20.x", 155 | "@types/vscode": "^1.99.0", 156 | "@typescript-eslint/eslint-plugin": "^8.31.1", 157 | "@typescript-eslint/parser": "^8.31.1", 158 | "@vscode/test-cli": "^0.0.10", 159 | "@vscode/test-electron": "^2.5.2", 160 | "esbuild": "^0.25.3", 161 | "eslint": "^9.25.1", 162 | "npm-run-all": "^4.1.5", 163 | "typescript": "^5.8.3" 164 | }, 165 | "dependencies": { 166 | "@ai-sdk/openai-compatible": "^0.2.14", 167 | "@ax-llm/ax": "^11.0.41", 168 | "@ax-llm/ax-ai-sdk-provider": "^11.0.41", 169 | "@octokit/rest": "^21.1.1", 170 | "@opentelemetry/api-logs": "^0.200.0", 171 | "@opentelemetry/exporter-logs-otlp-http": "^0.201.0", 172 | "@opentelemetry/exporter-trace-otlp-http": "^0.200.0", 173 | "@vscode/chat-extension-utils": "^0.0.0-alpha.5", 174 | "@vscode/extension-telemetry": "^1.0.0", 175 | "ai": "^4.3.15", 176 | "octokit": "^4.1.3", 177 | "vscode-messenger": "^0.5.1", 178 | "zod": "^3.24.4" 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /resources/dark/server.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /resources/light/server.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import { logger, getLogger } from "./telemetry"; 2 | import { setCommonLogAttributes } from "./utilities/signoz"; 3 | import { shutdownLogs } from "./utilities/logging"; 4 | import * as vscode from "vscode"; 5 | import { TelemetryReporter } from "@vscode/extension-telemetry"; 6 | import { CopilotMcpViewProvider } from "./panels/ExtensionPanel"; 7 | import { GITHUB_AUTH_PROVIDER_ID, SCOPES } from "./utilities/const"; 8 | import { CopilotChatProvider } from "./utilities/CopilotChat"; 9 | import { handler } from "./McpAgent"; 10 | const connectionString = 11 | "InstrumentationKey=2c71cf43-4cb2-4e25-97c9-bd72614a9fe8;IngestionEndpoint=https://westus2-2.in.applicationinsights.azure.com/;LiveEndpoint=https://westus2.livediagnostics.monitor.azure.com/;ApplicationId=862c3c9c-392a-4a12-8475-5c9ebeff7aaf"; 12 | const telemetryReporter = new TelemetryReporter(connectionString); 13 | 14 | // This method is called when your extension is activated 15 | // Your extension is activated the very first time the command is executed 16 | export async function activate(context: vscode.ExtensionContext) { 17 | const Octokit = await import("@octokit/rest"); 18 | context.subscriptions.push(logger, { dispose: shutdownLogs }); 19 | context.subscriptions.push(telemetryReporter); 20 | // console.dir(await vscode.authentication.getAccounts('github'), {depth: null}); 21 | const session = await vscode.authentication.getSession( 22 | GITHUB_AUTH_PROVIDER_ID, 23 | SCOPES, 24 | { createIfNone: true } 25 | ); 26 | const octokit = new Octokit.Octokit({ 27 | auth: session.accessToken, 28 | }); 29 | console.log("Initializing Copilot Provider"); 30 | const copilot = await CopilotChatProvider.initialize(context); 31 | const models = await copilot.getModels(); 32 | setCommonLogAttributes({ ...session.account }); 33 | if (vscode.env.isNewAppInstall) { 34 | getLogger().logUsage("newUserInstall"); 35 | } 36 | getLogger().logUsage("activate"); 37 | // Use the console to output diagnostic information (console.log) and errors (console.error) 38 | // This line of code will only be executed once when your extension is activated 39 | console.log('Congratulations, your extension "copilot-mcp" is now active!'); 40 | // The command has been defined in the package.json file 41 | // Now provide the implementation of the command with registerCommand 42 | // The commandId parameter must match the command field in package.json 43 | const disposable = vscode.commands.registerCommand( 44 | "copilot-mcp.helloWorld", 45 | () => { 46 | // The code you place here will be executed every time your command is executed 47 | // Display a message box to the user 48 | vscode.window.showInformationMessage( 49 | "Hello World from copilot-mcp!" 50 | ); 51 | } 52 | ); 53 | 54 | context.subscriptions.push(disposable); 55 | 56 | const provider = new CopilotMcpViewProvider( 57 | context.extensionUri, 58 | session.accessToken, 59 | telemetryReporter, 60 | session 61 | ); 62 | context.subscriptions.push( 63 | vscode.window.registerWebviewViewProvider( 64 | CopilotMcpViewProvider.viewType, 65 | provider 66 | ) 67 | ); 68 | 69 | // Register the chat participant and its request handler 70 | const mcpChatAgent = vscode.chat.createChatParticipant( 71 | "copilot.mcp-agent", 72 | handler 73 | ); 74 | 75 | // Optionally, set some properties for @cat 76 | mcpChatAgent.iconPath = vscode.Uri.joinPath( 77 | context.extensionUri, 78 | "logo.png" 79 | ); 80 | 81 | // Show "What's New" page if the extension has been updated 82 | await showUpdatesToUser(context); 83 | } 84 | 85 | // Helper that shows the WHATS_NEW.md preview when appropriate 86 | async function showUpdatesToUser(context: vscode.ExtensionContext) { 87 | try { 88 | // Locate this extension in VS Code's registry so we can read its package.json metadata 89 | const thisExtension = vscode.extensions.all.find( 90 | ext => ext.extensionUri.toString() === context.extensionUri.toString() 91 | ); 92 | if (!thisExtension) { 93 | return; // Should never happen, but guard just in case 94 | } 95 | 96 | const currentVersion: string = thisExtension.packageJSON.version; 97 | const storageKey = "copilotMcp.whatsNewVersionShown"; 98 | const lastVersion: string | undefined = context.globalState.get(storageKey); 99 | 100 | // Show the What's New page only for new installs or when the user upgrades to a version they haven't seen yet 101 | if (lastVersion === currentVersion) { 102 | return; // User has already been shown this version's notes 103 | } 104 | 105 | // Open the WHATS_NEW.md file bundled with the extension in the built-in Markdown preview 106 | const whatsNewUri = vscode.Uri.joinPath(context.extensionUri, "WHATS_NEW.md"); 107 | await vscode.commands.executeCommand("markdown.showPreview", whatsNewUri); 108 | 109 | // Persist that we've shown the notes for this version so we don't show again 110 | await context.globalState.update(storageKey, currentVersion); 111 | } catch (error) { 112 | console.error("Failed to display What's New information:", error); 113 | } 114 | } 115 | 116 | // This method is called when your extension is deactivated 117 | export function deactivate() { } 118 | -------------------------------------------------------------------------------- /src/shared/types/rpcTypes.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type RequestType, 3 | type NotificationType, 4 | } from "vscode-messenger-common"; 5 | 6 | export const searchServersType: RequestType< 7 | { query: string; page?: number; perPage?: number }, 8 | { results: any[]; totalCount: number; currentPage: number; perPage: number } 9 | > = { method: "search" }; 10 | 11 | export const getReadmeType: RequestType< 12 | { fullName: string; name: string; owner: any }, 13 | { readme: string; fullName: string } 14 | > = { method: "requestReadme" }; 15 | 16 | export const getMcpConfigType: RequestType = { 17 | method: "getMcpConfigObject", 18 | }; 19 | 20 | export const updateMcpConfigType: NotificationType<{ servers: any[] }> = { 21 | method: "updateMcpConfigObject", 22 | }; 23 | 24 | export const updateServerEnvVarType: NotificationType< 25 | { serverName: string; envKey: string; newValue: string } 26 | > = { method: "updateServerEnvVar" }; 27 | 28 | export const deleteServerType: NotificationType<{ serverName: string }> = { method: "deleteServer" }; 29 | 30 | export const aiAssistedSetupType: RequestType<{repo: any}, boolean> = { method: "aiAssistedSetup" }; 31 | 32 | export const sendFeedbackType: NotificationType<{ feedback: string }> = { method: "sendFeedback" }; -------------------------------------------------------------------------------- /src/telemetry/index.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { signozTelemetry } from "../utilities/signoz"; 3 | // Filled asynchronously at activation time 4 | export const logger = vscode.env.createTelemetryLogger(signozTelemetry, { 5 | ignoreBuiltInCommonProperties: false, 6 | ignoreUnhandledErrors: true, 7 | }); 8 | 9 | export function getLogger() { 10 | return logger; 11 | } 12 | -------------------------------------------------------------------------------- /src/test/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/types/octokit.d.ts: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/types/registry.ts: -------------------------------------------------------------------------------- 1 | // TypeScript interfaces for Extension Publisher Backend API (/v1/publish-server) 2 | 3 | export interface ServerDetailPayload { 4 | id?: string; 5 | name: string; // e.g., io.github.owner/repo - This is required 6 | description?: string; 7 | version_detail?: VersionDetail; 8 | registries?: Registry[]; // Changed from 'Registries' to 'Registry' for singular array item 9 | remotes?: Remote[]; // Changed from 'Remotes' to 'Remote' for singular array item 10 | } 11 | 12 | export interface VersionDetail { 13 | version?: string; 14 | release_date?: string; // RFC 3339 date format 15 | is_latest?: boolean; 16 | } 17 | 18 | export interface Registry { // Singular form for array elements 19 | name?: string; 20 | package_name?: string; 21 | license?: string; 22 | command_arguments?: CommandArguments; 23 | } 24 | 25 | export interface Remote { // Singular form for array elements 26 | transport_type?: string; 27 | url?: string; 28 | } 29 | 30 | export interface CommandArguments { 31 | sub_commands?: SubCommand[]; 32 | positional_arguments?: PositionalArgument[]; 33 | environment_variables?: EnvironmentVariable[]; 34 | named_arguments?: NamedArgument[]; 35 | } 36 | 37 | export interface EnvironmentVariable { 38 | name?: string; 39 | description?: string; 40 | required?: boolean; 41 | } 42 | 43 | export interface Argument { 44 | name?: string; 45 | description?: string; 46 | default_value?: string; 47 | is_required?: boolean; 48 | is_editable?: boolean; 49 | is_repeatable?: boolean; 50 | choices?: string[]; 51 | } 52 | 53 | export interface PositionalArgument { 54 | position?: number; 55 | argument?: Argument; 56 | } 57 | 58 | export interface SubCommand { 59 | name?: string; 60 | description?: string; 61 | named_arguments?: NamedArgument[]; 62 | } 63 | 64 | export interface NamedArgument { 65 | short_flag?: string; 66 | long_flag?: string; 67 | requires_value?: boolean; 68 | argument?: Argument; 69 | } 70 | 71 | -------------------------------------------------------------------------------- /src/utilities/getNonce.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A helper function that returns a unique alphanumeric identifier called a nonce. 3 | * 4 | * @remarks This function is primarily used to help enforce content security 5 | * policies for resources/scripts being executed in a webview context. 6 | * 7 | * @returns A nonce 8 | */ 9 | export function getNonce() { 10 | let text = ""; 11 | const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 12 | for (let i = 0; i < 32; i++) { 13 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 14 | } 15 | return text; 16 | } -------------------------------------------------------------------------------- /src/utilities/getUri.ts: -------------------------------------------------------------------------------- 1 | import { Uri, Webview } from "vscode"; 2 | 3 | /** 4 | * A helper function which will get the webview URI of a given file or resource. 5 | * 6 | * @remarks This URI can be used within a webview's HTML as a link to the 7 | * given file/resource. 8 | * 9 | * @param webview A reference to the extension webview 10 | * @param extensionUri The URI of the directory containing the extension 11 | * @param pathList An array of strings representing the path to a file/resource 12 | * @returns A URI pointing to the file/resource 13 | */ 14 | export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { 15 | return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); 16 | } -------------------------------------------------------------------------------- /src/utilities/logging.ts: -------------------------------------------------------------------------------- 1 | // src/utilities/logging.ts 2 | import { logs, SeverityNumber } from "@opentelemetry/api-logs"; 3 | import { 4 | LoggerProvider, 5 | BatchLogRecordProcessor, 6 | } from "@opentelemetry/sdk-logs"; 7 | import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http"; 8 | import { Resource, resourceFromAttributes } from "@opentelemetry/resources"; 9 | import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"; 10 | 11 | const exporter = new OTLPLogExporter({ 12 | // Explicit URL → the exporter will **not** append /v1/logs when this is set 13 | url: "https://events.automatalabs.io/v1/logs", 14 | headers: { "signoz-access-token": process.env.SIGNOZ_TOKEN ?? "" }, 15 | }); 16 | 17 | const provider = new LoggerProvider({ 18 | resource: resourceFromAttributes({ 19 | [SemanticResourceAttributes.SERVICE_NAME]: "copilot-mcp-extension", 20 | }), 21 | }); 22 | 23 | provider.addLogRecordProcessor(new BatchLogRecordProcessor(exporter)); 24 | 25 | // Register globally 26 | logs.setGlobalLoggerProvider(provider); 27 | 28 | // Graceful shutdown on reload / quit 29 | export async function shutdownLogs() { 30 | await provider.shutdown(); 31 | } 32 | -------------------------------------------------------------------------------- /src/utilities/repoSearch.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { GITHUB_AUTH_PROVIDER_ID, SCOPES } from "./const"; 4 | // Define interfaces for better type safety and clarity 5 | export interface SearchMcpServersParams { 6 | query?: string; // Optional: user's specific search keywords 7 | page: number; 8 | perPage: number; 9 | } 10 | 11 | export interface McpServerAuthor { 12 | name: string; 13 | profileUrl: string; 14 | avatarUrl: string; 15 | } 16 | 17 | export interface McpServerResult { 18 | id: number; 19 | url: string; 20 | name: string; 21 | fullName: string; 22 | stars: number; 23 | author: McpServerAuthor; 24 | description: string | null; 25 | readme?: string; // Made optional, as it won't be populated here 26 | language: string | null; 27 | updatedAt: string; 28 | } 29 | 30 | export interface SearchMcpServersResponse { 31 | results: McpServerResult[]; 32 | totalCount: number; 33 | hasMore: boolean; 34 | } 35 | 36 | export async function searchMcpServers( 37 | params: SearchMcpServersParams 38 | ): Promise { 39 | try { 40 | const session = await vscode.authentication.getSession( 41 | GITHUB_AUTH_PROVIDER_ID, 42 | SCOPES 43 | ); 44 | const Octokit = await import("octokit"); 45 | const octokit = new Octokit.Octokit({ 46 | auth: session?.accessToken, 47 | }); 48 | const baseQuery = `"mcp" in:name,description,topics "${params.query}" in:name,description`; 49 | const fullQuery = params.query?.includes("mcp") ? params.query : baseQuery; 50 | 51 | const response = await octokit.rest.search.repos({ 52 | q: fullQuery, 53 | page: params.page, 54 | per_page: params.perPage, 55 | // sort: 'stars', // Optional: sort by stars or relevance 56 | order: "desc", 57 | }); 58 | 59 | const processedResults: McpServerResult[] = []; 60 | 61 | for (const repo of response.data.items) { 62 | if (!repo.owner) { 63 | continue; 64 | } 65 | 66 | // README fetching logic is removed from here. 67 | // The RepoCard will request it separately. 68 | processedResults.push({ 69 | fullName: repo.full_name, 70 | stars: repo.stargazers_count || 0, 71 | author: { 72 | name: repo.owner.login, 73 | profileUrl: repo.owner.html_url, 74 | avatarUrl: repo.owner.avatar_url, 75 | }, 76 | updatedAt: repo.updated_at, 77 | ...repo, 78 | url: repo.html_url, 79 | }); 80 | } 81 | 82 | const hasMore = 83 | params.page * params.perPage < response.data.total_count; 84 | 85 | return { 86 | results: processedResults, 87 | totalCount: response.data.total_count, 88 | hasMore, 89 | }; 90 | } catch (error: any) { 91 | console.error( 92 | `Error searching repositories with query "${params.query}":`, 93 | error.message 94 | ); 95 | if (error.status === 422) { 96 | return { 97 | results: [], 98 | totalCount: 0, 99 | hasMore: false, 100 | error: "Invalid search query or parameters.", 101 | } as any; 102 | } 103 | return { 104 | results: [], 105 | totalCount: 0, 106 | hasMore: false, 107 | error: error.message, 108 | } as any; 109 | } 110 | } 111 | interface searchWithReadme { 112 | query: string; 113 | endCursor?: string; 114 | } 115 | export async function searchMcpServers2(payload: searchWithReadme) { 116 | const graphQLOperation = ` 117 | query SearchRepositories($userQuery: String!, $endCursor: String ) { 118 | search(query: $userQuery, type: REPOSITORY, first: 10, after: $endCursor ) { 119 | repositoryCount 120 | edges { 121 | node { 122 | ... on Repository { 123 | nameWithOwner 124 | description 125 | url 126 | readme: object(expression: "HEAD:README.md") { 127 | ... on Blob { 128 | text 129 | } 130 | } 131 | } 132 | } 133 | } 134 | pageInfo { 135 | endCursor 136 | hasNextPage 137 | } 138 | } 139 | } 140 | `; 141 | 142 | try { 143 | const session = await vscode.authentication.getSession( 144 | GITHUB_AUTH_PROVIDER_ID, 145 | SCOPES 146 | ); 147 | const { graphql } = await import("@octokit/graphql"); 148 | // 3. Execute the GraphQL operation, passing the 'gitHubSearchSyntaxQuery' 149 | // as the value for '$searchQueryVariable'. 150 | const response: any = await graphql( 151 | // Assuming 'graphql' is your client function 152 | graphQLOperation, 153 | { 154 | userQuery: payload.query, // Pass the constructed string as the variable 155 | endCursor: payload.endCursor, 156 | headers: { 157 | Authorization: `Bearer ${session?.accessToken}`, 158 | }, 159 | } 160 | ); 161 | console.dir(response, { depth: null, colors: true }); 162 | if (response.search.edges.length === 0) { 163 | // throw new Error('No results found'); // Uncomment if you want this behavior 164 | console.log("No results found for your query."); 165 | } 166 | return { 167 | results: response.search.edges.map((edge: any) => { 168 | const repo = edge.node; 169 | return { 170 | fullName: repo.nameWithOwner, 171 | description: repo.description, 172 | url: repo.url, 173 | readme: repo.readme ? repo.readme.text : null, // Handle readme text 174 | }; 175 | }), 176 | totalCount: response.search.repositoryCount, 177 | pageInfo: response.search.pageInfo, 178 | }; 179 | } catch (error: any) { 180 | console.error( 181 | `Error searching repositories with user query "${payload.query}":` 182 | ); 183 | // @octokit/graphql errors often have a 'response.errors' array 184 | if (error.response && error.response.errors) { 185 | error.response.errors.forEach((err: any) => 186 | console.error(`- ${err.message}`) 187 | ); 188 | } else { 189 | console.error(error); 190 | } 191 | throw error; // Re-throw if you want to handle it further up the call stack 192 | } 193 | } 194 | interface GetReadmeParams { 195 | repoOwner: string; 196 | repoName: string; 197 | } 198 | export async function getReadme(payload: GetReadmeParams) { 199 | const graphqlQuery = `query GetReadme($owner: String!, $name: String!) { 200 | repository(owner: $owner, name: $name) { 201 | readme: object(expression: "HEAD:README.md") { 202 | ... on Blob { 203 | text 204 | } 205 | } 206 | } 207 | }`; 208 | const session = await vscode.authentication.getSession( 209 | GITHUB_AUTH_PROVIDER_ID, 210 | SCOPES 211 | ); 212 | const { graphql } = await import("@octokit/graphql"); 213 | const { repository }: any = await graphql( 214 | // Assuming 'graphql' is your client function 215 | graphqlQuery, 216 | { 217 | owner: payload.repoOwner, // Pass the constructed string as the variable 218 | name: payload.repoName, 219 | headers: { 220 | Authorization: `Bearer ${session?.accessToken}`, 221 | }, 222 | } 223 | ); 224 | return repository.readme.text; 225 | } 226 | -------------------------------------------------------------------------------- /src/utilities/signoz.ts: -------------------------------------------------------------------------------- 1 | // src/utilities/signoz.ts 2 | import type * as vscode from "vscode"; 3 | import { logs, SeverityNumber } from "@opentelemetry/api-logs"; 4 | 5 | const otelLogger = logs.getLogger("copilot-mcp"); 6 | 7 | // shared attributes you want on every record (can be filled later) 8 | let common: Record = {}; 9 | 10 | /** Call once – after any async look-ups – to set global attributes */ 11 | export function setCommonLogAttributes(attrs: Record) { 12 | common = { ...common, ...attrs }; 13 | } 14 | 15 | export const signozTelemetry: vscode.TelemetrySender = { 16 | sendEventData(event, data) { 17 | otelLogger.emit({ 18 | body: event, 19 | severityNumber: SeverityNumber.INFO, 20 | severityText: "INFO", 21 | attributes: { ...common, ...data }, 22 | }); 23 | }, 24 | 25 | sendErrorData(error, data) { 26 | otelLogger.emit({ 27 | body: error.message, 28 | severityNumber: SeverityNumber.ERROR, 29 | severityText: "ERROR", 30 | attributes: { 31 | ...common, 32 | ...data, 33 | errorName: error.name, 34 | stack: error.stack ?? "", 35 | }, 36 | }); 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "Node16", 4 | "moduleResolution": "Node16", 5 | "target": "ES2022", 6 | "lib": [ 7 | "ES2022", 8 | "DOM" 9 | ], 10 | "sourceMap": true, 11 | "rootDir": "src", 12 | "strict": true, /* enable all strict type-checking options */ 13 | /* Additional Checks */ 14 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 15 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 16 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 17 | }, 18 | "exclude": ["web", "node_modules", ".vscode-test"] 19 | } 20 | -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Setup 13 | 14 | * install the recommended extensions (amodio.tsl-problem-matcher, ms-vscode.extension-test-runner, and dbaeumer.vscode-eslint) 15 | 16 | 17 | ## Get up and running straight away 18 | 19 | * Press `F5` to open a new window with your extension loaded. 20 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 21 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 22 | * Find output from your extension in the debug console. 23 | 24 | ## Make changes 25 | 26 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 27 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 28 | 29 | 30 | ## Explore the API 31 | 32 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 33 | 34 | ## Run tests 35 | 36 | * Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner) 37 | * Run the "watch" task via the **Tasks: Run Task** command. Make sure this is running, or tests might not be discovered. 38 | * Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A` 39 | * See the output of the test result in the Test Results view. 40 | * Make changes to `src/test/extension.test.ts` or create new test files inside the `test` folder. 41 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 42 | * You can create folders inside the `test` folder to structure your tests any way you want. 43 | 44 | ## Go further 45 | 46 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 47 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. 48 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 49 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: 13 | 14 | ```js 15 | export default tseslint.config({ 16 | extends: [ 17 | // Remove ...tseslint.configs.recommended and replace with this 18 | ...tseslint.configs.recommendedTypeChecked, 19 | // Alternatively, use this for stricter rules 20 | ...tseslint.configs.strictTypeChecked, 21 | // Optionally, add this for stylistic rules 22 | ...tseslint.configs.stylisticTypeChecked, 23 | ], 24 | languageOptions: { 25 | // other options... 26 | parserOptions: { 27 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 28 | tsconfigRootDir: import.meta.dirname, 29 | }, 30 | }, 31 | }) 32 | ``` 33 | 34 | You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: 35 | 36 | ```js 37 | // eslint.config.js 38 | import reactX from 'eslint-plugin-react-x' 39 | import reactDom from 'eslint-plugin-react-dom' 40 | 41 | export default tseslint.config({ 42 | plugins: { 43 | // Add the react-x and react-dom plugins 44 | 'react-x': reactX, 45 | 'react-dom': reactDom, 46 | }, 47 | rules: { 48 | // other rules... 49 | // Enable its recommended typescript rules 50 | ...reactX.configs['recommended-typescript'].rules, 51 | ...reactDom.configs.recommended.rules, 52 | }, 53 | }) 54 | ``` 55 | -------------------------------------------------------------------------------- /web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/index.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 | } -------------------------------------------------------------------------------- /web/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | '@typescript-eslint/no-explicit-any': 'off' 27 | }, 28 | }, 29 | ) 30 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@hookform/resolvers": "^5.0.1", 14 | "@octokit/rest": "^21.1.1", 15 | "@radix-ui/react-accordion": "^1.2.10", 16 | "@radix-ui/react-alert-dialog": "^1.1.13", 17 | "@radix-ui/react-aspect-ratio": "^1.1.6", 18 | "@radix-ui/react-avatar": "^1.1.9", 19 | "@radix-ui/react-checkbox": "^1.3.1", 20 | "@radix-ui/react-collapsible": "^1.1.10", 21 | "@radix-ui/react-context-menu": "^2.2.14", 22 | "@radix-ui/react-dialog": "^1.1.13", 23 | "@radix-ui/react-dropdown-menu": "^2.1.14", 24 | "@radix-ui/react-hover-card": "^1.1.13", 25 | "@radix-ui/react-label": "^2.1.6", 26 | "@radix-ui/react-menubar": "^1.1.14", 27 | "@radix-ui/react-navigation-menu": "^1.2.12", 28 | "@radix-ui/react-popover": "^1.1.13", 29 | "@radix-ui/react-progress": "^1.1.6", 30 | "@radix-ui/react-radio-group": "^1.3.6", 31 | "@radix-ui/react-scroll-area": "^1.2.8", 32 | "@radix-ui/react-select": "^2.2.4", 33 | "@radix-ui/react-separator": "^1.1.6", 34 | "@radix-ui/react-slider": "^1.3.4", 35 | "@radix-ui/react-slot": "^1.2.2", 36 | "@radix-ui/react-switch": "^1.2.4", 37 | "@radix-ui/react-tabs": "^1.1.11", 38 | "@radix-ui/react-toggle": "^1.1.8", 39 | "@radix-ui/react-toggle-group": "^1.1.9", 40 | "@radix-ui/react-tooltip": "^1.2.6", 41 | "@tailwindcss/vite": "^4.1.5", 42 | "@vscode-elements/elements": "^1.15.0", 43 | "class-variance-authority": "^0.7.1", 44 | "clsx": "^2.1.1", 45 | "cmdk": "^1.1.1", 46 | "date-fns": "^4.1.0", 47 | "embla-carousel-react": "^8.6.0", 48 | "input-otp": "^1.4.2", 49 | "lucide-react": "^0.507.0", 50 | "next-themes": "^0.4.6", 51 | "react": "^19.1.0", 52 | "react-day-picker": "^8.10.1", 53 | "react-dom": "^19.1.0", 54 | "react-hook-form": "^7.56.3", 55 | "react-markdown": "^10.1.0", 56 | "react-resizable-panels": "^3.0.1", 57 | "recharts": "^2.15.3", 58 | "rehype-raw": "^7.0.0", 59 | "remark-gfm": "^4.0.1", 60 | "sonner": "^2.0.3", 61 | "tailwind-merge": "^3.2.0", 62 | "tailwindcss": "^4.1.5", 63 | "tailwindcss-animate": "^1.0.7", 64 | "vaul": "^1.1.2", 65 | "vscode-messenger": "^0.5.1", 66 | "vscode-messenger-webview": "^0.5.1", 67 | "zod": "^3.24.4" 68 | }, 69 | "devDependencies": { 70 | "@eslint/js": "^9.25.0", 71 | "@types/node": "^22.15.12", 72 | "@types/react": "^19.1.2", 73 | "@types/react-dom": "^19.1.2", 74 | "@vitejs/plugin-react": "^4.4.1", 75 | "eslint": "^9.25.0", 76 | "eslint-plugin-react-hooks": "^5.2.0", 77 | "eslint-plugin-react-refresh": "^0.4.19", 78 | "globals": "^16.0.0", 79 | "tw-animate-css": "^1.2.9", 80 | "typescript": "~5.8.3", 81 | "typescript-eslint": "^8.30.1", 82 | "vite": "^6.3.5" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /web/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .read-the-docs { 37 | color: #888; 38 | } 39 | -------------------------------------------------------------------------------- /web/src/App.tsx: -------------------------------------------------------------------------------- 1 | import MCPServers from "./components/MCPServers"; 2 | 3 | function App() { 4 | return ( 5 | 6 | ); 7 | } 8 | 9 | export default App; -------------------------------------------------------------------------------- /web/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/components/MCPServers.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; 3 | import InstalledMCPServers from './InstalledMCPServers'; 4 | import SearchMCPServers from './SearchMCPServers'; 5 | 6 | const MCPServers: React.FC = () => { 7 | return ( 8 |
9 | 10 | 11 | 12 | Installed 13 | 14 | 15 | Search 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | ); 27 | }; 28 | 29 | export default MCPServers; 30 | -------------------------------------------------------------------------------- /web/src/components/RepoCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useMemo } from "react"; 2 | import ReactMarkdown from "react-markdown"; 3 | import remarkGfm from "remark-gfm"; 4 | import rehypeRaw from "rehype-raw"; 5 | import { 6 | Card, 7 | CardContent, 8 | CardDescription, 9 | CardHeader, 10 | CardTitle, 11 | CardFooter, 12 | } from "@/components/ui/card"; 13 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 14 | import { Star, Code2, CalendarDays, BookText } from "lucide-react"; 15 | import { Button } from "@/components/ui/button"; 16 | import { useVscodeApi } from "@/contexts/VscodeApiContext"; 17 | import { Messenger } from "vscode-messenger-webview"; 18 | import { aiAssistedSetupType, getReadmeType } from "../../../src/shared/types/rpcTypes"; 19 | interface RepoCardProps { 20 | repo: any; 21 | } 22 | 23 | const RepoCard: React.FC = ({ repo }) => { 24 | const vscodeApi = useVscodeApi(); 25 | const messenger = useMemo(() => new Messenger(vscodeApi), [vscodeApi]); 26 | const [isLoading, setIsLoading] = useState(false); 27 | const [readmeContent, setReadmeContent] = useState(null); 28 | const [shouldShowInstallButton, setShouldShowInstallButton] = useState(false); 29 | const [installError, setInstallError] = useState(false); 30 | 31 | useEffect(() => { 32 | messenger.start(); 33 | async function getReadme() { 34 | const result= await messenger.sendRequest(getReadmeType, { 35 | type: 'extension' 36 | },{ 37 | name: repo.name, 38 | fullName: repo.fullName, 39 | owner: repo.owner 40 | }); 41 | if (result.readme && result.fullName === repo.fullName) { 42 | setReadmeContent(result.readme); 43 | const readmeLines = result.readme.replace(/\n/g, ""); 44 | const showButton = 45 | readmeLines.includes(`"command": "uvx"`) || 46 | readmeLines.includes(`"command": "npx"`) || 47 | readmeLines.includes(`"command": "pypi"`) || 48 | readmeLines.includes(`"command": "docker"`); 49 | setShouldShowInstallButton(showButton); 50 | } else { 51 | setShouldShowInstallButton(false); 52 | } 53 | } 54 | getReadme(); 55 | 56 | 57 | // const message = event.data; 58 | // console.log("Received message:", message); 59 | // if (message.type === "receivedReadme" && message.payload.fullName === repo.fullName) { 60 | // const currentReadmeContent: string = message.payload.readme; 61 | // setReadmeContent(currentReadmeContent); 62 | // if (currentReadmeContent) { 63 | // const readmeLines = currentReadmeContent.replace(/\n/g, ""); 64 | // const showButton = 65 | // readmeLines.includes(`"command": "uvx"`) || 66 | // readmeLines.includes(`"command": "npx"`) || 67 | // readmeLines.includes(`"command": "pypi"`) || 68 | // readmeLines.includes(`"command": "docker"`); 69 | // setShouldShowInstallButton(showButton); 70 | // } else { 71 | // setShouldShowInstallButton(false); 72 | // } 73 | // } else if (message.type === "finishInstall" && message.payload.fullName === repo.fullName) { 74 | // setIsLoading(false); 75 | // } 76 | // }; 77 | 78 | // window.addEventListener("message", handleMessage); 79 | // return () => { 80 | // window.removeEventListener("message", handleMessage); 81 | // }; 82 | // eslint-disable-next-line react-hooks/exhaustive-deps 83 | }, [messenger]); 84 | 85 | // Helper function to format date (can be expanded) 86 | const formatDate = (dateString: string) => { 87 | return new Date(dateString).toLocaleDateString(undefined, { 88 | year: "numeric", 89 | month: "short", 90 | day: "numeric", 91 | }); 92 | }; 93 | 94 | const fallbackName = repo.author.name.substring(0, 2).toUpperCase(); 95 | 96 | // shouldShowInstallButton is now a state variable updated by useEffect 97 | 98 | const handleInstallClick = async () => { 99 | setIsLoading(true); 100 | if (!readmeContent) { 101 | console.error("README content is not available for install."); 102 | setIsLoading(false); 103 | return; 104 | } 105 | // Listener for "finish" is now part of the main message handler 106 | const result = await messenger.sendRequest(aiAssistedSetupType, { 107 | type: 'extension' 108 | }, { 109 | repo: { ...repo, readme: readmeContent } 110 | }); 111 | if (result) { 112 | setIsLoading(false); 113 | setInstallError(false); 114 | } else { 115 | setIsLoading(false); 116 | setInstallError(true); 117 | } 118 | // setIsLoading(false) will be handled by 'finishInstall' message 119 | }; 120 | 121 | return ( 122 | 123 | 124 |
125 | 126 | 127 | {fallbackName} 128 | 129 |
130 | 136 | 137 | {repo.fullName} 138 | 139 | 140 | 141 | By: {repo.author.name} 142 | 143 |
144 |
145 |
146 | 147 |

148 | {repo.description || "No description available."} 149 |

150 |
151 |
152 | 153 | {repo.stars.toLocaleString()} Stars 154 |
155 |
156 | 157 | {repo.language || "N/A"} 158 |
159 |
160 | 161 | Last Updated: {formatDate(repo.updatedAt)} 162 |
163 |
164 |
165 |

166 | 167 | README Snippet: 168 |

169 |
170 | {readmeContent ? ( 171 | 176 | ) : ( 177 |

Loading README...

178 | )} 179 |
180 |
181 |
182 | {shouldShowInstallButton && !isLoading && ( 183 | 184 | 191 | 192 | )} 193 | {/* Red button to indicate install error with "Retry Install" text */} 194 | {installError && ( 195 | 196 | 203 | 204 | )} 205 |
206 | ); 207 | }; 208 | 209 | export default RepoCard; 210 | -------------------------------------------------------------------------------- /web/src/components/SearchMCPServers.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, type ChangeEvent, useMemo } from 'react'; 2 | import { Input } from '@/components/ui/input'; 3 | import { Button } from '@/components/ui/button'; 4 | import { useDebounce } from '@/hooks/useDebounce'; 5 | import { useVscodeApi } from '@/contexts/VscodeApiContext'; 6 | import { Messenger } from 'vscode-messenger-webview'; 7 | import RepoCard from './RepoCard'; 8 | import { searchServersType } from '../../../src/shared/types/rpcTypes'; 9 | // Define an interface for the VSCode API 10 | interface McpServerAuthor { 11 | name: string; 12 | profileUrl: string; 13 | avatarUrl: string; 14 | } 15 | 16 | interface SearchResult { 17 | id: number; 18 | url: string; 19 | name: string; 20 | fullName: string; 21 | stars: number; 22 | author: McpServerAuthor; 23 | description: string | null; 24 | readme: string; // A short snippet of the README 25 | language: string | null; 26 | updatedAt: string; 27 | // Add other properties as needed based on the actual structure 28 | } 29 | 30 | const ITEMS_PER_PAGE = 10; // Define items per page 31 | 32 | const SearchMCPServers: React.FC = () => { 33 | const vscodeApi = useVscodeApi(); 34 | const messenger = useMemo(() => new Messenger(vscodeApi), [vscodeApi]); 35 | 36 | const [searchTerm, setSearchTerm] = useState(''); 37 | const debouncedSearchTerm = useDebounce(searchTerm, 500); 38 | const [results, setResults] = useState([]); 39 | const [isLoading, setIsLoading] = useState(false); 40 | const [error, setError] = useState(null); 41 | 42 | const [currentPage, setCurrentPage] = useState(1); 43 | const [totalResults, setTotalResults] = useState(0); 44 | // totalPages will be derived from totalResults and ITEMS_PER_PAGE 45 | 46 | useEffect(() => { 47 | const handleMessage = (event: MessageEvent) => { 48 | const message = event.data; // The event data normally comes in the `data` property 49 | if (message.type === 'receivedSearchResults') { 50 | console.log('receivedSearchResults', message.data.results); 51 | setResults(message.data.results || []); 52 | setTotalResults(message.data.totalCount || 0); 53 | setIsLoading(false); 54 | setError(null); 55 | } else if (message.type === 'error') { // Handle potential errors from backend 56 | setError(message.data.message || 'An unknown error occurred.'); 57 | setIsLoading(false); 58 | } 59 | }; 60 | 61 | window.addEventListener('message', handleMessage); 62 | 63 | // Cleanup listener on component unmount 64 | return () => { 65 | window.removeEventListener('message', handleMessage); 66 | }; 67 | }, []); 68 | 69 | useEffect(() => { 70 | messenger.start(); 71 | }, [messenger]); 72 | 73 | const performSearch = async (page: number, debouncedSearchTerm: string) => { 74 | if (debouncedSearchTerm && messenger) { 75 | setIsLoading(true); 76 | setError(null); 77 | const result = await messenger.sendRequest(searchServersType, { 78 | type: 'extension' 79 | },{ 80 | query: debouncedSearchTerm, 81 | page: page, 82 | perPage: ITEMS_PER_PAGE, 83 | }); 84 | setResults(result.results || []); 85 | setTotalResults(result.totalCount || 0); 86 | setIsLoading(false); 87 | setError(null); 88 | } else if (!debouncedSearchTerm) { 89 | setResults([]); 90 | setTotalResults(0); 91 | setIsLoading(false); 92 | setError(null); 93 | } 94 | }; 95 | 96 | 97 | 98 | useEffect(() => { 99 | // Reset to page 1 when search term changes 100 | setCurrentPage(1); 101 | performSearch(1, debouncedSearchTerm); // Perform search with page 1 102 | // eslint-disable-next-line react-hooks/exhaustive-deps 103 | }, [debouncedSearchTerm]); // Only trigger on debouncedSearchTerm change for new searches 104 | 105 | 106 | useEffect(() => { 107 | // This effect handles subsequent page changes for an existing search term 108 | if (debouncedSearchTerm) { // Only search if there's a term 109 | performSearch(currentPage, debouncedSearchTerm); 110 | } 111 | // eslint-disable-next-line react-hooks/exhaustive-deps 112 | }, [currentPage]); // Trigger only when currentPage changes for pagination 113 | 114 | 115 | const handleInputChange = (event: ChangeEvent) => { 116 | setSearchTerm(event.target.value); 117 | }; 118 | 119 | const totalPages = Math.ceil(totalResults / ITEMS_PER_PAGE); 120 | 121 | const handlePreviousPage = () => { 122 | setCurrentPage((prev) => Math.max(prev - 1, 1)); 123 | }; 124 | 125 | const handleNextPage = () => { 126 | setCurrentPage((prev) => Math.min(prev + 1, totalPages)); 127 | }; 128 | 129 | return ( 130 |
131 | 138 | {isLoading &&

Loading...

} 139 | {error &&

Error: {error}

} 140 | {!isLoading && !error && debouncedSearchTerm && results.length === 0 && ( 141 |

No results found for "{debouncedSearchTerm}".

142 | )} 143 | {!isLoading && !error && results.length > 0 && ( 144 | <> 145 |
146 | {(results as any).map((repo: any) => ( 147 | 148 | ))} 149 |
150 | {totalPages > 1 && ( 151 |
152 | 158 | 159 | Page {currentPage} of {totalPages} (Total: {totalResults}) 160 | 161 | 167 |
168 | )} 169 | 170 | )} 171 |
172 | ); 173 | }; 174 | 175 | export default SearchMCPServers; -------------------------------------------------------------------------------- /web/src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 3 | import { ChevronDownIcon } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | function Accordion({ 8 | ...props 9 | }: React.ComponentProps) { 10 | return 11 | } 12 | 13 | function AccordionItem({ 14 | className, 15 | ...props 16 | }: React.ComponentProps) { 17 | return ( 18 | 23 | ) 24 | } 25 | 26 | function AccordionTrigger({ 27 | className, 28 | children, 29 | ...props 30 | }: React.ComponentProps) { 31 | return ( 32 | 33 | svg]:rotate-180", 37 | className 38 | )} 39 | {...props} 40 | > 41 | {children} 42 | 43 | 44 | 45 | ) 46 | } 47 | 48 | function AccordionContent({ 49 | className, 50 | children, 51 | ...props 52 | }: React.ComponentProps) { 53 | return ( 54 | 59 |
{children}
60 |
61 | ) 62 | } 63 | 64 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 65 | -------------------------------------------------------------------------------- /web/src/components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" 5 | 6 | import { cn } from "@/lib/utils" 7 | import { buttonVariants } from "@/components/ui/button" 8 | 9 | function AlertDialog({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return 13 | } 14 | 15 | function AlertDialogTrigger({ 16 | ...props 17 | }: React.ComponentProps) { 18 | return ( 19 | 20 | ) 21 | } 22 | 23 | function AlertDialogPortal({ 24 | ...props 25 | }: React.ComponentProps) { 26 | return ( 27 | 28 | ) 29 | } 30 | 31 | function AlertDialogOverlay({ 32 | className, 33 | ...props 34 | }: React.ComponentProps) { 35 | return ( 36 | 44 | ) 45 | } 46 | 47 | function AlertDialogContent({ 48 | className, 49 | ...props 50 | }: React.ComponentProps) { 51 | return ( 52 | 53 | 54 | 62 | 63 | ) 64 | } 65 | 66 | function AlertDialogHeader({ 67 | className, 68 | ...props 69 | }: React.ComponentProps<"div">) { 70 | return ( 71 |
76 | ) 77 | } 78 | 79 | function AlertDialogFooter({ 80 | className, 81 | ...props 82 | }: React.ComponentProps<"div">) { 83 | return ( 84 |
92 | ) 93 | } 94 | 95 | function AlertDialogTitle({ 96 | className, 97 | ...props 98 | }: React.ComponentProps) { 99 | return ( 100 | 105 | ) 106 | } 107 | 108 | function AlertDialogDescription({ 109 | className, 110 | ...props 111 | }: React.ComponentProps) { 112 | return ( 113 | 118 | ) 119 | } 120 | 121 | function AlertDialogAction({ 122 | className, 123 | ...props 124 | }: React.ComponentProps) { 125 | return ( 126 | 130 | ) 131 | } 132 | 133 | function AlertDialogCancel({ 134 | className, 135 | ...props 136 | }: React.ComponentProps) { 137 | return ( 138 | 142 | ) 143 | } 144 | 145 | export { 146 | AlertDialog, 147 | AlertDialogPortal, 148 | AlertDialogOverlay, 149 | AlertDialogTrigger, 150 | AlertDialogContent, 151 | AlertDialogHeader, 152 | AlertDialogFooter, 153 | AlertDialogTitle, 154 | AlertDialogDescription, 155 | AlertDialogAction, 156 | AlertDialogCancel, 157 | } 158 | -------------------------------------------------------------------------------- /web/src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * 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 | -------------------------------------------------------------------------------- /web/src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 2 | 3 | function AspectRatio({ 4 | ...props 5 | }: React.ComponentProps) { 6 | return 7 | } 8 | 9 | export { AspectRatio } 10 | -------------------------------------------------------------------------------- /web/src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * 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 | -------------------------------------------------------------------------------- /web/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-auto", 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", 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 } -------------------------------------------------------------------------------- /web/src/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * 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