The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .dockerignore
├── .git-blame-ignore-revs
├── .gitattributes
├── .github
    └── workflows
    │   ├── claude.yml
    │   ├── cli_tests.yml
    │   ├── e2e_tests.yml
    │   └── main.yml
├── .gitignore
├── .node-version
├── .npmrc
├── .prettierignore
├── .prettierrc
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── SECURITY.md
├── cli
    ├── package.json
    ├── scripts
    │   ├── cli-tests.js
    │   └── make-executable.js
    ├── src
    │   ├── cli.ts
    │   ├── client
    │   │   ├── connection.ts
    │   │   ├── index.ts
    │   │   ├── prompts.ts
    │   │   ├── resources.ts
    │   │   ├── tools.ts
    │   │   └── types.ts
    │   ├── error-handler.ts
    │   ├── index.ts
    │   └── transport.ts
    └── tsconfig.json
├── client
    ├── .gitignore
    ├── README.md
    ├── bin
    │   ├── client.js
    │   └── start.js
    ├── components.json
    ├── e2e
    │   ├── global-teardown.js
    │   └── transport-type-dropdown.spec.ts
    ├── eslint.config.js
    ├── index.html
    ├── jest.config.cjs
    ├── package.json
    ├── playwright.config.ts
    ├── postcss.config.js
    ├── public
    │   └── mcp.svg
    ├── src
    │   ├── App.css
    │   ├── App.tsx
    │   ├── __mocks__
    │   │   └── styleMock.js
    │   ├── __tests__
    │   │   └── App.config.test.tsx
    │   ├── components
    │   │   ├── AuthDebugger.tsx
    │   │   ├── ConsoleTab.tsx
    │   │   ├── DynamicJsonForm.tsx
    │   │   ├── HistoryAndNotifications.tsx
    │   │   ├── JsonEditor.tsx
    │   │   ├── JsonView.tsx
    │   │   ├── ListPane.tsx
    │   │   ├── OAuthCallback.tsx
    │   │   ├── OAuthDebugCallback.tsx
    │   │   ├── OAuthFlowProgress.tsx
    │   │   ├── PingTab.tsx
    │   │   ├── PromptsTab.tsx
    │   │   ├── ResourceLinkView.tsx
    │   │   ├── ResourcesTab.tsx
    │   │   ├── RootsTab.tsx
    │   │   ├── SamplingRequest.tsx
    │   │   ├── SamplingTab.tsx
    │   │   ├── Sidebar.tsx
    │   │   ├── ToolResults.tsx
    │   │   ├── ToolsTab.tsx
    │   │   ├── __tests__
    │   │   │   ├── AuthDebugger.test.tsx
    │   │   │   ├── DynamicJsonForm.array.test.tsx
    │   │   │   ├── DynamicJsonForm.test.tsx
    │   │   │   ├── HistoryAndNotifications.test.tsx
    │   │   │   ├── ResourcesTab.test.tsx
    │   │   │   ├── Sidebar.test.tsx
    │   │   │   ├── ToolsTab.test.tsx
    │   │   │   ├── samplingRequest.test.tsx
    │   │   │   └── samplingTab.test.tsx
    │   │   └── ui
    │   │   │   ├── alert.tsx
    │   │   │   ├── button.tsx
    │   │   │   ├── checkbox.tsx
    │   │   │   ├── combobox.tsx
    │   │   │   ├── command.tsx
    │   │   │   ├── dialog.tsx
    │   │   │   ├── input.tsx
    │   │   │   ├── label.tsx
    │   │   │   ├── popover.tsx
    │   │   │   ├── select.tsx
    │   │   │   ├── tabs.tsx
    │   │   │   ├── textarea.tsx
    │   │   │   ├── toast.tsx
    │   │   │   ├── toaster.tsx
    │   │   │   └── tooltip.tsx
    │   ├── index.css
    │   ├── lib
    │   │   ├── auth-types.ts
    │   │   ├── auth.ts
    │   │   ├── configurationTypes.ts
    │   │   ├── constants.ts
    │   │   ├── hooks
    │   │   │   ├── __tests__
    │   │   │   │   └── useConnection.test.tsx
    │   │   │   ├── useCompletionState.ts
    │   │   │   ├── useConnection.ts
    │   │   │   ├── useDraggablePane.ts
    │   │   │   ├── useTheme.ts
    │   │   │   └── useToast.ts
    │   │   ├── notificationTypes.ts
    │   │   ├── oauth-state-machine.ts
    │   │   └── utils.ts
    │   ├── main.tsx
    │   ├── utils
    │   │   ├── __tests__
    │   │   │   ├── configUtils.test.ts
    │   │   │   ├── escapeUnicode.test.ts
    │   │   │   ├── jsonUtils.test.ts
    │   │   │   ├── oauthUtils.ts
    │   │   │   └── schemaUtils.test.ts
    │   │   ├── configUtils.ts
    │   │   ├── escapeUnicode.ts
    │   │   ├── jsonUtils.ts
    │   │   ├── oauthUtils.ts
    │   │   └── schemaUtils.ts
    │   └── vite-env.d.ts
    ├── tailwind.config.js
    ├── tsconfig.app.json
    ├── tsconfig.jest.json
    ├── tsconfig.json
    ├── tsconfig.node.json
    └── vite.config.ts
├── mcp-inspector.png
├── package-lock.json
├── package.json
├── sample-config.json
├── scripts
    ├── README.md
    ├── check-version-consistency.js
    └── update-version.js
└── server
    ├── package.json
    ├── src
        ├── index.ts
        └── mcpProxy.ts
    └── tsconfig.json


/.dockerignore:
--------------------------------------------------------------------------------
 1 | # Version control
 2 | .git
 3 | .gitignore
 4 | 
 5 | # Node.js
 6 | node_modules
 7 | npm-debug.log
 8 | 
 9 | # Build artifacts
10 | client/dist
11 | client/build
12 | server/dist
13 | server/build
14 | 
15 | # Environment variables
16 | .env
17 | .env.local
18 | .env.development
19 | .env.test
20 | .env.production
21 | 
22 | # Editor files
23 | .vscode
24 | .idea
25 | 
26 | # Logs
27 | logs
28 | *.log
29 | 
30 | # Testing
31 | coverage
32 | 
33 | # Docker
34 | Dockerfile
35 | .dockerignore


--------------------------------------------------------------------------------
/.git-blame-ignore-revs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/modelcontextprotocol/inspector/1ea8e9a9395386b0e77efaa64ccaa4c28c571d10/.git-blame-ignore-revs


--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | package-lock.json linguist-generated=true
2 | 


--------------------------------------------------------------------------------
/.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 |       (
17 |         (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
18 |         (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
19 |         (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
20 |         (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
21 |       ) &&
22 |       (
23 |         github.actor == 'ihrpr' ||
24 |         github.actor == 'olaservo'
25 |       )
26 |     runs-on: ubuntu-latest
27 |     permissions:
28 |       contents: read
29 |       pull-requests: read
30 |       issues: read
31 |       id-token: write
32 |     steps:
33 |       - name: Checkout repository
34 |         uses: actions/checkout@v4
35 |         with:
36 |           fetch-depth: 1
37 | 
38 |       - name: Run Claude Code
39 |         id: claude
40 |         uses: anthropics/claude-code-action@beta
41 |         with:
42 |           anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
43 | 
44 |           # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
45 |           # model: "claude-opus-4-20250514"
46 | 
47 |           # Optional: Customize the trigger phrase (default: @claude)
48 |           # trigger_phrase: "/claude"
49 | 
50 |           # Optional: Trigger when specific user is assigned to an issue
51 |           # assignee_trigger: "claude-bot"
52 | 
53 |           # Optional: Allow Claude to run specific commands
54 |           # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
55 | 
56 |           # Optional: Add custom instructions for Claude to customize its behavior for your project
57 |           # custom_instructions: |
58 |           #   Follow our coding standards
59 |           #   Ensure all new code has tests
60 |           #   Use TypeScript for new files
61 | 
62 |           # Optional: Custom environment variables for Claude
63 |           # claude_env: |
64 |           #   NODE_ENV: test
65 | 


--------------------------------------------------------------------------------
/.github/workflows/cli_tests.yml:
--------------------------------------------------------------------------------
 1 | name: CLI Tests
 2 | 
 3 | on:
 4 |   push:
 5 |     paths:
 6 |       - "cli/**"
 7 |   pull_request:
 8 |     paths:
 9 |       - "cli/**"
10 | 
11 | jobs:
12 |   test:
13 |     runs-on: ubuntu-latest
14 |     defaults:
15 |       run:
16 |         working-directory: ./cli
17 |     steps:
18 |       - uses: actions/checkout@v4
19 | 
20 |       - name: Set up Node.js
21 |         uses: actions/setup-node@v4
22 |         with:
23 |           node-version-file: package.json
24 |           cache: npm
25 | 
26 |       - name: Install dependencies
27 |         run: |
28 |           cd ..
29 |           npm ci --ignore-scripts
30 | 
31 |       - name: Build CLI
32 |         run: npm run build
33 | 
34 |       - name: Explicitly pre-install test dependencies
35 |         run: npx -y @modelcontextprotocol/server-everything --help || true
36 | 
37 |       - name: Run tests
38 |         run: npm test
39 |         env:
40 |           NPM_CONFIG_YES: true
41 |           CI: true
42 | 


--------------------------------------------------------------------------------
/.github/workflows/e2e_tests.yml:
--------------------------------------------------------------------------------
 1 | name: Playwright Tests
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [main]
 6 |   pull_request:
 7 |     branches: [main]
 8 | 
 9 | jobs:
10 |   test:
11 |     # Installing Playright dependencies can take quite awhile, and also depends on GitHub CI load.
12 |     timeout-minutes: 15
13 |     runs-on: ubuntu-latest
14 | 
15 |     steps:
16 |       - name: Install dependencies
17 |         run: |
18 |           sudo apt-get update
19 |           sudo apt-get install -y libwoff1
20 | 
21 |       - uses: actions/checkout@v4
22 | 
23 |       - uses: actions/setup-node@v4
24 |         id: setup_node
25 |         with:
26 |           node-version-file: package.json
27 |           cache: npm
28 | 
29 |       # Cache Playwright browsers
30 |       - name: Cache Playwright browsers
31 |         id: cache-playwright
32 |         uses: actions/cache@v4
33 |         with:
34 |           path: ~/.cache/ms-playwright # The default Playwright cache path
35 |           key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }} # Cache key based on OS and package-lock.json
36 |           restore-keys: |
37 |             ${{ runner.os }}-playwright-
38 | 
39 |       - name: Install dependencies
40 |         run: npm ci
41 | 
42 |       - name: Install Playwright dependencies
43 |         run: npx playwright install-deps
44 | 
45 |       - name: Install Playwright and browsers unless cached
46 |         run: npx playwright install --with-deps
47 |         if: steps.cache-playwright.outputs.cache-hit != 'true'
48 | 
49 |       - name: Run Playwright tests
50 |         id: playwright-tests
51 |         run: npm run test:e2e
52 | 
53 |       - name: Upload Playwright Report and Screenshots
54 |         uses: actions/upload-artifact@v4
55 |         if: steps.playwright-tests.conclusion != 'skipped'
56 |         with:
57 |           name: playwright-report
58 |           path: |
59 |             client/playwright-report/
60 |             client/test-results/
61 |             client/results.json
62 |           retention-days: 2
63 | 
64 |       - name: Publish Playwright Test Summary
65 |         uses: daun/playwright-report-summary@v3
66 |         if: steps.playwright-tests.conclusion != 'skipped'
67 |         with:
68 |           create-comment: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
69 |           report-file: client/results.json
70 |           comment-title: "🎭 Playwright E2E Test Results"
71 |           job-summary: true
72 |           icon-style: "emojis"
73 |           custom-info: |
74 |             **Test Environment:** Ubuntu Latest, Node.js ${{ steps.setup_node.outputs.node-version }}
75 |             **Browsers:** Chromium, Firefox
76 | 
77 |             📊 [View Detailed HTML Report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) (download artifacts)
78 |           test-command: "npm run test:e2e"
79 | 


--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
  1 | on:
  2 |   push:
  3 |     branches:
  4 |       - main
  5 | 
  6 |   pull_request:
  7 |   release:
  8 |     types: [published]
  9 | 
 10 | jobs:
 11 |   build:
 12 |     runs-on: ubuntu-latest
 13 | 
 14 |     steps:
 15 |       - uses: actions/checkout@v4
 16 | 
 17 |       - name: Check formatting
 18 |         run: npx prettier --check .
 19 | 
 20 |       - uses: actions/setup-node@v4
 21 |         with:
 22 |           node-version-file: package.json
 23 |           cache: npm
 24 | 
 25 |       # Working around https://github.com/npm/cli/issues/4828
 26 |       # - run: npm ci
 27 |       - run: npm install --no-package-lock
 28 | 
 29 |       - name: Check version consistency
 30 |         run: npm run check-version
 31 | 
 32 |       - name: Check linting
 33 |         working-directory: ./client
 34 |         run: npm run lint
 35 | 
 36 |       - name: Run client tests
 37 |         working-directory: ./client
 38 |         run: npm test
 39 | 
 40 |       - run: npm run build
 41 | 
 42 |   publish:
 43 |     runs-on: ubuntu-latest
 44 |     if: github.event_name == 'release'
 45 |     environment: release
 46 |     needs: build
 47 | 
 48 |     permissions:
 49 |       contents: read
 50 |       id-token: write
 51 | 
 52 |     steps:
 53 |       - uses: actions/checkout@v4
 54 |       - uses: actions/setup-node@v4
 55 |         with:
 56 |           node-version-file: package.json
 57 |           cache: npm
 58 |           registry-url: "https://registry.npmjs.org"
 59 | 
 60 |       # Working around https://github.com/npm/cli/issues/4828
 61 |       # - run: npm ci
 62 |       - run: npm install --no-package-lock
 63 | 
 64 |       # TODO: Add --provenance once the repo is public
 65 |       - run: npm run publish-all
 66 |         env:
 67 |           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
 68 | 
 69 |   publish-github-container-registry:
 70 |     runs-on: ubuntu-latest
 71 |     if: github.event_name == 'release'
 72 |     environment: release
 73 |     needs: build
 74 |     permissions:
 75 |       contents: read
 76 |       packages: write
 77 |       attestations: write
 78 |       id-token: write
 79 |     steps:
 80 |       - uses: actions/checkout@v4
 81 | 
 82 |       - name: Log in to the Container registry
 83 |         uses: docker/login-action@v3
 84 |         with:
 85 |           registry: ghcr.io
 86 |           username: ${{ github.actor }}
 87 |           password: ${{ secrets.GITHUB_TOKEN }}
 88 | 
 89 |       - name: Extract metadata (tags, labels) for Docker
 90 |         id: meta
 91 |         uses: docker/metadata-action@v5
 92 |         with:
 93 |           images: ghcr.io/${{ github.repository }}
 94 | 
 95 |       - name: Set up QEMU
 96 |         uses: docker/setup-qemu-action@v3
 97 | 
 98 |       - name: Set up Docker Buildx
 99 |         uses: docker/setup-buildx-action@v3
100 | 
101 |       - name: Build and push Docker image
102 |         id: push
103 |         uses: docker/build-push-action@v6
104 |         with:
105 |           context: .
106 |           push: true
107 |           platforms: linux/amd64,linux/arm64
108 |           tags: ${{ steps.meta.outputs.tags }}
109 |           labels: ${{ steps.meta.outputs.labels }}
110 | 
111 |       - name: Generate artifact attestation
112 |         uses: actions/attest-build-provenance@v2
113 |         with:
114 |           subject-name: ghcr.io/${{ github.repository }}
115 |           subject-digest: ${{ steps.push.outputs.digest }}
116 |           push-to-registry: true
117 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | .DS_Store
 2 | .vscode
 3 | .idea
 4 | node_modules/
 5 | *-workspace/
 6 | server/build
 7 | client/dist
 8 | client/tsconfig.app.tsbuildinfo
 9 | client/tsconfig.node.tsbuildinfo
10 | cli/build
11 | test-output
12 | # symlinked by `npm run link:sdk`:
13 | sdk
14 | client/playwright-report/
15 | client/results.json
16 | client/test-results/
17 | 


--------------------------------------------------------------------------------
/.node-version:
--------------------------------------------------------------------------------
1 | 22.x.x
2 | 


--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry="https://registry.npmjs.org/"
2 | @modelcontextprotocol:registry="https://registry.npmjs.org/"
3 | 


--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | packages
2 | server/build
3 | CODE_OF_CONDUCT.md
4 | SECURITY.md
5 | 


--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/modelcontextprotocol/inspector/1ea8e9a9395386b0e77efaa64ccaa4c28c571d10/.prettierrc


--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
 1 | # MCP Inspector Development Guide
 2 | 
 3 | ## Build Commands
 4 | 
 5 | - Build all: `npm run build`
 6 | - Build client: `npm run build-client`
 7 | - Build server: `npm run build-server`
 8 | - Development mode: `npm run dev` (use `npm run dev:windows` on Windows)
 9 | - Format code: `npm run prettier-fix`
10 | - Client lint: `cd client && npm run lint`
11 | 
12 | ## Code Style Guidelines
13 | 
14 | - Use TypeScript with proper type annotations
15 | - Follow React functional component patterns with hooks
16 | - Use ES modules (import/export) not CommonJS
17 | - Use Prettier for formatting (auto-formatted on commit)
18 | - Follow existing naming conventions:
19 |   - camelCase for variables and functions
20 |   - PascalCase for component names and types
21 |   - kebab-case for file names
22 | - Use async/await for asynchronous operations
23 | - Implement proper error handling with try/catch blocks
24 | - Use Tailwind CSS for styling in the client
25 | - Keep components small and focused on a single responsibility
26 | 
27 | ## Project Organization
28 | 
29 | The project is organized as a monorepo with workspaces:
30 | 
31 | - `client/`: React frontend with Vite, TypeScript and Tailwind
32 | - `server/`: Express backend with TypeScript
33 | - `cli/`: Command-line interface for testing and invoking MCP server methods directly
34 | 


--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
  1 | # Contributor Covenant Code of Conduct
  2 | 
  3 | ## Our Pledge
  4 | 
  5 | We as members, contributors, and leaders pledge to make participation in our
  6 | community a harassment-free experience for everyone, regardless of age, body
  7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
  8 | identity and expression, level of experience, education, socio-economic status,
  9 | nationality, personal appearance, race, religion, or sexual identity
 10 | and orientation.
 11 | 
 12 | We pledge to act and interact in ways that contribute to an open, welcoming,
 13 | diverse, inclusive, and healthy community.
 14 | 
 15 | ## Our Standards
 16 | 
 17 | Examples of behavior that contributes to a positive environment for our
 18 | community include:
 19 | 
 20 | * Demonstrating empathy and kindness toward other people
 21 | * Being respectful of differing opinions, viewpoints, and experiences
 22 | * Giving and gracefully accepting constructive feedback
 23 | * Accepting responsibility and apologizing to those affected by our mistakes,
 24 |   and learning from the experience
 25 | * Focusing on what is best not just for us as individuals, but for the
 26 |   overall community
 27 | 
 28 | Examples of unacceptable behavior include:
 29 | 
 30 | * The use of sexualized language or imagery, and sexual attention or
 31 |   advances of any kind
 32 | * Trolling, insulting or derogatory comments, and personal or political attacks
 33 | * Public or private harassment
 34 | * Publishing others' private information, such as a physical or email
 35 |   address, without their explicit permission
 36 | * Other conduct which could reasonably be considered inappropriate in a
 37 |   professional setting
 38 | 
 39 | ## Enforcement Responsibilities
 40 | 
 41 | Community leaders are responsible for clarifying and enforcing our standards of
 42 | acceptable behavior and will take appropriate and fair corrective action in
 43 | response to any behavior that they deem inappropriate, threatening, offensive,
 44 | or harmful.
 45 | 
 46 | Community leaders have the right and responsibility to remove, edit, or reject
 47 | comments, commits, code, wiki edits, issues, and other contributions that are
 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
 49 | decisions when appropriate.
 50 | 
 51 | ## Scope
 52 | 
 53 | This Code of Conduct applies within all community spaces, and also applies when
 54 | an individual is officially representing the community in public spaces.
 55 | Examples of representing our community include using an official e-mail address,
 56 | posting via an official social media account, or acting as an appointed
 57 | representative at an online or offline event.
 58 | 
 59 | ## Enforcement
 60 | 
 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
 62 | reported to the community leaders responsible for enforcement at
 63 | mcp-coc@anthropic.com.
 64 | All complaints will be reviewed and investigated promptly and fairly.
 65 | 
 66 | All community leaders are obligated to respect the privacy and security of the
 67 | reporter of any incident.
 68 | 
 69 | ## Enforcement Guidelines
 70 | 
 71 | Community leaders will follow these Community Impact Guidelines in determining
 72 | the consequences for any action they deem in violation of this Code of Conduct:
 73 | 
 74 | ### 1. Correction
 75 | 
 76 | **Community Impact**: Use of inappropriate language or other behavior deemed
 77 | unprofessional or unwelcome in the community.
 78 | 
 79 | **Consequence**: A private, written warning from community leaders, providing
 80 | clarity around the nature of the violation and an explanation of why the
 81 | behavior was inappropriate. A public apology may be requested.
 82 | 
 83 | ### 2. Warning
 84 | 
 85 | **Community Impact**: A violation through a single incident or series
 86 | of actions.
 87 | 
 88 | **Consequence**: A warning with consequences for continued behavior. No
 89 | interaction with the people involved, including unsolicited interaction with
 90 | those enforcing the Code of Conduct, for a specified period of time. This
 91 | includes avoiding interactions in community spaces as well as external channels
 92 | like social media. Violating these terms may lead to a temporary or
 93 | permanent ban.
 94 | 
 95 | ### 3. Temporary Ban
 96 | 
 97 | **Community Impact**: A serious violation of community standards, including
 98 | sustained inappropriate behavior.
 99 | 
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 | 
106 | ### 4. Permanent Ban
107 | 
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior,  harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 | 
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 | 
115 | ## Attribution
116 | 
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 | 
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 | 
124 | [homepage]: https://www.contributor-covenant.org
125 | 
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 | 


--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
 1 | # Contributing to Model Context Protocol Inspector
 2 | 
 3 | Thanks for your interest in contributing! This guide explains how to get involved.
 4 | 
 5 | ## Getting Started
 6 | 
 7 | 1. Fork the repository and clone it locally
 8 | 2. Install dependencies with `npm install`
 9 | 3. Run `npm run dev` to start both client and server in development mode
10 | 4. Use the web UI at http://localhost:6274 to interact with the inspector
11 | 
12 | ## Development Process & Pull Requests
13 | 
14 | 1. Create a new branch for your changes
15 | 2. Make your changes following existing code style and conventions. You can run `npm run prettier-check` and `npm run prettier-fix` as applicable.
16 | 3. Test changes locally by running `npm test` and `npm run test:e2e`
17 | 4. Update documentation as needed
18 | 5. Use clear commit messages explaining your changes
19 | 6. Verify all changes work as expected
20 | 7. Submit a pull request
21 | 8. PRs will be reviewed by maintainers
22 | 
23 | ## Code of Conduct
24 | 
25 | This project follows our [Code of Conduct](CODE_OF_CONDUCT.md). Please read it before contributing.
26 | 
27 | ## Security
28 | 
29 | If you find a security vulnerability, please refer to our [Security Policy](SECURITY.md) for reporting instructions.
30 | 
31 | ## Questions?
32 | 
33 | Feel free to [open an issue](https://github.com/modelcontextprotocol/mcp-inspector/issues) for questions or create a discussion for general topics.
34 | 
35 | ## License
36 | 
37 | By contributing, you agree that your contributions will be licensed under the MIT license.
38 | 


--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
 1 | # Build stage
 2 | FROM node:24-slim AS builder
 3 | 
 4 | # Set working directory
 5 | WORKDIR /app
 6 | 
 7 | # Copy package files for installation
 8 | COPY package*.json ./
 9 | COPY .npmrc ./
10 | COPY client/package*.json ./client/
11 | COPY server/package*.json ./server/
12 | COPY cli/package*.json ./cli/
13 | 
14 | # Install dependencies
15 | RUN npm ci --ignore-scripts
16 | 
17 | # Copy source files
18 | COPY . .
19 | 
20 | # Build the application
21 | RUN npm run build
22 | 
23 | # Production stage
24 | FROM node:24-slim
25 | 
26 | WORKDIR /app
27 | 
28 | # Copy package files for production
29 | COPY package*.json ./
30 | COPY .npmrc ./
31 | COPY client/package*.json ./client/
32 | COPY server/package*.json ./server/
33 | COPY cli/package*.json ./cli/
34 | 
35 | # Install only production dependencies
36 | RUN npm ci --omit=dev --ignore-scripts
37 | 
38 | # Copy built files from builder stage
39 | COPY --from=builder /app/client/dist ./client/dist
40 | COPY --from=builder /app/client/bin ./client/bin
41 | COPY --from=builder /app/server/build ./server/build
42 | COPY --from=builder /app/cli/build ./cli/build
43 | 
44 | # Set default port values as environment variables
45 | ENV CLIENT_PORT=6274
46 | ENV SERVER_PORT=6277
47 | 
48 | # Document which ports the application uses internally
49 | EXPOSE ${CLIENT_PORT} ${SERVER_PORT}
50 | 
51 | # Use ENTRYPOINT with CMD for arguments
52 | ENTRYPOINT ["npm", "start"]


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2024 Anthropic, PBC
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | 


--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
 1 | # Security Policy
 2 | Thank you for helping us keep the inspector secure.
 3 | 
 4 | ## Reporting Security Issues
 5 | 
 6 | This project is maintained by [Anthropic](https://www.anthropic.com/) as part of the Model Context Protocol project.
 7 | 
 8 | The security of our systems and user data is Anthropic’s top priority. We appreciate the work of security researchers acting in good faith in identifying and reporting potential vulnerabilities.
 9 | 
10 | Our security program is managed on HackerOne and we ask that any validated vulnerability in this functionality be reported through their [submission form](https://hackerone.com/anthropic-vdp/reports/new?type=team&report_type=vulnerability).
11 | 
12 | ## Vulnerability Disclosure Program
13 | 
14 | Our Vulnerability Program Guidelines are defined on our [HackerOne program page](https://hackerone.com/anthropic-vdp).
15 | 


--------------------------------------------------------------------------------
/cli/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "@modelcontextprotocol/inspector-cli",
 3 |   "version": "0.16.1",
 4 |   "description": "CLI for the Model Context Protocol inspector",
 5 |   "license": "MIT",
 6 |   "author": "Anthropic, PBC (https://anthropic.com)",
 7 |   "homepage": "https://modelcontextprotocol.io",
 8 |   "bugs": "https://github.com/modelcontextprotocol/inspector/issues",
 9 |   "main": "build/cli.js",
10 |   "type": "module",
11 |   "bin": {
12 |     "mcp-inspector-cli": "build/cli.js"
13 |   },
14 |   "files": [
15 |     "build"
16 |   ],
17 |   "scripts": {
18 |     "build": "tsc",
19 |     "postbuild": "node scripts/make-executable.js",
20 |     "test": "node scripts/cli-tests.js"
21 |   },
22 |   "devDependencies": {},
23 |   "dependencies": {
24 |     "@modelcontextprotocol/sdk": "^1.13.1",
25 |     "commander": "^13.1.0",
26 |     "spawn-rx": "^5.1.2"
27 |   }
28 | }
29 | 


--------------------------------------------------------------------------------
/cli/scripts/make-executable.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Cross-platform script to make a file executable
 3 |  */
 4 | import { promises as fs } from "fs";
 5 | import { platform } from "os";
 6 | import { execSync } from "child_process";
 7 | import path from "path";
 8 | 
 9 | const TARGET_FILE = path.resolve("build/cli.js");
10 | 
11 | async function makeExecutable() {
12 |   try {
13 |     // On Unix-like systems (Linux, macOS), use chmod
14 |     if (platform() !== "win32") {
15 |       execSync(`chmod +x "${TARGET_FILE}"`);
16 |       console.log("Made file executable with chmod");
17 |     } else {
18 |       // On Windows, no need to make files "executable" in the Unix sense
19 |       // Just ensure the file exists
20 |       await fs.access(TARGET_FILE);
21 |       console.log("File exists and is accessible on Windows");
22 |     }
23 |   } catch (error) {
24 |     console.error("Error making file executable:", error);
25 |     process.exit(1);
26 |   }
27 | }
28 | 
29 | makeExecutable();
30 | 


--------------------------------------------------------------------------------
/cli/src/client/connection.ts:
--------------------------------------------------------------------------------
 1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 2 | import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
 3 | import { McpResponse } from "./types.js";
 4 | 
 5 | export const validLogLevels = [
 6 |   "trace",
 7 |   "debug",
 8 |   "info",
 9 |   "warn",
10 |   "error",
11 | ] as const;
12 | 
13 | export type LogLevel = (typeof validLogLevels)[number];
14 | 
15 | export async function connect(
16 |   client: Client,
17 |   transport: Transport,
18 | ): Promise<void> {
19 |   try {
20 |     await client.connect(transport);
21 |   } catch (error) {
22 |     throw new Error(
23 |       `Failed to connect to MCP server: ${error instanceof Error ? error.message : String(error)}`,
24 |     );
25 |   }
26 | }
27 | 
28 | export async function disconnect(transport: Transport): Promise<void> {
29 |   try {
30 |     await transport.close();
31 |   } catch (error) {
32 |     throw new Error(
33 |       `Failed to disconnect from MCP server: ${error instanceof Error ? error.message : String(error)}`,
34 |     );
35 |   }
36 | }
37 | 
38 | // Set logging level
39 | export async function setLoggingLevel(
40 |   client: Client,
41 |   level: LogLevel,
42 | ): Promise<McpResponse> {
43 |   try {
44 |     const response = await client.setLoggingLevel(level as any);
45 |     return response;
46 |   } catch (error) {
47 |     throw new Error(
48 |       `Failed to set logging level: ${error instanceof Error ? error.message : String(error)}`,
49 |     );
50 |   }
51 | }
52 | 


--------------------------------------------------------------------------------
/cli/src/client/index.ts:
--------------------------------------------------------------------------------
1 | // Re-export everything from the client modules
2 | export * from "./connection.js";
3 | export * from "./prompts.js";
4 | export * from "./resources.js";
5 | export * from "./tools.js";
6 | export * from "./types.js";
7 | 


--------------------------------------------------------------------------------
/cli/src/client/prompts.ts:
--------------------------------------------------------------------------------
 1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 2 | import { McpResponse } from "./types.js";
 3 | 
 4 | // List available prompts
 5 | export async function listPrompts(client: Client): Promise<McpResponse> {
 6 |   try {
 7 |     const response = await client.listPrompts();
 8 |     return response;
 9 |   } catch (error) {
10 |     throw new Error(
11 |       `Failed to list prompts: ${error instanceof Error ? error.message : String(error)}`,
12 |     );
13 |   }
14 | }
15 | 
16 | // Get a prompt
17 | export async function getPrompt(
18 |   client: Client,
19 |   name: string,
20 |   args?: Record<string, string>,
21 | ): Promise<McpResponse> {
22 |   try {
23 |     const response = await client.getPrompt({
24 |       name,
25 |       arguments: args || {},
26 |     });
27 | 
28 |     return response;
29 |   } catch (error) {
30 |     throw new Error(
31 |       `Failed to get prompt: ${error instanceof Error ? error.message : String(error)}`,
32 |     );
33 |   }
34 | }
35 | 


--------------------------------------------------------------------------------
/cli/src/client/resources.ts:
--------------------------------------------------------------------------------
 1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 2 | import { McpResponse } from "./types.js";
 3 | 
 4 | // List available resources
 5 | export async function listResources(client: Client): Promise<McpResponse> {
 6 |   try {
 7 |     const response = await client.listResources();
 8 |     return response;
 9 |   } catch (error) {
10 |     throw new Error(
11 |       `Failed to list resources: ${error instanceof Error ? error.message : String(error)}`,
12 |     );
13 |   }
14 | }
15 | 
16 | // Read a resource
17 | export async function readResource(
18 |   client: Client,
19 |   uri: string,
20 | ): Promise<McpResponse> {
21 |   try {
22 |     const response = await client.readResource({ uri });
23 |     return response;
24 |   } catch (error) {
25 |     throw new Error(
26 |       `Failed to read resource ${uri}: ${error instanceof Error ? error.message : String(error)}`,
27 |     );
28 |   }
29 | }
30 | 
31 | // List resource templates
32 | export async function listResourceTemplates(
33 |   client: Client,
34 | ): Promise<McpResponse> {
35 |   try {
36 |     const response = await client.listResourceTemplates();
37 |     return response;
38 |   } catch (error) {
39 |     throw new Error(
40 |       `Failed to list resource templates: ${error instanceof Error ? error.message : String(error)}`,
41 |     );
42 |   }
43 | }
44 | 


--------------------------------------------------------------------------------
/cli/src/client/tools.ts:
--------------------------------------------------------------------------------
 1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 2 | import { Tool } from "@modelcontextprotocol/sdk/types.js";
 3 | import { McpResponse } from "./types.js";
 4 | 
 5 | type JsonSchemaType = {
 6 |   type: "string" | "number" | "integer" | "boolean" | "array" | "object";
 7 |   description?: string;
 8 |   properties?: Record<string, JsonSchemaType>;
 9 |   items?: JsonSchemaType;
10 | };
11 | 
12 | export async function listTools(client: Client): Promise<McpResponse> {
13 |   try {
14 |     const response = await client.listTools();
15 |     return response;
16 |   } catch (error) {
17 |     throw new Error(
18 |       `Failed to list tools: ${error instanceof Error ? error.message : String(error)}`,
19 |     );
20 |   }
21 | }
22 | 
23 | function convertParameterValue(value: string, schema: JsonSchemaType): unknown {
24 |   if (!value) {
25 |     return value;
26 |   }
27 | 
28 |   if (schema.type === "number" || schema.type === "integer") {
29 |     return Number(value);
30 |   }
31 | 
32 |   if (schema.type === "boolean") {
33 |     return value.toLowerCase() === "true";
34 |   }
35 | 
36 |   if (schema.type === "object" || schema.type === "array") {
37 |     try {
38 |       return JSON.parse(value);
39 |     } catch (error) {
40 |       return value;
41 |     }
42 |   }
43 | 
44 |   return value;
45 | }
46 | 
47 | function convertParameters(
48 |   tool: Tool,
49 |   params: Record<string, string>,
50 | ): Record<string, unknown> {
51 |   const result: Record<string, unknown> = {};
52 |   const properties = tool.inputSchema.properties || {};
53 | 
54 |   for (const [key, value] of Object.entries(params)) {
55 |     const paramSchema = properties[key] as JsonSchemaType | undefined;
56 | 
57 |     if (paramSchema) {
58 |       result[key] = convertParameterValue(value, paramSchema);
59 |     } else {
60 |       // If no schema is found for this parameter, keep it as string
61 |       result[key] = value;
62 |     }
63 |   }
64 | 
65 |   return result;
66 | }
67 | 
68 | export async function callTool(
69 |   client: Client,
70 |   name: string,
71 |   args: Record<string, string>,
72 | ): Promise<McpResponse> {
73 |   try {
74 |     const toolsResponse = await listTools(client);
75 |     const tools = toolsResponse.tools as Tool[];
76 |     const tool = tools.find((t) => t.name === name);
77 | 
78 |     let convertedArgs: Record<string, unknown> = args;
79 | 
80 |     if (tool) {
81 |       // Convert parameters based on the tool's schema
82 |       convertedArgs = convertParameters(tool, args);
83 |     }
84 | 
85 |     const response = await client.callTool({
86 |       name: name,
87 |       arguments: convertedArgs,
88 |     });
89 |     return response;
90 |   } catch (error) {
91 |     throw new Error(
92 |       `Failed to call tool ${name}: ${error instanceof Error ? error.message : String(error)}`,
93 |     );
94 |   }
95 | }
96 | 


--------------------------------------------------------------------------------
/cli/src/client/types.ts:
--------------------------------------------------------------------------------
1 | export type McpResponse = Record<string, unknown>;
2 | 


--------------------------------------------------------------------------------
/cli/src/error-handler.ts:
--------------------------------------------------------------------------------
 1 | function formatError(error: unknown): string {
 2 |   let message: string;
 3 | 
 4 |   if (error instanceof Error) {
 5 |     message = error.message;
 6 |   } else if (typeof error === "string") {
 7 |     message = error;
 8 |   } else {
 9 |     message = "Unknown error";
10 |   }
11 | 
12 |   return message;
13 | }
14 | 
15 | export function handleError(error: unknown): never {
16 |   const errorMessage = formatError(error);
17 |   console.error(errorMessage);
18 | 
19 |   process.exit(1);
20 | }
21 | 


--------------------------------------------------------------------------------
/cli/src/transport.ts:
--------------------------------------------------------------------------------
 1 | import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
 2 | import {
 3 |   getDefaultEnvironment,
 4 |   StdioClientTransport,
 5 | } from "@modelcontextprotocol/sdk/client/stdio.js";
 6 | import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
 7 | import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
 8 | import { findActualExecutable } from "spawn-rx";
 9 | 
10 | export type TransportOptions = {
11 |   transportType: "sse" | "stdio" | "http";
12 |   command?: string;
13 |   args?: string[];
14 |   url?: string;
15 | };
16 | 
17 | function createStdioTransport(options: TransportOptions): Transport {
18 |   let args: string[] = [];
19 | 
20 |   if (options.args !== undefined) {
21 |     args = options.args;
22 |   }
23 | 
24 |   const processEnv: Record<string, string> = {};
25 | 
26 |   for (const [key, value] of Object.entries(process.env)) {
27 |     if (value !== undefined) {
28 |       processEnv[key] = value;
29 |     }
30 |   }
31 | 
32 |   const defaultEnv = getDefaultEnvironment();
33 | 
34 |   const env: Record<string, string> = {
35 |     ...processEnv,
36 |     ...defaultEnv,
37 |   };
38 | 
39 |   const { cmd: actualCommand, args: actualArgs } = findActualExecutable(
40 |     options.command ?? "",
41 |     args,
42 |   );
43 | 
44 |   return new StdioClientTransport({
45 |     command: actualCommand,
46 |     args: actualArgs,
47 |     env,
48 |     stderr: "pipe",
49 |   });
50 | }
51 | 
52 | export function createTransport(options: TransportOptions): Transport {
53 |   const { transportType } = options;
54 | 
55 |   try {
56 |     if (transportType === "stdio") {
57 |       return createStdioTransport(options);
58 |     }
59 | 
60 |     // If not STDIO, then it must be either SSE or HTTP.
61 |     if (!options.url) {
62 |       throw new Error("URL must be provided for SSE or HTTP transport types.");
63 |     }
64 |     const url = new URL(options.url);
65 | 
66 |     if (transportType === "sse") {
67 |       return new SSEClientTransport(url);
68 |     }
69 | 
70 |     if (transportType === "http") {
71 |       return new StreamableHTTPClientTransport(url);
72 |     }
73 | 
74 |     throw new Error(`Unsupported transport type: ${transportType}`);
75 |   } catch (error) {
76 |     throw new Error(
77 |       `Failed to create transport: ${error instanceof Error ? error.message : String(error)}`,
78 |     );
79 |   }
80 | }
81 | 


--------------------------------------------------------------------------------
/cli/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "NodeNext",
 5 |     "moduleResolution": "NodeNext",
 6 |     "outDir": "./build",
 7 |     "rootDir": "./src",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true,
12 |     "resolveJsonModule": true,
13 |     "noUncheckedIndexedAccess": true
14 |   },
15 |   "include": ["src/**/*"],
16 |   "exclude": ["node_modules", "packages", "**/*.spec.ts", "build"]
17 | }
18 | 


--------------------------------------------------------------------------------
/client/.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 | 


--------------------------------------------------------------------------------
/client/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/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-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 | - Configure the top-level `parserOptions` property like this:
15 | 
16 | ```js
17 | export default tseslint.config({
18 |   languageOptions: {
19 |     // other options...
20 |     parserOptions: {
21 |       project: ["./tsconfig.node.json", "./tsconfig.app.json"],
22 |       tsconfigRootDir: import.meta.dirname,
23 |     },
24 |   },
25 | });
26 | ```
27 | 
28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
29 | - Optionally add `...tseslint.configs.stylisticTypeChecked`
30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
31 | 
32 | ```js
33 | // eslint.config.js
34 | import react from "eslint-plugin-react";
35 | 
36 | export default tseslint.config({
37 |   // Set the react version
38 |   settings: { react: { version: "18.3" } },
39 |   plugins: {
40 |     // Add the react plugin
41 |     react,
42 |   },
43 |   rules: {
44 |     // other rules...
45 |     // Enable its recommended rules
46 |     ...react.configs.recommended.rules,
47 |     ...react.configs["jsx-runtime"].rules,
48 |   },
49 | });
50 | ```
51 | 


--------------------------------------------------------------------------------
/client/bin/client.js:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env node
 2 | 
 3 | import open from "open";
 4 | import { join, dirname } from "path";
 5 | import { fileURLToPath } from "url";
 6 | import handler from "serve-handler";
 7 | import http from "http";
 8 | 
 9 | const __dirname = dirname(fileURLToPath(import.meta.url));
10 | const distPath = join(__dirname, "../dist");
11 | 
12 | const server = http.createServer((request, response) => {
13 |   const handlerOptions = {
14 |     public: distPath,
15 |     rewrites: [{ source: "/**", destination: "/index.html" }],
16 |     headers: [
17 |       {
18 |         // Ensure index.html is never cached
19 |         source: "index.html",
20 |         headers: [
21 |           {
22 |             key: "Cache-Control",
23 |             value: "no-cache, no-store, max-age=0",
24 |           },
25 |         ],
26 |       },
27 |       {
28 |         // Allow long-term caching for hashed assets
29 |         source: "assets/**",
30 |         headers: [
31 |           {
32 |             key: "Cache-Control",
33 |             value: "public, max-age=31536000, immutable",
34 |           },
35 |         ],
36 |       },
37 |     ],
38 |   };
39 | 
40 |   return handler(request, response, handlerOptions);
41 | });
42 | 
43 | const port = parseInt(process.env.CLIENT_PORT || "6274", 10);
44 | const host = process.env.HOST || "localhost";
45 | server.on("listening", () => {
46 |   const url = process.env.INSPECTOR_URL || `http://${host}:${port}`;
47 |   console.log(`\n🚀 MCP Inspector is up and running at:\n   ${url}\n`);
48 |   if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") {
49 |     console.log(`🌐 Opening browser...`);
50 |     open(url);
51 |   }
52 | });
53 | server.on("error", (err) => {
54 |   if (err.message.includes(`EADDRINUSE`)) {
55 |     console.error(
56 |       `❌  MCP Inspector PORT IS IN USE at http://${host}:${port} ❌ `,
57 |     );
58 |   } else {
59 |     throw err;
60 |   }
61 | });
62 | server.listen(port, host);
63 | 


--------------------------------------------------------------------------------
/client/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": "tailwind.config.js",
 8 |     "css": "src/index.css",
 9 |     "baseColor": "slate",
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 | }
21 | 


--------------------------------------------------------------------------------
/client/e2e/global-teardown.js:
--------------------------------------------------------------------------------
 1 | import { rimraf } from "rimraf";
 2 | 
 3 | async function globalTeardown() {
 4 |   if (!process.env.CI) {
 5 |     console.log("Cleaning up test-results directory...");
 6 |     // Add a small delay to ensure all Playwright files are written
 7 |     await new Promise((resolve) => setTimeout(resolve, 100));
 8 |     await rimraf("./e2e/test-results");
 9 |     console.log("Test-results directory cleaned up.");
10 |   }
11 | }
12 | 
13 | export default globalTeardown;
14 | 
15 | // Call the function when this script is run directly
16 | if (import.meta.url === `file://${process.argv[1]}`) {
17 |   globalTeardown().catch(console.error);
18 | }
19 | 


--------------------------------------------------------------------------------
/client/e2e/transport-type-dropdown.spec.ts:
--------------------------------------------------------------------------------
  1 | import { test, expect } from "@playwright/test";
  2 | 
  3 | // Adjust the URL if your dev server runs on a different port
  4 | const APP_URL = "http://localhost:6274/";
  5 | 
  6 | test.describe("Transport Type Dropdown", () => {
  7 |   test("should have options for STDIO, SSE, and Streamable HTTP", async ({
  8 |     page,
  9 |   }) => {
 10 |     await page.goto(APP_URL);
 11 | 
 12 |     // Wait for the Transport Type dropdown to be visible
 13 |     const selectTrigger = page.getByLabel("Transport Type");
 14 |     await expect(selectTrigger).toBeVisible();
 15 | 
 16 |     // Open the dropdown
 17 |     await selectTrigger.click();
 18 | 
 19 |     // Check for the three options
 20 |     await expect(page.getByRole("option", { name: "STDIO" })).toBeVisible();
 21 |     await expect(page.getByRole("option", { name: "SSE" })).toBeVisible();
 22 |     await expect(
 23 |       page.getByRole("option", { name: "Streamable HTTP" }),
 24 |     ).toBeVisible();
 25 |   });
 26 | 
 27 |   test("should show Command and Arguments fields and hide URL field when Transport Type is STDIO", async ({
 28 |     page,
 29 |   }) => {
 30 |     await page.goto(APP_URL);
 31 | 
 32 |     // Wait for the Transport Type dropdown to be visible
 33 |     const selectTrigger = page.getByLabel("Transport Type");
 34 |     await expect(selectTrigger).toBeVisible();
 35 | 
 36 |     // Open the dropdown and select STDIO
 37 |     await selectTrigger.click();
 38 |     await page.getByRole("option", { name: "STDIO" }).click();
 39 | 
 40 |     // Wait for the form to update
 41 |     await page.waitForTimeout(100);
 42 | 
 43 |     // Check that Command and Arguments fields are visible
 44 |     await expect(page.locator("#command-input")).toBeVisible();
 45 |     await expect(page.locator("#arguments-input")).toBeVisible();
 46 | 
 47 |     // Check that URL field is not visible
 48 |     await expect(page.locator("#sse-url-input")).not.toBeVisible();
 49 | 
 50 |     // Also verify the labels are present
 51 |     await expect(page.getByText("Command")).toBeVisible();
 52 |     await expect(page.getByText("Arguments")).toBeVisible();
 53 |     await expect(page.getByText("URL")).not.toBeVisible();
 54 |   });
 55 | 
 56 |   test("should show URL field and hide Command and Arguments fields when Transport Type is SSE", async ({
 57 |     page,
 58 |   }) => {
 59 |     await page.goto(APP_URL);
 60 | 
 61 |     // Wait for the Transport Type dropdown to be visible
 62 |     const selectTrigger = page.getByLabel("Transport Type");
 63 |     await expect(selectTrigger).toBeVisible();
 64 | 
 65 |     // Open the dropdown and select SSE
 66 |     await selectTrigger.click();
 67 |     await page.getByRole("option", { name: "SSE" }).click();
 68 | 
 69 |     // Wait for the form to update
 70 |     await page.waitForTimeout(100);
 71 | 
 72 |     // Check that URL field is visible
 73 |     await expect(page.locator("#sse-url-input")).toBeVisible();
 74 | 
 75 |     // Check that Command and Arguments fields are not visible
 76 |     await expect(page.locator("#command-input")).not.toBeVisible();
 77 |     await expect(page.locator("#arguments-input")).not.toBeVisible();
 78 | 
 79 |     // Also verify the labels are present/absent
 80 |     await expect(page.getByText("URL")).toBeVisible();
 81 |     await expect(page.getByText("Command")).not.toBeVisible();
 82 |     await expect(page.getByText("Arguments")).not.toBeVisible();
 83 |   });
 84 | 
 85 |   test("should show URL field and hide Command and Arguments fields when Transport Type is Streamable HTTP", async ({
 86 |     page,
 87 |   }) => {
 88 |     await page.goto(APP_URL);
 89 | 
 90 |     // Wait for the Transport Type dropdown to be visible
 91 |     const selectTrigger = page.getByLabel("Transport Type");
 92 |     await expect(selectTrigger).toBeVisible();
 93 | 
 94 |     // Open the dropdown and select Streamable HTTP
 95 |     await selectTrigger.click();
 96 |     await page.getByRole("option", { name: "Streamable HTTP" }).click();
 97 | 
 98 |     // Wait for the form to update
 99 |     await page.waitForTimeout(100);
100 | 
101 |     // Check that URL field is visible
102 |     await expect(page.locator("#sse-url-input")).toBeVisible();
103 | 
104 |     // Check that Command and Arguments fields are not visible
105 |     await expect(page.locator("#command-input")).not.toBeVisible();
106 |     await expect(page.locator("#arguments-input")).not.toBeVisible();
107 | 
108 |     // Also verify the labels are present/absent
109 |     await expect(page.getByText("URL")).toBeVisible();
110 |     await expect(page.getByText("Command")).not.toBeVisible();
111 |     await expect(page.getByText("Arguments")).not.toBeVisible();
112 |   });
113 | });
114 | 


--------------------------------------------------------------------------------
/client/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 |     },
27 |   },
28 | );
29 | 


--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
 1 | <!doctype html>
 2 | <html lang="en">
 3 |   <head>
 4 |     <meta charset="UTF-8" />
 5 |     <link rel="icon" type="image/svg+xml" href="/mcp.svg" />
 6 |     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 7 |     <title>MCP Inspector</title>
 8 |   </head>
 9 |   <body>
10 |     <div id="root" class="w-full"></div>
11 |     <script type="module" src="/src/main.tsx"></script>
12 |   </body>
13 | </html>
14 | 


--------------------------------------------------------------------------------
/client/jest.config.cjs:
--------------------------------------------------------------------------------
 1 | module.exports = {
 2 |   preset: "ts-jest",
 3 |   testEnvironment: "jest-fixed-jsdom",
 4 |   moduleNameMapper: {
 5 |     "^@/(.*)
quot;: "<rootDir>/src/$1",
 6 |     "\\.css
quot;: "<rootDir>/src/__mocks__/styleMock.js",
 7 |   },
 8 |   transform: {
 9 |     "^.+\\.tsx?
quot;: [
10 |       "ts-jest",
11 |       {
12 |         jsx: "react-jsx",
13 |         tsconfig: "tsconfig.jest.json",
14 |       },
15 |     ],
16 |   },
17 |   extensionsToTreatAsEsm: [".ts", ".tsx"],
18 |   testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)
quot;,
19 |   // Exclude directories and files that don't need to be tested
20 |   testPathIgnorePatterns: [
21 |     "/node_modules/",
22 |     "/dist/",
23 |     "/bin/",
24 |     "/e2e/",
25 |     "\\.config\\.(js|ts|cjs|mjs)
quot;,
26 |   ],
27 |   // Exclude the same patterns from coverage reports
28 |   coveragePathIgnorePatterns: [
29 |     "/node_modules/",
30 |     "/dist/",
31 |     "/bin/",
32 |     "/e2e/",
33 |     "\\.config\\.(js|ts|cjs|mjs)
quot;,
34 |   ],
35 | };
36 | 


--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "@modelcontextprotocol/inspector-client",
 3 |   "version": "0.16.1",
 4 |   "description": "Client-side application for the Model Context Protocol inspector",
 5 |   "license": "MIT",
 6 |   "author": "Anthropic, PBC (https://anthropic.com)",
 7 |   "homepage": "https://modelcontextprotocol.io",
 8 |   "bugs": "https://github.com/modelcontextprotocol/inspector/issues",
 9 |   "type": "module",
10 |   "bin": {
11 |     "mcp-inspector-client": "./bin/start.js"
12 |   },
13 |   "files": [
14 |     "bin",
15 |     "dist"
16 |   ],
17 |   "scripts": {
18 |     "dev": "vite --port 6274",
19 |     "build": "tsc -b && vite build",
20 |     "lint": "eslint .",
21 |     "preview": "vite preview --port 6274",
22 |     "test": "jest --config jest.config.cjs",
23 |     "test:watch": "jest --config jest.config.cjs --watch",
24 |     "test:e2e": "playwright test e2e && npm run cleanup:e2e",
25 |     "cleanup:e2e": "node e2e/global-teardown.js"
26 |   },
27 |   "dependencies": {
28 |     "@modelcontextprotocol/sdk": "^1.13.1",
29 |     "@radix-ui/react-checkbox": "^1.1.4",
30 |     "ajv": "^6.12.6",
31 |     "@radix-ui/react-dialog": "^1.1.3",
32 |     "@radix-ui/react-icons": "^1.3.0",
33 |     "@radix-ui/react-label": "^2.1.0",
34 |     "@radix-ui/react-popover": "^1.1.3",
35 |     "@radix-ui/react-select": "^2.1.2",
36 |     "@radix-ui/react-slot": "^1.1.0",
37 |     "@radix-ui/react-tabs": "^1.1.1",
38 |     "@radix-ui/react-toast": "^1.2.6",
39 |     "@radix-ui/react-tooltip": "^1.1.8",
40 |     "class-variance-authority": "^0.7.0",
41 |     "clsx": "^2.1.1",
42 |     "cmdk": "^1.0.4",
43 |     "lucide-react": "^0.523.0",
44 |     "pkce-challenge": "^4.1.0",
45 |     "prismjs": "^1.30.0",
46 |     "react": "^18.3.1",
47 |     "react-dom": "^18.3.1",
48 |     "react-simple-code-editor": "^0.14.1",
49 |     "serve-handler": "^6.1.6",
50 |     "tailwind-merge": "^2.5.3",
51 |     "tailwindcss-animate": "^1.0.7",
52 |     "zod": "^3.23.8"
53 |   },
54 |   "devDependencies": {
55 |     "@eslint/js": "^9.11.1",
56 |     "@testing-library/jest-dom": "^6.6.3",
57 |     "@testing-library/react": "^16.2.0",
58 |     "@types/jest": "^29.5.14",
59 |     "@types/node": "^22.7.5",
60 |     "@types/prismjs": "^1.26.5",
61 |     "@types/react": "^18.3.10",
62 |     "@types/react-dom": "^18.3.0",
63 |     "@types/serve-handler": "^6.1.4",
64 |     "@vitejs/plugin-react": "^4.3.2",
65 |     "autoprefixer": "^10.4.20",
66 |     "co": "^4.6.0",
67 |     "eslint": "^9.11.1",
68 |     "eslint-plugin-react-hooks": "^5.1.0-rc.0",
69 |     "eslint-plugin-react-refresh": "^0.4.12",
70 |     "globals": "^15.9.0",
71 |     "jest": "^29.7.0",
72 |     "jest-environment-jsdom": "^29.7.0",
73 |     "postcss": "^8.4.47",
74 |     "tailwindcss": "^3.4.13",
75 |     "ts-jest": "^29.2.6",
76 |     "typescript": "^5.5.3",
77 |     "typescript-eslint": "^8.7.0",
78 |     "vite": "^6.3.0"
79 |   }
80 | }
81 | 


--------------------------------------------------------------------------------
/client/playwright.config.ts:
--------------------------------------------------------------------------------
 1 | import { defineConfig, devices } from "@playwright/test";
 2 | 
 3 | /**
 4 |  * @see https://playwright.dev/docs/test-configuration
 5 |  */
 6 | export default defineConfig({
 7 |   /* Run your local dev server before starting the tests */
 8 |   webServer: {
 9 |     cwd: "..",
10 |     command: "npm run dev",
11 |     url: "http://localhost:6274",
12 |     reuseExistingServer: !process.env.CI,
13 |   },
14 | 
15 |   testDir: "./e2e",
16 |   outputDir: "./e2e/test-results",
17 |   /* Run tests in files in parallel */
18 |   fullyParallel: true,
19 |   /* Fail the build on CI if you accidentally left test.only in the source code. */
20 |   forbidOnly: !!process.env.CI,
21 |   /* Retry on CI only */
22 |   retries: process.env.CI ? 2 : 0,
23 |   /* Opt out of parallel tests on CI. */
24 |   workers: process.env.CI ? 1 : undefined,
25 |   /* Reporter to use. See https://playwright.dev/docs/test-reporters */
26 |   reporter: process.env.CI
27 |     ? [
28 |         ["html", { outputFolder: "playwright-report" }],
29 |         ["json", { outputFile: "results.json" }],
30 |         ["line"],
31 |       ]
32 |     : [["line"]],
33 |   /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
34 |   use: {
35 |     /* Base URL to use in actions like `await page.goto('/')`. */
36 |     baseURL: "http://localhost:6274",
37 | 
38 |     /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
39 |     trace: "on-first-retry",
40 | 
41 |     /* Take screenshots on failure */
42 |     screenshot: "only-on-failure",
43 | 
44 |     /* Record video on failure */
45 |     video: "retain-on-failure",
46 |   },
47 | 
48 |   /* Configure projects for major browsers */
49 |   projects: [
50 |     {
51 |       name: "chromium",
52 |       use: { ...devices["Desktop Chrome"] },
53 |     },
54 | 
55 |     {
56 |       name: "firefox",
57 |       use: { ...devices["Desktop Firefox"] },
58 |     },
59 | 
60 |     // Skip WebKit on macOS due to compatibility issues
61 |     ...(process.platform !== "darwin"
62 |       ? [
63 |           {
64 |             name: "webkit",
65 |             use: { ...devices["Desktop Safari"] },
66 |           },
67 |         ]
68 |       : []),
69 |   ],
70 | });
71 | 


--------------------------------------------------------------------------------
/client/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 |   plugins: {
3 |     tailwindcss: {},
4 |     autoprefixer: {},
5 |   },
6 | };
7 | 


--------------------------------------------------------------------------------
/client/public/mcp.svg:
--------------------------------------------------------------------------------
 1 | <svg width="180" height="180" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg">
 2 | <g clip-path="url(#clip0_19_13)">
 3 | <path d="M18 84.8528L85.8822 16.9706C95.2548 7.59798 110.451 7.59798 119.823 16.9706V16.9706C129.196 26.3431 129.196 41.5391 119.823 50.9117L68.5581 102.177" stroke="black" stroke-width="12" stroke-linecap="round"/>
 4 | <path d="M69.2652 101.47L119.823 50.9117C129.196 41.5391 144.392 41.5391 153.765 50.9117L154.118 51.2652C163.491 60.6378 163.491 75.8338 154.118 85.2063L92.7248 146.6C89.6006 149.724 89.6006 154.789 92.7248 157.913L105.331 170.52" stroke="black" stroke-width="12" stroke-linecap="round"/>
 5 | <path d="M102.853 33.9411L52.6482 84.1457C43.2756 93.5183 43.2756 108.714 52.6482 118.087V118.087C62.0208 127.459 77.2167 127.459 86.5893 118.087L136.794 67.8822" stroke="black" stroke-width="12" stroke-linecap="round"/>
 6 | </g>
 7 | <defs>
 8 | <clipPath id="clip0_19_13">
 9 | <rect width="180" height="180" fill="white"/>
10 | </clipPath>
11 | </defs>
12 | </svg>
13 | 


--------------------------------------------------------------------------------
/client/src/App.css:
--------------------------------------------------------------------------------
 1 | .logo {
 2 |   height: 6em;
 3 |   padding: 1.5em;
 4 |   will-change: filter;
 5 |   transition: filter 300ms;
 6 | }
 7 | .logo:hover {
 8 |   filter: drop-shadow(0 0 2em #646cffaa);
 9 | }
10 | .logo.react:hover {
11 |   filter: drop-shadow(0 0 2em #61dafbaa);
12 | }
13 | 
14 | @keyframes logo-spin {
15 |   from {
16 |     transform: rotate(0deg);
17 |   }
18 |   to {
19 |     transform: rotate(360deg);
20 |   }
21 | }
22 | 
23 | @media (prefers-reduced-motion: no-preference) {
24 |   a:nth-of-type(2) .logo {
25 |     animation: logo-spin infinite 20s linear;
26 |   }
27 | }
28 | 
29 | .card {
30 |   padding: 2em;
31 | }
32 | 
33 | .read-the-docs {
34 |   color: #888;
35 | }
36 | 


--------------------------------------------------------------------------------
/client/src/__mocks__/styleMock.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 | 


--------------------------------------------------------------------------------
/client/src/components/ConsoleTab.tsx:
--------------------------------------------------------------------------------
 1 | import { TabsContent } from "@/components/ui/tabs";
 2 | 
 3 | const ConsoleTab = () => (
 4 |   <TabsContent value="console" className="h-96">
 5 |     <div className="bg-gray-900 text-gray-100 p-4 rounded-lg h-full font-mono text-sm overflow-auto">
 6 |       <div className="opacity-50">Welcome to MCP Client Console</div>
 7 |       {/* Console output would go here */}
 8 |     </div>
 9 |   </TabsContent>
10 | );
11 | 
12 | export default ConsoleTab;
13 | 


--------------------------------------------------------------------------------
/client/src/components/HistoryAndNotifications.tsx:
--------------------------------------------------------------------------------
  1 | import { ServerNotification } from "@modelcontextprotocol/sdk/types.js";
  2 | import { useState } from "react";
  3 | import JsonView from "./JsonView";
  4 | 
  5 | const HistoryAndNotifications = ({
  6 |   requestHistory,
  7 |   serverNotifications,
  8 | }: {
  9 |   requestHistory: Array<{ request: string; response?: string }>;
 10 |   serverNotifications: ServerNotification[];
 11 | }) => {
 12 |   const [expandedRequests, setExpandedRequests] = useState<{
 13 |     [key: number]: boolean;
 14 |   }>({});
 15 |   const [expandedNotifications, setExpandedNotifications] = useState<{
 16 |     [key: number]: boolean;
 17 |   }>({});
 18 | 
 19 |   const toggleRequestExpansion = (index: number) => {
 20 |     setExpandedRequests((prev) => ({ ...prev, [index]: !prev[index] }));
 21 |   };
 22 | 
 23 |   const toggleNotificationExpansion = (index: number) => {
 24 |     setExpandedNotifications((prev) => ({ ...prev, [index]: !prev[index] }));
 25 |   };
 26 | 
 27 |   return (
 28 |     <div className="bg-card overflow-hidden flex h-full">
 29 |       <div className="flex-1 overflow-y-auto p-4 border-r">
 30 |         <h2 className="text-lg font-semibold mb-4">History</h2>
 31 |         {requestHistory.length === 0 ? (
 32 |           <p className="text-sm text-gray-500 dark:text-gray-400 italic">
 33 |             No history yet
 34 |           </p>
 35 |         ) : (
 36 |           <ul className="space-y-3">
 37 |             {requestHistory
 38 |               .slice()
 39 |               .reverse()
 40 |               .map((request, index) => (
 41 |                 <li
 42 |                   key={index}
 43 |                   className="text-sm text-foreground bg-secondary py-2 px-3 rounded"
 44 |                 >
 45 |                   <div
 46 |                     className="flex justify-between items-center cursor-pointer"
 47 |                     onClick={() =>
 48 |                       toggleRequestExpansion(requestHistory.length - 1 - index)
 49 |                     }
 50 |                   >
 51 |                     <span className="font-mono">
 52 |                       {requestHistory.length - index}.{" "}
 53 |                       {JSON.parse(request.request).method}
 54 |                     </span>
 55 |                     <span>
 56 |                       {expandedRequests[requestHistory.length - 1 - index]
 57 |                         ? "▼"
 58 |                         : "▶"}
 59 |                     </span>
 60 |                   </div>
 61 |                   {expandedRequests[requestHistory.length - 1 - index] && (
 62 |                     <>
 63 |                       <div className="mt-2">
 64 |                         <div className="flex justify-between items-center mb-1">
 65 |                           <span className="font-semibold text-blue-600">
 66 |                             Request:
 67 |                           </span>
 68 |                         </div>
 69 | 
 70 |                         <JsonView
 71 |                           data={request.request}
 72 |                           className="bg-background"
 73 |                         />
 74 |                       </div>
 75 |                       {request.response && (
 76 |                         <div className="mt-2">
 77 |                           <div className="flex justify-between items-center mb-1">
 78 |                             <span className="font-semibold text-green-600">
 79 |                               Response:
 80 |                             </span>
 81 |                           </div>
 82 |                           <JsonView
 83 |                             data={request.response}
 84 |                             className="bg-background"
 85 |                           />
 86 |                         </div>
 87 |                       )}
 88 |                     </>
 89 |                   )}
 90 |                 </li>
 91 |               ))}
 92 |           </ul>
 93 |         )}
 94 |       </div>
 95 |       <div className="flex-1 overflow-y-auto p-4">
 96 |         <h2 className="text-lg font-semibold mb-4">Server Notifications</h2>
 97 |         {serverNotifications.length === 0 ? (
 98 |           <p className="text-sm text-gray-500 dark:text-gray-400 italic">
 99 |             No notifications yet
100 |           </p>
101 |         ) : (
102 |           <ul className="space-y-3">
103 |             {serverNotifications
104 |               .slice()
105 |               .reverse()
106 |               .map((notification, index) => (
107 |                 <li
108 |                   key={index}
109 |                   className="text-sm text-foreground bg-secondary py-2 px-3 rounded"
110 |                 >
111 |                   <div
112 |                     className="flex justify-between items-center cursor-pointer"
113 |                     onClick={() =>
114 |                       toggleNotificationExpansion(
115 |                         serverNotifications.length - 1 - index,
116 |                       )
117 |                     }
118 |                   >
119 |                     <span className="font-mono">
120 |                       {serverNotifications.length - index}.{" "}
121 |                       {notification.method}
122 |                     </span>
123 |                     <span>
124 |                       {expandedNotifications[
125 |                         serverNotifications.length - 1 - index
126 |                       ]
127 |                         ? "▼"
128 |                         : "▶"}
129 |                     </span>
130 |                   </div>
131 |                   {expandedNotifications[
132 |                     serverNotifications.length - 1 - index
133 |                   ] && (
134 |                     <div className="mt-2">
135 |                       <div className="flex justify-between items-center mb-1">
136 |                         <span className="font-semibold text-purple-600">
137 |                           Details:
138 |                         </span>
139 |                       </div>
140 |                       <JsonView
141 |                         data={JSON.stringify(notification, null, 2)}
142 |                         className="bg-background"
143 |                       />
144 |                     </div>
145 |                   )}
146 |                 </li>
147 |               ))}
148 |           </ul>
149 |         )}
150 |       </div>
151 |     </div>
152 |   );
153 | };
154 | 
155 | export default HistoryAndNotifications;
156 | 


--------------------------------------------------------------------------------
/client/src/components/JsonEditor.tsx:
--------------------------------------------------------------------------------
 1 | import { useState, useEffect } from "react";
 2 | import Editor from "react-simple-code-editor";
 3 | import Prism from "prismjs";
 4 | import "prismjs/components/prism-json";
 5 | import "prismjs/themes/prism.css";
 6 | 
 7 | interface JsonEditorProps {
 8 |   value: string;
 9 |   onChange: (value: string) => void;
10 |   error?: string;
11 | }
12 | 
13 | const JsonEditor = ({
14 |   value,
15 |   onChange,
16 |   error: externalError,
17 | }: JsonEditorProps) => {
18 |   const [editorContent, setEditorContent] = useState(value || "");
19 |   const [internalError, setInternalError] = useState<string | undefined>(
20 |     undefined,
21 |   );
22 | 
23 |   useEffect(() => {
24 |     setEditorContent(value || "");
25 |   }, [value]);
26 | 
27 |   const handleEditorChange = (newContent: string) => {
28 |     setEditorContent(newContent);
29 |     setInternalError(undefined);
30 |     onChange(newContent);
31 |   };
32 | 
33 |   const displayError = internalError || externalError;
34 | 
35 |   return (
36 |     <div className="relative">
37 |       <div
38 |         className={`border rounded-md ${
39 |           displayError
40 |             ? "border-red-500"
41 |             : "border-gray-200 dark:border-gray-800"
42 |         }`}
43 |       >
44 |         <Editor
45 |           value={editorContent}
46 |           onValueChange={handleEditorChange}
47 |           highlight={(code) =>
48 |             Prism.highlight(code, Prism.languages.json, "json")
49 |           }
50 |           padding={10}
51 |           style={{
52 |             fontFamily: '"Fira code", "Fira Mono", monospace',
53 |             fontSize: 14,
54 |             backgroundColor: "transparent",
55 |             minHeight: "100px",
56 |           }}
57 |           className="w-full"
58 |         />
59 |       </div>
60 |       {displayError && (
61 |         <p className="text-sm text-red-500 mt-1">{displayError}</p>
62 |       )}
63 |     </div>
64 |   );
65 | };
66 | 
67 | export default JsonEditor;
68 | 


--------------------------------------------------------------------------------
/client/src/components/ListPane.tsx:
--------------------------------------------------------------------------------
 1 | import { Button } from "./ui/button";
 2 | 
 3 | type ListPaneProps<T> = {
 4 |   items: T[];
 5 |   listItems: () => void;
 6 |   clearItems: () => void;
 7 |   setSelectedItem: (item: T) => void;
 8 |   renderItem: (item: T) => React.ReactNode;
 9 |   title: string;
10 |   buttonText: string;
11 |   isButtonDisabled?: boolean;
12 | };
13 | 
14 | const ListPane = <T extends object>({
15 |   items,
16 |   listItems,
17 |   clearItems,
18 |   setSelectedItem,
19 |   renderItem,
20 |   title,
21 |   buttonText,
22 |   isButtonDisabled,
23 | }: ListPaneProps<T>) => (
24 |   <div className="bg-card border border-border rounded-lg shadow">
25 |     <div className="p-4 border-b border-gray-200 dark:border-border">
26 |       <h3 className="font-semibold dark:text-white">{title}</h3>
27 |     </div>
28 |     <div className="p-4">
29 |       <Button
30 |         variant="outline"
31 |         className="w-full mb-4"
32 |         onClick={listItems}
33 |         disabled={isButtonDisabled}
34 |       >
35 |         {buttonText}
36 |       </Button>
37 |       <Button
38 |         variant="outline"
39 |         className="w-full mb-4"
40 |         onClick={clearItems}
41 |         disabled={items.length === 0}
42 |       >
43 |         Clear
44 |       </Button>
45 |       <div className="space-y-2 overflow-y-auto max-h-96">
46 |         {items.map((item, index) => (
47 |           <div
48 |             key={index}
49 |             className="flex items-center py-2 px-4 rounded hover:bg-gray-50 dark:hover:bg-secondary cursor-pointer"
50 |             onClick={() => setSelectedItem(item)}
51 |           >
52 |             {renderItem(item)}
53 |           </div>
54 |         ))}
55 |       </div>
56 |     </div>
57 |   </div>
58 | );
59 | 
60 | export default ListPane;
61 | 


--------------------------------------------------------------------------------
/client/src/components/OAuthCallback.tsx:
--------------------------------------------------------------------------------
 1 | import { useEffect, useRef } from "react";
 2 | import { InspectorOAuthClientProvider } from "../lib/auth";
 3 | import { SESSION_KEYS } from "../lib/constants";
 4 | import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
 5 | import { useToast } from "@/lib/hooks/useToast";
 6 | import {
 7 |   generateOAuthErrorDescription,
 8 |   parseOAuthCallbackParams,
 9 | } from "@/utils/oauthUtils.ts";
10 | 
11 | interface OAuthCallbackProps {
12 |   onConnect: (serverUrl: string) => void;
13 | }
14 | 
15 | const OAuthCallback = ({ onConnect }: OAuthCallbackProps) => {
16 |   const { toast } = useToast();
17 |   const hasProcessedRef = useRef(false);
18 | 
19 |   useEffect(() => {
20 |     const handleCallback = async () => {
21 |       // Skip if we've already processed this callback
22 |       if (hasProcessedRef.current) {
23 |         return;
24 |       }
25 |       hasProcessedRef.current = true;
26 | 
27 |       const notifyError = (description: string) =>
28 |         void toast({
29 |           title: "OAuth Authorization Error",
30 |           description,
31 |           variant: "destructive",
32 |         });
33 | 
34 |       const params = parseOAuthCallbackParams(window.location.search);
35 |       if (!params.successful) {
36 |         return notifyError(generateOAuthErrorDescription(params));
37 |       }
38 | 
39 |       const serverUrl = sessionStorage.getItem(SESSION_KEYS.SERVER_URL);
40 |       if (!serverUrl) {
41 |         return notifyError("Missing Server URL");
42 |       }
43 | 
44 |       let result;
45 |       try {
46 |         // Create an auth provider with the current server URL
47 |         const serverAuthProvider = new InspectorOAuthClientProvider(serverUrl);
48 | 
49 |         result = await auth(serverAuthProvider, {
50 |           serverUrl,
51 |           authorizationCode: params.code,
52 |         });
53 |       } catch (error) {
54 |         console.error("OAuth callback error:", error);
55 |         return notifyError(`Unexpected error occurred: ${error}`);
56 |       }
57 | 
58 |       if (result !== "AUTHORIZED") {
59 |         return notifyError(
60 |           `Expected to be authorized after providing auth code, got: ${result}`,
61 |         );
62 |       }
63 | 
64 |       // Finally, trigger auto-connect
65 |       toast({
66 |         title: "Success",
67 |         description: "Successfully authenticated with OAuth",
68 |         variant: "default",
69 |       });
70 |       onConnect(serverUrl);
71 |     };
72 | 
73 |     handleCallback().finally(() => {
74 |       window.history.replaceState({}, document.title, "/");
75 |     });
76 |   }, [toast, onConnect]);
77 | 
78 |   return (
79 |     <div className="flex items-center justify-center h-screen">
80 |       <p className="text-lg text-gray-500">Processing OAuth callback...</p>
81 |     </div>
82 |   );
83 | };
84 | 
85 | export default OAuthCallback;
86 | 


--------------------------------------------------------------------------------
/client/src/components/OAuthDebugCallback.tsx:
--------------------------------------------------------------------------------
  1 | import { useEffect } from "react";
  2 | import { SESSION_KEYS } from "../lib/constants";
  3 | import {
  4 |   generateOAuthErrorDescription,
  5 |   parseOAuthCallbackParams,
  6 | } from "@/utils/oauthUtils.ts";
  7 | import { AuthDebuggerState } from "@/lib/auth-types";
  8 | 
  9 | interface OAuthCallbackProps {
 10 |   onConnect: ({
 11 |     authorizationCode,
 12 |     errorMsg,
 13 |     restoredState,
 14 |   }: {
 15 |     authorizationCode?: string;
 16 |     errorMsg?: string;
 17 |     restoredState?: AuthDebuggerState;
 18 |   }) => void;
 19 | }
 20 | 
 21 | const OAuthDebugCallback = ({ onConnect }: OAuthCallbackProps) => {
 22 |   useEffect(() => {
 23 |     let isProcessed = false;
 24 | 
 25 |     const handleCallback = async () => {
 26 |       // Skip if we've already processed this callback
 27 |       if (isProcessed) {
 28 |         return;
 29 |       }
 30 |       isProcessed = true;
 31 | 
 32 |       const params = parseOAuthCallbackParams(window.location.search);
 33 |       if (!params.successful) {
 34 |         const errorMsg = generateOAuthErrorDescription(params);
 35 |         onConnect({ errorMsg });
 36 |         return;
 37 |       }
 38 | 
 39 |       const serverUrl = sessionStorage.getItem(SESSION_KEYS.SERVER_URL);
 40 | 
 41 |       // Try to restore the auth state
 42 |       const storedState = sessionStorage.getItem(
 43 |         SESSION_KEYS.AUTH_DEBUGGER_STATE,
 44 |       );
 45 |       let restoredState = null;
 46 |       if (storedState) {
 47 |         try {
 48 |           restoredState = JSON.parse(storedState);
 49 |           // Clean up the stored state
 50 |           sessionStorage.removeItem(SESSION_KEYS.AUTH_DEBUGGER_STATE);
 51 |         } catch (e) {
 52 |           console.error("Failed to parse stored auth state:", e);
 53 |         }
 54 |       }
 55 | 
 56 |       // ServerURL isn't set, this can happen if we've opened the
 57 |       // authentication request in a new tab, so we don't have the same
 58 |       // session storage
 59 |       if (!serverUrl) {
 60 |         // If there's no server URL, we're likely in a new tab
 61 |         // Just display the code for manual copying
 62 |         return;
 63 |       }
 64 | 
 65 |       if (!params.code) {
 66 |         onConnect({ errorMsg: "Missing authorization code" });
 67 |         return;
 68 |       }
 69 | 
 70 |       // Instead of storing in sessionStorage, pass the code directly
 71 |       // to the auth state manager through onConnect, along with restored state
 72 |       onConnect({ authorizationCode: params.code, restoredState });
 73 |     };
 74 | 
 75 |     handleCallback().finally(() => {
 76 |       // Only redirect if we have the URL set, otherwise assume this was
 77 |       // in a new tab
 78 |       if (sessionStorage.getItem(SESSION_KEYS.SERVER_URL)) {
 79 |         window.history.replaceState({}, document.title, "/");
 80 |       }
 81 |     });
 82 | 
 83 |     return () => {
 84 |       isProcessed = true;
 85 |     };
 86 |   }, [onConnect]);
 87 | 
 88 |   const callbackParams = parseOAuthCallbackParams(window.location.search);
 89 | 
 90 |   return (
 91 |     <div className="flex items-center justify-center h-screen">
 92 |       <div className="mt-4 p-4 bg-secondary rounded-md max-w-md">
 93 |         <p className="mb-2 text-sm">
 94 |           Please copy this authorization code and return to the Auth Debugger:
 95 |         </p>
 96 |         <code className="block p-2 bg-muted rounded-sm overflow-x-auto text-xs">
 97 |           {callbackParams.successful && "code" in callbackParams
 98 |             ? callbackParams.code
 99 |             : `No code found: ${callbackParams.error}, ${callbackParams.error_description}`}
100 |         </code>
101 |         <p className="mt-4 text-xs text-muted-foreground">
102 |           Close this tab and paste the code in the OAuth flow to complete
103 |           authentication.
104 |         </p>
105 |       </div>
106 |     </div>
107 |   );
108 | };
109 | 
110 | export default OAuthDebugCallback;
111 | 


--------------------------------------------------------------------------------
/client/src/components/PingTab.tsx:
--------------------------------------------------------------------------------
 1 | import { TabsContent } from "@/components/ui/tabs";
 2 | import { Button } from "@/components/ui/button";
 3 | 
 4 | const PingTab = ({ onPingClick }: { onPingClick: () => void }) => {
 5 |   return (
 6 |     <TabsContent value="ping">
 7 |       <div className="grid grid-cols-2 gap-4">
 8 |         <div className="col-span-2 flex justify-center items-center">
 9 |           <Button
10 |             onClick={onPingClick}
11 |             className="font-bold py-6 px-12 rounded-full"
12 |           >
13 |             Ping Server
14 |           </Button>
15 |         </div>
16 |       </div>
17 |     </TabsContent>
18 |   );
19 | };
20 | 
21 | export default PingTab;
22 | 


--------------------------------------------------------------------------------
/client/src/components/PromptsTab.tsx:
--------------------------------------------------------------------------------
  1 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
  2 | import { Button } from "@/components/ui/button";
  3 | import { Combobox } from "@/components/ui/combobox";
  4 | import { Label } from "@/components/ui/label";
  5 | import { TabsContent } from "@/components/ui/tabs";
  6 | 
  7 | import {
  8 |   ListPromptsResult,
  9 |   PromptReference,
 10 |   ResourceReference,
 11 | } from "@modelcontextprotocol/sdk/types.js";
 12 | import { AlertCircle } from "lucide-react";
 13 | import { useEffect, useState } from "react";
 14 | import ListPane from "./ListPane";
 15 | import { useCompletionState } from "@/lib/hooks/useCompletionState";
 16 | import JsonView from "./JsonView";
 17 | 
 18 | export type Prompt = {
 19 |   name: string;
 20 |   description?: string;
 21 |   arguments?: {
 22 |     name: string;
 23 |     description?: string;
 24 |     required?: boolean;
 25 |   }[];
 26 | };
 27 | 
 28 | const PromptsTab = ({
 29 |   prompts,
 30 |   listPrompts,
 31 |   clearPrompts,
 32 |   getPrompt,
 33 |   selectedPrompt,
 34 |   setSelectedPrompt,
 35 |   handleCompletion,
 36 |   completionsSupported,
 37 |   promptContent,
 38 |   nextCursor,
 39 |   error,
 40 | }: {
 41 |   prompts: Prompt[];
 42 |   listPrompts: () => void;
 43 |   clearPrompts: () => void;
 44 |   getPrompt: (name: string, args: Record<string, string>) => void;
 45 |   selectedPrompt: Prompt | null;
 46 |   setSelectedPrompt: (prompt: Prompt | null) => void;
 47 |   handleCompletion: (
 48 |     ref: PromptReference | ResourceReference,
 49 |     argName: string,
 50 |     value: string,
 51 |   ) => Promise<string[]>;
 52 |   completionsSupported: boolean;
 53 |   promptContent: string;
 54 |   nextCursor: ListPromptsResult["nextCursor"];
 55 |   error: string | null;
 56 | }) => {
 57 |   const [promptArgs, setPromptArgs] = useState<Record<string, string>>({});
 58 |   const { completions, clearCompletions, requestCompletions } =
 59 |     useCompletionState(handleCompletion, completionsSupported);
 60 | 
 61 |   useEffect(() => {
 62 |     clearCompletions();
 63 |   }, [clearCompletions, selectedPrompt]);
 64 | 
 65 |   const handleInputChange = async (argName: string, value: string) => {
 66 |     setPromptArgs((prev) => ({ ...prev, [argName]: value }));
 67 | 
 68 |     if (selectedPrompt) {
 69 |       requestCompletions(
 70 |         {
 71 |           type: "ref/prompt",
 72 |           name: selectedPrompt.name,
 73 |         },
 74 |         argName,
 75 |         value,
 76 |       );
 77 |     }
 78 |   };
 79 | 
 80 |   const handleGetPrompt = () => {
 81 |     if (selectedPrompt) {
 82 |       getPrompt(selectedPrompt.name, promptArgs);
 83 |     }
 84 |   };
 85 | 
 86 |   return (
 87 |     <TabsContent value="prompts">
 88 |       <div className="grid grid-cols-2 gap-4">
 89 |         <ListPane
 90 |           items={prompts}
 91 |           listItems={listPrompts}
 92 |           clearItems={() => {
 93 |             clearPrompts();
 94 |             setSelectedPrompt(null);
 95 |           }}
 96 |           setSelectedItem={(prompt) => {
 97 |             setSelectedPrompt(prompt);
 98 |             setPromptArgs({});
 99 |           }}
100 |           renderItem={(prompt) => (
101 |             <div className="flex flex-col items-start">
102 |               <span className="flex-1">{prompt.name}</span>
103 |               <span className="text-sm text-gray-500 text-left">
104 |                 {prompt.description}
105 |               </span>
106 |             </div>
107 |           )}
108 |           title="Prompts"
109 |           buttonText={nextCursor ? "List More Prompts" : "List Prompts"}
110 |           isButtonDisabled={!nextCursor && prompts.length > 0}
111 |         />
112 | 
113 |         <div className="bg-card border border-border rounded-lg shadow">
114 |           <div className="p-4 border-b border-gray-200 dark:border-border">
115 |             <h3 className="font-semibold">
116 |               {selectedPrompt ? selectedPrompt.name : "Select a prompt"}
117 |             </h3>
118 |           </div>
119 |           <div className="p-4">
120 |             {error ? (
121 |               <Alert variant="destructive">
122 |                 <AlertCircle className="h-4 w-4" />
123 |                 <AlertTitle>Error</AlertTitle>
124 |                 <AlertDescription>{error}</AlertDescription>
125 |               </Alert>
126 |             ) : selectedPrompt ? (
127 |               <div className="space-y-4">
128 |                 {selectedPrompt.description && (
129 |                   <p className="text-sm text-gray-600 dark:text-gray-400">
130 |                     {selectedPrompt.description}
131 |                   </p>
132 |                 )}
133 |                 {selectedPrompt.arguments?.map((arg) => (
134 |                   <div key={arg.name}>
135 |                     <Label htmlFor={arg.name}>{arg.name}</Label>
136 |                     <Combobox
137 |                       id={arg.name}
138 |                       placeholder={`Enter ${arg.name}`}
139 |                       value={promptArgs[arg.name] || ""}
140 |                       onChange={(value) => handleInputChange(arg.name, value)}
141 |                       onInputChange={(value) =>
142 |                         handleInputChange(arg.name, value)
143 |                       }
144 |                       options={completions[arg.name] || []}
145 |                     />
146 | 
147 |                     {arg.description && (
148 |                       <p className="text-xs text-gray-500 mt-1">
149 |                         {arg.description}
150 |                         {arg.required && (
151 |                           <span className="text-xs mt-1 ml-1">(Required)</span>
152 |                         )}
153 |                       </p>
154 |                     )}
155 |                   </div>
156 |                 ))}
157 |                 <Button onClick={handleGetPrompt} className="w-full">
158 |                   Get Prompt
159 |                 </Button>
160 |                 {promptContent && (
161 |                   <JsonView data={promptContent} withCopyButton={false} />
162 |                 )}
163 |               </div>
164 |             ) : (
165 |               <Alert>
166 |                 <AlertDescription>
167 |                   Select a prompt from the list to view and use it
168 |                 </AlertDescription>
169 |               </Alert>
170 |             )}
171 |           </div>
172 |         </div>
173 |       </div>
174 |     </TabsContent>
175 |   );
176 | };
177 | 
178 | export default PromptsTab;
179 | 


--------------------------------------------------------------------------------
/client/src/components/ResourceLinkView.tsx:
--------------------------------------------------------------------------------
  1 | import { useState, useCallback, useMemo, memo } from "react";
  2 | import JsonView from "./JsonView";
  3 | 
  4 | interface ResourceLinkViewProps {
  5 |   uri: string;
  6 |   name?: string;
  7 |   description?: string;
  8 |   mimeType?: string;
  9 |   resourceContent: string;
 10 |   onReadResource?: (uri: string) => void;
 11 | }
 12 | 
 13 | const ResourceLinkView = memo(
 14 |   ({
 15 |     uri,
 16 |     name,
 17 |     description,
 18 |     mimeType,
 19 |     resourceContent,
 20 |     onReadResource,
 21 |   }: ResourceLinkViewProps) => {
 22 |     const [{ expanded, loading }, setState] = useState({
 23 |       expanded: false,
 24 |       loading: false,
 25 |     });
 26 | 
 27 |     const expandedContent = useMemo(
 28 |       () =>
 29 |         expanded && resourceContent ? (
 30 |           <div className="mt-2">
 31 |             <div className="flex justify-between items-center mb-1">
 32 |               <span className="font-semibold text-green-600">Resource:</span>
 33 |             </div>
 34 |             <JsonView data={resourceContent} className="bg-background" />
 35 |           </div>
 36 |         ) : null,
 37 |       [expanded, resourceContent],
 38 |     );
 39 | 
 40 |     const handleClick = useCallback(() => {
 41 |       if (!onReadResource) return;
 42 |       if (!expanded) {
 43 |         setState((prev) => ({ ...prev, expanded: true, loading: true }));
 44 |         onReadResource(uri);
 45 |         setState((prev) => ({ ...prev, loading: false }));
 46 |       } else {
 47 |         setState((prev) => ({ ...prev, expanded: false }));
 48 |       }
 49 |     }, [expanded, onReadResource, uri]);
 50 | 
 51 |     const handleKeyDown = useCallback(
 52 |       (e: React.KeyboardEvent) => {
 53 |         if ((e.key === "Enter" || e.key === " ") && onReadResource) {
 54 |           e.preventDefault();
 55 |           handleClick();
 56 |         }
 57 |       },
 58 |       [handleClick, onReadResource],
 59 |     );
 60 | 
 61 |     return (
 62 |       <div className="text-sm text-foreground bg-secondary py-2 px-3 rounded">
 63 |         <div
 64 |           className="flex justify-between items-center cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1 rounded"
 65 |           onClick={onReadResource ? handleClick : undefined}
 66 |           onKeyDown={onReadResource ? handleKeyDown : undefined}
 67 |           tabIndex={onReadResource ? 0 : -1}
 68 |           role="button"
 69 |           aria-expanded={expanded}
 70 |           aria-label={`${expanded ? "Collapse" : "Expand"} resource ${uri}`}
 71 |         >
 72 |           <div className="flex-1 min-w-0">
 73 |             <div className="flex items-start justify-between gap-2 mb-1">
 74 |               <span className="text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline px-1 py-0.5 break-all font-mono flex-1 min-w-0">
 75 |                 {uri}
 76 |               </span>
 77 |               <div className="flex items-center gap-2 flex-shrink-0">
 78 |                 {mimeType && (
 79 |                   <span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
 80 |                     {mimeType}
 81 |                   </span>
 82 |                 )}
 83 |                 {onReadResource && (
 84 |                   <span className="ml-2 flex-shrink-0" aria-hidden="true">
 85 |                     {loading ? (
 86 |                       <div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
 87 |                     ) : (
 88 |                       <span>{expanded ? "▼" : "▶"}</span>
 89 |                     )}
 90 |                   </span>
 91 |                 )}
 92 |               </div>
 93 |             </div>
 94 |             {name && (
 95 |               <div className="font-semibold text-sm text-gray-900 dark:text-gray-100 mb-1">
 96 |                 {name}
 97 |               </div>
 98 |             )}
 99 |             {description && (
100 |               <p className="text-sm text-gray-600 dark:text-gray-300 leading-relaxed">
101 |                 {description}
102 |               </p>
103 |             )}
104 |           </div>
105 |         </div>
106 |         {expandedContent}
107 |       </div>
108 |     );
109 |   },
110 | );
111 | 
112 | export default ResourceLinkView;
113 | 


--------------------------------------------------------------------------------
/client/src/components/RootsTab.tsx:
--------------------------------------------------------------------------------
 1 | import { Alert, AlertDescription } from "@/components/ui/alert";
 2 | import { Button } from "@/components/ui/button";
 3 | import { Input } from "@/components/ui/input";
 4 | import { TabsContent } from "@/components/ui/tabs";
 5 | import { Root } from "@modelcontextprotocol/sdk/types.js";
 6 | import { Plus, Minus, Save } from "lucide-react";
 7 | 
 8 | const RootsTab = ({
 9 |   roots,
10 |   setRoots,
11 |   onRootsChange,
12 | }: {
13 |   roots: Root[];
14 |   setRoots: React.Dispatch<React.SetStateAction<Root[]>>;
15 |   onRootsChange: () => void;
16 | }) => {
17 |   const addRoot = () => {
18 |     setRoots((currentRoots) => [...currentRoots, { uri: "file://", name: "" }]);
19 |   };
20 | 
21 |   const removeRoot = (index: number) => {
22 |     setRoots((currentRoots) => currentRoots.filter((_, i) => i !== index));
23 |   };
24 | 
25 |   const updateRoot = (index: number, field: keyof Root, value: string) => {
26 |     setRoots((currentRoots) =>
27 |       currentRoots.map((root, i) =>
28 |         i === index ? { ...root, [field]: value } : root,
29 |       ),
30 |     );
31 |   };
32 | 
33 |   const handleSave = () => {
34 |     onRootsChange();
35 |   };
36 | 
37 |   return (
38 |     <TabsContent value="roots">
39 |       <div className="space-y-4">
40 |         <Alert>
41 |           <AlertDescription>
42 |             Configure the root directories that the server can access
43 |           </AlertDescription>
44 |         </Alert>
45 | 
46 |         {roots.map((root, index) => (
47 |           <div key={index} className="flex gap-2 items-center">
48 |             <Input
49 |               placeholder="file:// URI"
50 |               value={root.uri}
51 |               onChange={(e) => updateRoot(index, "uri", e.target.value)}
52 |               className="flex-1"
53 |             />
54 |             <Button
55 |               variant="destructive"
56 |               size="sm"
57 |               onClick={() => removeRoot(index)}
58 |             >
59 |               <Minus className="h-4 w-4" />
60 |             </Button>
61 |           </div>
62 |         ))}
63 | 
64 |         <div className="flex gap-2">
65 |           <Button variant="outline" onClick={addRoot}>
66 |             <Plus className="h-4 w-4 mr-2" />
67 |             Add Root
68 |           </Button>
69 |           <Button onClick={handleSave}>
70 |             <Save className="h-4 w-4 mr-2" />
71 |             Save Changes
72 |           </Button>
73 |         </div>
74 |       </div>
75 |     </TabsContent>
76 |   );
77 | };
78 | 
79 | export default RootsTab;
80 | 


--------------------------------------------------------------------------------
/client/src/components/SamplingRequest.tsx:
--------------------------------------------------------------------------------
  1 | import { Button } from "@/components/ui/button";
  2 | import JsonView from "./JsonView";
  3 | import { useMemo, useState } from "react";
  4 | import {
  5 |   CreateMessageResult,
  6 |   CreateMessageResultSchema,
  7 | } from "@modelcontextprotocol/sdk/types.js";
  8 | import { PendingRequest } from "./SamplingTab";
  9 | import DynamicJsonForm from "./DynamicJsonForm";
 10 | import { useToast } from "@/lib/hooks/useToast";
 11 | import { JsonSchemaType, JsonValue } from "@/utils/jsonUtils";
 12 | 
 13 | export type SamplingRequestProps = {
 14 |   request: PendingRequest;
 15 |   onApprove: (id: number, result: CreateMessageResult) => void;
 16 |   onReject: (id: number) => void;
 17 | };
 18 | 
 19 | const SamplingRequest = ({
 20 |   onApprove,
 21 |   request,
 22 |   onReject,
 23 | }: SamplingRequestProps) => {
 24 |   const { toast } = useToast();
 25 | 
 26 |   const [messageResult, setMessageResult] = useState<JsonValue>({
 27 |     model: "stub-model",
 28 |     stopReason: "endTurn",
 29 |     role: "assistant",
 30 |     content: {
 31 |       type: "text",
 32 |       text: "",
 33 |     },
 34 |   });
 35 | 
 36 |   const contentType = (
 37 |     (messageResult as { [key: string]: JsonValue })?.content as {
 38 |       [key: string]: JsonValue;
 39 |     }
 40 |   )?.type;
 41 | 
 42 |   const schema = useMemo(() => {
 43 |     const s: JsonSchemaType = {
 44 |       type: "object",
 45 |       description: "Message result",
 46 |       properties: {
 47 |         model: {
 48 |           type: "string",
 49 |           default: "stub-model",
 50 |           description: "model name",
 51 |         },
 52 |         stopReason: {
 53 |           type: "string",
 54 |           default: "endTurn",
 55 |           description: "Stop reason",
 56 |         },
 57 |         role: {
 58 |           type: "string",
 59 |           default: "endTurn",
 60 |           description: "Role of the model",
 61 |         },
 62 |         content: {
 63 |           type: "object",
 64 |           properties: {
 65 |             type: {
 66 |               type: "string",
 67 |               default: "text",
 68 |               description: "Type of content",
 69 |             },
 70 |           },
 71 |         },
 72 |       },
 73 |     };
 74 | 
 75 |     if (contentType === "text" && s.properties) {
 76 |       s.properties.content.properties = {
 77 |         ...s.properties.content.properties,
 78 |         text: {
 79 |           type: "string",
 80 |           default: "",
 81 |           description: "text content",
 82 |         },
 83 |       };
 84 |       setMessageResult((prev) => ({
 85 |         ...(prev as { [key: string]: JsonValue }),
 86 |         content: {
 87 |           type: contentType,
 88 |           text: "",
 89 |         },
 90 |       }));
 91 |     } else if (contentType === "image" && s.properties) {
 92 |       s.properties.content.properties = {
 93 |         ...s.properties.content.properties,
 94 |         data: {
 95 |           type: "string",
 96 |           default: "",
 97 |           description: "Base64 encoded image data",
 98 |         },
 99 |         mimeType: {
100 |           type: "string",
101 |           default: "",
102 |           description: "Mime type of the image",
103 |         },
104 |       };
105 |       setMessageResult((prev) => ({
106 |         ...(prev as { [key: string]: JsonValue }),
107 |         content: {
108 |           type: contentType,
109 |           data: "",
110 |           mimeType: "",
111 |         },
112 |       }));
113 |     }
114 | 
115 |     return s;
116 |   }, [contentType]);
117 | 
118 |   const handleApprove = (id: number) => {
119 |     const validationResult = CreateMessageResultSchema.safeParse(messageResult);
120 |     if (!validationResult.success) {
121 |       toast({
122 |         title: "Error",
123 |         description: `There was an error validating the message result: ${validationResult.error.message}`,
124 |         variant: "destructive",
125 |       });
126 |       return;
127 |     }
128 | 
129 |     onApprove(id, validationResult.data);
130 |   };
131 | 
132 |   return (
133 |     <div
134 |       data-testid="sampling-request"
135 |       className="flex gap-4 p-4 border rounded-lg space-y-4"
136 |     >
137 |       <div className="flex-1 bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-2 rounded">
138 |         <JsonView data={JSON.stringify(request.request)} />
139 |       </div>
140 |       <form className="flex-1 space-y-4">
141 |         <div className="space-y-2">
142 |           <DynamicJsonForm
143 |             schema={schema}
144 |             value={messageResult}
145 |             onChange={(newValue: JsonValue) => {
146 |               setMessageResult(newValue);
147 |             }}
148 |           />
149 |         </div>
150 |         <div className="flex space-x-2 mt-1">
151 |           <Button type="button" onClick={() => handleApprove(request.id)}>
152 |             Approve
153 |           </Button>
154 |           <Button
155 |             type="button"
156 |             variant="outline"
157 |             onClick={() => onReject(request.id)}
158 |           >
159 |             Reject
160 |           </Button>
161 |         </div>
162 |       </form>
163 |     </div>
164 |   );
165 | };
166 | 
167 | export default SamplingRequest;
168 | 


--------------------------------------------------------------------------------
/client/src/components/SamplingTab.tsx:
--------------------------------------------------------------------------------
 1 | import { Alert, AlertDescription } from "@/components/ui/alert";
 2 | import { TabsContent } from "@/components/ui/tabs";
 3 | import {
 4 |   CreateMessageRequest,
 5 |   CreateMessageResult,
 6 | } from "@modelcontextprotocol/sdk/types.js";
 7 | import SamplingRequest from "./SamplingRequest";
 8 | 
 9 | export type PendingRequest = {
10 |   id: number;
11 |   request: CreateMessageRequest;
12 | };
13 | 
14 | export type Props = {
15 |   pendingRequests: PendingRequest[];
16 |   onApprove: (id: number, result: CreateMessageResult) => void;
17 |   onReject: (id: number) => void;
18 | };
19 | 
20 | const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => {
21 |   return (
22 |     <TabsContent value="sampling">
23 |       <div className="h-96">
24 |         <Alert>
25 |           <AlertDescription>
26 |             When the server requests LLM sampling, requests will appear here for
27 |             approval.
28 |           </AlertDescription>
29 |         </Alert>
30 |         <div className="mt-4 space-y-4">
31 |           <h3 className="text-lg font-semibold">Recent Requests</h3>
32 |           {pendingRequests.map((request) => (
33 |             <SamplingRequest
34 |               key={request.id}
35 |               request={request}
36 |               onApprove={onApprove}
37 |               onReject={onReject}
38 |             />
39 |           ))}
40 |           {pendingRequests.length === 0 && (
41 |             <p className="text-gray-500">No pending requests</p>
42 |           )}
43 |         </div>
44 |       </div>
45 |     </TabsContent>
46 |   );
47 | };
48 | 
49 | export default SamplingTab;
50 | 


--------------------------------------------------------------------------------
/client/src/components/__tests__/samplingRequest.test.tsx:
--------------------------------------------------------------------------------
 1 | import { render, screen, fireEvent } from "@testing-library/react";
 2 | import SamplingRequest from "../SamplingRequest";
 3 | import { PendingRequest } from "../SamplingTab";
 4 | 
 5 | const mockRequest: PendingRequest = {
 6 |   id: 1,
 7 |   request: {
 8 |     method: "sampling/createMessage",
 9 |     params: {
10 |       messages: [
11 |         {
12 |           role: "user",
13 |           content: {
14 |             type: "text",
15 |             text: "What files are in the current directory?",
16 |           },
17 |         },
18 |       ],
19 |       systemPrompt: "You are a helpful file system assistant.",
20 |       includeContext: "thisServer",
21 |       maxTokens: 100,
22 |     },
23 |   },
24 | };
25 | 
26 | describe("Form to handle sampling response", () => {
27 |   const mockOnApprove = jest.fn();
28 |   const mockOnReject = jest.fn();
29 | 
30 |   afterEach(() => {
31 |     jest.clearAllMocks();
32 |   });
33 | 
34 |   it("should call onApprove with correct text content when Approve button is clicked", () => {
35 |     render(
36 |       <SamplingRequest
37 |         request={mockRequest}
38 |         onApprove={mockOnApprove}
39 |         onReject={mockOnReject}
40 |       />,
41 |     );
42 | 
43 |     // Click the Approve button
44 |     fireEvent.click(screen.getByRole("button", { name: /approve/i }));
45 | 
46 |     // Assert that onApprove is called with the correct arguments
47 |     expect(mockOnApprove).toHaveBeenCalledWith(mockRequest.id, {
48 |       model: "stub-model",
49 |       stopReason: "endTurn",
50 |       role: "assistant",
51 |       content: {
52 |         type: "text",
53 |         text: "",
54 |       },
55 |     });
56 |   });
57 | 
58 |   it("should call onReject with correct request id when Reject button is clicked", () => {
59 |     render(
60 |       <SamplingRequest
61 |         request={mockRequest}
62 |         onApprove={mockOnApprove}
63 |         onReject={mockOnReject}
64 |       />,
65 |     );
66 | 
67 |     // Click the Approve button
68 |     fireEvent.click(screen.getByRole("button", { name: /Reject/i }));
69 | 
70 |     // Assert that onApprove is called with the correct arguments
71 |     expect(mockOnReject).toHaveBeenCalledWith(mockRequest.id);
72 |   });
73 | });
74 | 


--------------------------------------------------------------------------------
/client/src/components/__tests__/samplingTab.test.tsx:
--------------------------------------------------------------------------------
 1 | import { render, screen } from "@testing-library/react";
 2 | import { Tabs } from "@/components/ui/tabs";
 3 | import SamplingTab, { PendingRequest } from "../SamplingTab";
 4 | 
 5 | describe("Sampling tab", () => {
 6 |   const mockOnApprove = jest.fn();
 7 |   const mockOnReject = jest.fn();
 8 | 
 9 |   const renderSamplingTab = (pendingRequests: PendingRequest[]) =>
10 |     render(
11 |       <Tabs defaultValue="sampling">
12 |         <SamplingTab
13 |           pendingRequests={pendingRequests}
14 |           onApprove={mockOnApprove}
15 |           onReject={mockOnReject}
16 |         />
17 |       </Tabs>,
18 |     );
19 | 
20 |   it("should render 'No pending requests' when there are no pending requests", () => {
21 |     renderSamplingTab([]);
22 |     expect(
23 |       screen.getByText(
24 |         "When the server requests LLM sampling, requests will appear here for approval.",
25 |       ),
26 |     ).toBeTruthy();
27 |     expect(screen.findByText("No pending requests")).toBeTruthy();
28 |   });
29 | 
30 |   it("should render the correct number of requests", () => {
31 |     renderSamplingTab(
32 |       Array.from({ length: 5 }, (_, i) => ({
33 |         id: i,
34 |         request: {
35 |           method: "sampling/createMessage",
36 |           params: {
37 |             messages: [
38 |               {
39 |                 role: "user",
40 |                 content: {
41 |                   type: "text",
42 |                   text: "What files are in the current directory?",
43 |                 },
44 |               },
45 |             ],
46 |             systemPrompt: "You are a helpful file system assistant.",
47 |             includeContext: "thisServer",
48 |             maxTokens: 100,
49 |           },
50 |         },
51 |       })),
52 |     );
53 |     expect(screen.getAllByTestId("sampling-request").length).toBe(5);
54 |   });
55 | });
56 | 


--------------------------------------------------------------------------------
/client/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 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
 8 |   {
 9 |     variants: {
10 |       variant: {
11 |         default: "bg-background text-foreground",
12 |         destructive:
13 |           "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14 |       },
15 |     },
16 |     defaultVariants: {
17 |       variant: "default",
18 |     },
19 |   },
20 | );
21 | 
22 | const Alert = React.forwardRef<
23 |   HTMLDivElement,
24 |   React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
25 | >(({ className, variant, ...props }, ref) => (
26 |   <div
27 |     ref={ref}
28 |     role="alert"
29 |     className={cn(alertVariants({ variant }), className)}
30 |     {...props}
31 |   />
32 | ));
33 | Alert.displayName = "Alert";
34 | 
35 | const AlertTitle = React.forwardRef<
36 |   HTMLParagraphElement,
37 |   React.HTMLAttributes<HTMLHeadingElement>
38 | >(({ className, ...props }, ref) => (
39 |   <h5
40 |     ref={ref}
41 |     className={cn("mb-1 font-medium leading-none tracking-tight", className)}
42 |     {...props}
43 |   />
44 | ));
45 | AlertTitle.displayName = "AlertTitle";
46 | 
47 | const AlertDescription = React.forwardRef<
48 |   HTMLParagraphElement,
49 |   React.HTMLAttributes<HTMLParagraphElement>
50 | >(({ className, ...props }, ref) => (
51 |   <div
52 |     ref={ref}
53 |     className={cn("text-sm [&_p]:leading-relaxed", className)}
54 |     {...props}
55 |   />
56 | ));
57 | AlertDescription.displayName = "AlertDescription";
58 | 
59 | export { Alert, AlertTitle, AlertDescription };
60 | 


--------------------------------------------------------------------------------
/client/src/components/ui/button.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 buttonVariants = cva(
 8 |   "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
 9 |   {
10 |     variants: {
11 |       variant: {
12 |         default:
13 |           "bg-primary text-primary-foreground shadow hover:bg-primary/90 dark:bg-gray-800 dark:text-white dark:hover:bg-gray-700",
14 |         destructive:
15 |           "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 |         outline:
17 |           "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 |         secondary:
19 |           "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 |         ghost: "hover:bg-accent hover:text-accent-foreground",
21 |         link: "text-primary underline-offset-4 hover:underline",
22 |       },
23 |       size: {
24 |         default: "h-9 px-4 py-2",
25 |         sm: "h-8 rounded-md px-3 text-xs",
26 |         lg: "h-10 rounded-md px-8",
27 |         icon: "h-9 w-9",
28 |       },
29 |     },
30 |     defaultVariants: {
31 |       variant: "default",
32 |       size: "default",
33 |     },
34 |   },
35 | );
36 | 
37 | export interface ButtonProps
38 |   extends React.ButtonHTMLAttributes<HTMLButtonElement>,
39 |     VariantProps<typeof buttonVariants> {
40 |   asChild?: boolean;
41 | }
42 | 
43 | const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
44 |   ({ className, variant, size, asChild = false, ...props }, ref) => {
45 |     const Comp = asChild ? Slot : "button";
46 |     return (
47 |       <Comp
48 |         className={cn(buttonVariants({ variant, size, className }))}
49 |         ref={ref}
50 |         {...props}
51 |       />
52 |     );
53 |   },
54 | );
55 | Button.displayName = "Button";
56 | 
57 | export { Button };
58 | 


--------------------------------------------------------------------------------
/client/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
 1 | "use client";
 2 | 
 3 | import * as React from "react";
 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
 5 | import { Check } from "lucide-react";
 6 | 
 7 | import { cn } from "@/lib/utils";
 8 | 
 9 | const Checkbox = React.forwardRef<
10 |   React.ElementRef<typeof CheckboxPrimitive.Root>,
11 |   React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
12 | >(({ className, ...props }, ref) => (
13 |   <CheckboxPrimitive.Root
14 |     ref={ref}
15 |     className={cn(
16 |       "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
17 |       className,
18 |     )}
19 |     {...props}
20 |   >
21 |     <CheckboxPrimitive.Indicator
22 |       className={cn("flex items-center justify-center text-current")}
23 |     >
24 |       <Check className="h-4 w-4" />
25 |     </CheckboxPrimitive.Indicator>
26 |   </CheckboxPrimitive.Root>
27 | ));
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName;
29 | 
30 | export { Checkbox };
31 | 


--------------------------------------------------------------------------------
/client/src/components/ui/combobox.tsx:
--------------------------------------------------------------------------------
 1 | import React from "react";
 2 | import { Check, ChevronsUpDown } from "lucide-react";
 3 | import { cn } from "@/lib/utils";
 4 | import { Button } from "@/components/ui/button";
 5 | import {
 6 |   Command,
 7 |   CommandEmpty,
 8 |   CommandGroup,
 9 |   CommandInput,
10 |   CommandItem,
11 | } from "@/components/ui/command";
12 | import {
13 |   Popover,
14 |   PopoverContent,
15 |   PopoverTrigger,
16 | } from "@/components/ui/popover";
17 | 
18 | interface ComboboxProps {
19 |   value: string;
20 |   onChange: (value: string) => void;
21 |   onInputChange: (value: string) => void;
22 |   options: string[];
23 |   placeholder?: string;
24 |   emptyMessage?: string;
25 |   id?: string;
26 | }
27 | 
28 | export function Combobox({
29 |   value,
30 |   onChange,
31 |   onInputChange,
32 |   options = [],
33 |   placeholder = "Select...",
34 |   emptyMessage = "No results found.",
35 |   id,
36 | }: ComboboxProps) {
37 |   const [open, setOpen] = React.useState(false);
38 | 
39 |   const handleSelect = React.useCallback(
40 |     (option: string) => {
41 |       onChange(option);
42 |       setOpen(false);
43 |     },
44 |     [onChange],
45 |   );
46 | 
47 |   const handleInputChange = React.useCallback(
48 |     (value: string) => {
49 |       onInputChange(value);
50 |     },
51 |     [onInputChange],
52 |   );
53 | 
54 |   return (
55 |     <Popover open={open} onOpenChange={setOpen}>
56 |       <PopoverTrigger asChild>
57 |         <Button
58 |           variant="outline"
59 |           role="combobox"
60 |           aria-expanded={open}
61 |           aria-controls={id}
62 |           className="w-full justify-between"
63 |         >
64 |           {value || placeholder}
65 |           <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
66 |         </Button>
67 |       </PopoverTrigger>
68 |       <PopoverContent className="w-full p-0" align="start">
69 |         <Command shouldFilter={false} id={id}>
70 |           <CommandInput
71 |             placeholder={placeholder}
72 |             value={value}
73 |             onValueChange={handleInputChange}
74 |           />
75 |           <CommandEmpty>{emptyMessage}</CommandEmpty>
76 |           <CommandGroup>
77 |             {options.map((option) => (
78 |               <CommandItem
79 |                 key={option}
80 |                 value={option}
81 |                 onSelect={() => handleSelect(option)}
82 |               >
83 |                 <Check
84 |                   className={cn(
85 |                     "mr-2 h-4 w-4",
86 |                     value === option ? "opacity-100" : "opacity-0",
87 |                   )}
88 |                 />
89 |                 {option}
90 |               </CommandItem>
91 |             ))}
92 |           </CommandGroup>
93 |         </Command>
94 |       </PopoverContent>
95 |     </Popover>
96 |   );
97 | }
98 | 


--------------------------------------------------------------------------------
/client/src/components/ui/command.tsx:
--------------------------------------------------------------------------------
  1 | import * as React from "react";
  2 | import { type DialogProps } from "@radix-ui/react-dialog";
  3 | import { Command as CommandPrimitive } from "cmdk";
  4 | import { cn } from "@/lib/utils";
  5 | import { Dialog, DialogContent } from "@/components/ui/dialog";
  6 | import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
  7 | 
  8 | const Command = React.forwardRef<
  9 |   React.ElementRef<typeof CommandPrimitive>,
 10 |   React.ComponentPropsWithoutRef<typeof CommandPrimitive>
 11 | >(({ className, ...props }, ref) => (
 12 |   <CommandPrimitive
 13 |     ref={ref}
 14 |     className={cn(
 15 |       "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
 16 |       className,
 17 |     )}
 18 |     {...props}
 19 |   />
 20 | ));
 21 | Command.displayName = CommandPrimitive.displayName;
 22 | 
 23 | const CommandDialog = ({ children, ...props }: DialogProps) => {
 24 |   return (
 25 |     <Dialog {...props}>
 26 |       <DialogContent className="overflow-hidden p-0">
 27 |         <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
 28 |           {children}
 29 |         </Command>
 30 |       </DialogContent>
 31 |     </Dialog>
 32 |   );
 33 | };
 34 | 
 35 | const CommandInput = React.forwardRef<
 36 |   React.ElementRef<typeof CommandPrimitive.Input>,
 37 |   React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
 38 | >(({ className, ...props }, ref) => (
 39 |   <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
 40 |     <MagnifyingGlassIcon className="mr-2 h-4 w-4 shrink-0 opacity-50" />
 41 |     <CommandPrimitive.Input
 42 |       ref={ref}
 43 |       className={cn(
 44 |         "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
 45 |         className,
 46 |       )}
 47 |       {...props}
 48 |     />
 49 |   </div>
 50 | ));
 51 | 
 52 | CommandInput.displayName = CommandPrimitive.Input.displayName;
 53 | 
 54 | const CommandList = React.forwardRef<
 55 |   React.ElementRef<typeof CommandPrimitive.List>,
 56 |   React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
 57 | >(({ className, ...props }, ref) => (
 58 |   <CommandPrimitive.List
 59 |     ref={ref}
 60 |     className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
 61 |     {...props}
 62 |   />
 63 | ));
 64 | 
 65 | CommandList.displayName = CommandPrimitive.List.displayName;
 66 | 
 67 | const CommandEmpty = React.forwardRef<
 68 |   React.ElementRef<typeof CommandPrimitive.Empty>,
 69 |   React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
 70 | >((props, ref) => (
 71 |   <CommandPrimitive.Empty
 72 |     ref={ref}
 73 |     className="py-6 text-center text-sm"
 74 |     {...props}
 75 |   />
 76 | ));
 77 | 
 78 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
 79 | 
 80 | const CommandGroup = React.forwardRef<
 81 |   React.ElementRef<typeof CommandPrimitive.Group>,
 82 |   React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
 83 | >(({ className, ...props }, ref) => (
 84 |   <CommandPrimitive.Group
 85 |     ref={ref}
 86 |     className={cn(
 87 |       "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
 88 |       className,
 89 |     )}
 90 |     {...props}
 91 |   />
 92 | ));
 93 | 
 94 | CommandGroup.displayName = CommandPrimitive.Group.displayName;
 95 | 
 96 | const CommandSeparator = React.forwardRef<
 97 |   React.ElementRef<typeof CommandPrimitive.Separator>,
 98 |   React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
 99 | >(({ className, ...props }, ref) => (
100 |   <CommandPrimitive.Separator
101 |     ref={ref}
102 |     className={cn("-mx-1 h-px bg-border", className)}
103 |     {...props}
104 |   />
105 | ));
106 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
107 | 
108 | const CommandItem = React.forwardRef<
109 |   React.ElementRef<typeof CommandPrimitive.Item>,
110 |   React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
111 | >(({ className, ...props }, ref) => (
112 |   <CommandPrimitive.Item
113 |     ref={ref}
114 |     className={cn(
115 |       "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
116 |       className,
117 |     )}
118 |     {...props}
119 |   />
120 | ));
121 | 
122 | CommandItem.displayName = CommandPrimitive.Item.displayName;
123 | 
124 | const CommandShortcut = ({
125 |   className,
126 |   ...props
127 | }: React.HTMLAttributes<HTMLSpanElement>) => {
128 |   return (
129 |     <span
130 |       className={cn(
131 |         "ml-auto text-xs tracking-widest text-muted-foreground",
132 |         className,
133 |       )}
134 |       {...props}
135 |     />
136 |   );
137 | };
138 | CommandShortcut.displayName = "CommandShortcut";
139 | 
140 | export {
141 |   Command,
142 |   CommandDialog,
143 |   CommandInput,
144 |   CommandList,
145 |   CommandEmpty,
146 |   CommandGroup,
147 |   CommandItem,
148 |   CommandShortcut,
149 |   CommandSeparator,
150 | };
151 | 


--------------------------------------------------------------------------------
/client/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
  1 | "use client";
  2 | 
  3 | import * as React from "react";
  4 | import * as DialogPrimitive from "@radix-ui/react-dialog";
  5 | import { cn } from "@/lib/utils";
  6 | import { Cross2Icon } from "@radix-ui/react-icons";
  7 | 
  8 | const Dialog = DialogPrimitive.Root;
  9 | 
 10 | const DialogTrigger = DialogPrimitive.Trigger;
 11 | 
 12 | const DialogPortal = DialogPrimitive.Portal;
 13 | 
 14 | const DialogClose = DialogPrimitive.Close;
 15 | 
 16 | const DialogOverlay = React.forwardRef<
 17 |   React.ElementRef<typeof DialogPrimitive.Overlay>,
 18 |   React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
 19 | >(({ className, ...props }, ref) => (
 20 |   <DialogPrimitive.Overlay
 21 |     ref={ref}
 22 |     className={cn(
 23 |       "fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
 24 |       className,
 25 |     )}
 26 |     {...props}
 27 |   />
 28 | ));
 29 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
 30 | 
 31 | const DialogContent = React.forwardRef<
 32 |   React.ElementRef<typeof DialogPrimitive.Content>,
 33 |   React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
 34 | >(({ className, children, ...props }, ref) => (
 35 |   <DialogPortal>
 36 |     <DialogOverlay />
 37 |     <DialogPrimitive.Content
 38 |       ref={ref}
 39 |       className={cn(
 40 |         "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
 41 |         className,
 42 |       )}
 43 |       {...props}
 44 |     >
 45 |       {children}
 46 |       <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
 47 |         <Cross2Icon className="h-4 w-4" />
 48 |         <span className="sr-only">Close</span>
 49 |       </DialogPrimitive.Close>
 50 |     </DialogPrimitive.Content>
 51 |   </DialogPortal>
 52 | ));
 53 | DialogContent.displayName = DialogPrimitive.Content.displayName;
 54 | 
 55 | const DialogHeader = ({
 56 |   className,
 57 |   ...props
 58 | }: React.HTMLAttributes<HTMLDivElement>) => (
 59 |   <div
 60 |     className={cn(
 61 |       "flex flex-col space-y-1.5 text-center sm:text-left",
 62 |       className,
 63 |     )}
 64 |     {...props}
 65 |   />
 66 | );
 67 | DialogHeader.displayName = "DialogHeader";
 68 | 
 69 | const DialogFooter = ({
 70 |   className,
 71 |   ...props
 72 | }: React.HTMLAttributes<HTMLDivElement>) => (
 73 |   <div
 74 |     className={cn(
 75 |       "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
 76 |       className,
 77 |     )}
 78 |     {...props}
 79 |   />
 80 | );
 81 | DialogFooter.displayName = "DialogFooter";
 82 | 
 83 | const DialogTitle = React.forwardRef<
 84 |   React.ElementRef<typeof DialogPrimitive.Title>,
 85 |   React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
 86 | >(({ className, ...props }, ref) => (
 87 |   <DialogPrimitive.Title
 88 |     ref={ref}
 89 |     className={cn(
 90 |       "text-lg font-semibold leading-none tracking-tight",
 91 |       className,
 92 |     )}
 93 |     {...props}
 94 |   />
 95 | ));
 96 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
 97 | 
 98 | const DialogDescription = React.forwardRef<
 99 |   React.ElementRef<typeof DialogPrimitive.Description>,
100 |   React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
101 | >(({ className, ...props }, ref) => (
102 |   <DialogPrimitive.Description
103 |     ref={ref}
104 |     className={cn("text-sm text-muted-foreground", className)}
105 |     {...props}
106 |   />
107 | ));
108 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
109 | 
110 | export {
111 |   Dialog,
112 |   DialogPortal,
113 |   DialogOverlay,
114 |   DialogTrigger,
115 |   DialogClose,
116 |   DialogContent,
117 |   DialogHeader,
118 |   DialogFooter,
119 |   DialogTitle,
120 |   DialogDescription,
121 | };
122 | 


--------------------------------------------------------------------------------
/client/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | 
 3 | import { cn } from "@/lib/utils";
 4 | 
 5 | export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
 6 | 
 7 | const Input = React.forwardRef<HTMLInputElement, InputProps>(
 8 |   ({ className, type, ...props }, ref) => {
 9 |     return (
10 |       <input
11 |         type={type}
12 |         className={cn(
13 |           "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
14 |           className,
15 |         )}
16 |         ref={ref}
17 |         {...props}
18 |       />
19 |     );
20 |   },
21 | );
22 | Input.displayName = "Input";
23 | 
24 | export { Input };
25 | 


--------------------------------------------------------------------------------
/client/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | import * as LabelPrimitive from "@radix-ui/react-label";
 3 | import { cva, type VariantProps } from "class-variance-authority";
 4 | 
 5 | import { cn } from "@/lib/utils";
 6 | 
 7 | const labelVariants = cva(
 8 |   "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
 9 | );
10 | 
11 | const Label = React.forwardRef<
12 |   React.ElementRef<typeof LabelPrimitive.Root>,
13 |   React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
14 |     VariantProps<typeof labelVariants>
15 | >(({ className, ...props }, ref) => (
16 |   <LabelPrimitive.Root
17 |     ref={ref}
18 |     className={cn(labelVariants(), className)}
19 |     {...props}
20 |   />
21 | ));
22 | Label.displayName = LabelPrimitive.Root.displayName;
23 | 
24 | export { Label };
25 | 


--------------------------------------------------------------------------------
/client/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | import * as PopoverPrimitive from "@radix-ui/react-popover";
 3 | 
 4 | import { cn } from "@/lib/utils";
 5 | 
 6 | const Popover = PopoverPrimitive.Root;
 7 | 
 8 | const PopoverTrigger = PopoverPrimitive.Trigger;
 9 | 
10 | const PopoverAnchor = PopoverPrimitive.Anchor;
11 | 
12 | const PopoverContent = React.forwardRef<
13 |   React.ElementRef<typeof PopoverPrimitive.Content>,
14 |   React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |   <PopoverPrimitive.Portal>
17 |     <PopoverPrimitive.Content
18 |       ref={ref}
19 |       align={align}
20 |       sideOffset={sideOffset}
21 |       className={cn(
22 |         "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
23 |         className,
24 |       )}
25 |       {...props}
26 |     />
27 |   </PopoverPrimitive.Portal>
28 | ));
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
30 | 
31 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
32 | 


--------------------------------------------------------------------------------
/client/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
  1 | import * as React from "react";
  2 | import {
  3 |   CaretSortIcon,
  4 |   CheckIcon,
  5 |   ChevronDownIcon,
  6 |   ChevronUpIcon,
  7 | } from "@radix-ui/react-icons";
  8 | import * as SelectPrimitive from "@radix-ui/react-select";
  9 | 
 10 | import { cn } from "@/lib/utils";
 11 | 
 12 | const Select = SelectPrimitive.Root;
 13 | 
 14 | const SelectGroup = SelectPrimitive.Group;
 15 | 
 16 | const SelectValue = SelectPrimitive.Value;
 17 | 
 18 | const SelectTrigger = React.forwardRef<
 19 |   React.ElementRef<typeof SelectPrimitive.Trigger>,
 20 |   React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
 21 | >(({ className, children, ...props }, ref) => (
 22 |   <SelectPrimitive.Trigger
 23 |     ref={ref}
 24 |     className={cn(
 25 |       "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 hover:border-[#646cff] hover:border-1",
 26 |       className,
 27 |     )}
 28 |     {...props}
 29 |   >
 30 |     {children}
 31 |     <SelectPrimitive.Icon asChild>
 32 |       <CaretSortIcon className="h-4 w-4 opacity-50" />
 33 |     </SelectPrimitive.Icon>
 34 |   </SelectPrimitive.Trigger>
 35 | ));
 36 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
 37 | 
 38 | const SelectScrollUpButton = React.forwardRef<
 39 |   React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
 40 |   React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
 41 | >(({ className, ...props }, ref) => (
 42 |   <SelectPrimitive.ScrollUpButton
 43 |     ref={ref}
 44 |     className={cn(
 45 |       "flex cursor-default items-center justify-center py-1",
 46 |       className,
 47 |     )}
 48 |     {...props}
 49 |   >
 50 |     <ChevronUpIcon />
 51 |   </SelectPrimitive.ScrollUpButton>
 52 | ));
 53 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
 54 | 
 55 | const SelectScrollDownButton = React.forwardRef<
 56 |   React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
 57 |   React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
 58 | >(({ className, ...props }, ref) => (
 59 |   <SelectPrimitive.ScrollDownButton
 60 |     ref={ref}
 61 |     className={cn(
 62 |       "flex cursor-default items-center justify-center py-1",
 63 |       className,
 64 |     )}
 65 |     {...props}
 66 |   >
 67 |     <ChevronDownIcon />
 68 |   </SelectPrimitive.ScrollDownButton>
 69 | ));
 70 | SelectScrollDownButton.displayName =
 71 |   SelectPrimitive.ScrollDownButton.displayName;
 72 | 
 73 | const SelectContent = React.forwardRef<
 74 |   React.ElementRef<typeof SelectPrimitive.Content>,
 75 |   React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
 76 | >(({ className, children, position = "popper", ...props }, ref) => (
 77 |   <SelectPrimitive.Portal>
 78 |     <SelectPrimitive.Content
 79 |       ref={ref}
 80 |       className={cn(
 81 |         "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
 82 |         position === "popper" &&
 83 |           "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
 84 |         className,
 85 |       )}
 86 |       position={position}
 87 |       {...props}
 88 |     >
 89 |       <SelectScrollUpButton />
 90 |       <SelectPrimitive.Viewport
 91 |         className={cn(
 92 |           "p-1",
 93 |           position === "popper" &&
 94 |             "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
 95 |         )}
 96 |       >
 97 |         {children}
 98 |       </SelectPrimitive.Viewport>
 99 |       <SelectScrollDownButton />
100 |     </SelectPrimitive.Content>
101 |   </SelectPrimitive.Portal>
102 | ));
103 | SelectContent.displayName = SelectPrimitive.Content.displayName;
104 | 
105 | const SelectLabel = React.forwardRef<
106 |   React.ElementRef<typeof SelectPrimitive.Label>,
107 |   React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
108 | >(({ className, ...props }, ref) => (
109 |   <SelectPrimitive.Label
110 |     ref={ref}
111 |     className={cn("px-2 py-1.5 text-sm font-semibold", className)}
112 |     {...props}
113 |   />
114 | ));
115 | SelectLabel.displayName = SelectPrimitive.Label.displayName;
116 | 
117 | const SelectItem = React.forwardRef<
118 |   React.ElementRef<typeof SelectPrimitive.Item>,
119 |   React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
120 | >(({ className, children, ...props }, ref) => (
121 |   <SelectPrimitive.Item
122 |     ref={ref}
123 |     className={cn(
124 |       "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
125 |       className,
126 |     )}
127 |     {...props}
128 |   >
129 |     <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
130 |       <SelectPrimitive.ItemIndicator>
131 |         <CheckIcon className="h-4 w-4" />
132 |       </SelectPrimitive.ItemIndicator>
133 |     </span>
134 |     <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
135 |   </SelectPrimitive.Item>
136 | ));
137 | SelectItem.displayName = SelectPrimitive.Item.displayName;
138 | 
139 | const SelectSeparator = React.forwardRef<
140 |   React.ElementRef<typeof SelectPrimitive.Separator>,
141 |   React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
142 | >(({ className, ...props }, ref) => (
143 |   <SelectPrimitive.Separator
144 |     ref={ref}
145 |     className={cn("-mx-1 my-1 h-px bg-muted", className)}
146 |     {...props}
147 |   />
148 | ));
149 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
150 | 
151 | export {
152 |   Select,
153 |   SelectGroup,
154 |   SelectValue,
155 |   SelectTrigger,
156 |   SelectContent,
157 |   SelectLabel,
158 |   SelectItem,
159 |   SelectSeparator,
160 |   SelectScrollUpButton,
161 |   SelectScrollDownButton,
162 | };
163 | 


--------------------------------------------------------------------------------
/client/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | import * as TabsPrimitive from "@radix-ui/react-tabs";
 3 | 
 4 | import { cn } from "@/lib/utils";
 5 | 
 6 | const Tabs = TabsPrimitive.Root;
 7 | 
 8 | const TabsList = React.forwardRef<
 9 |   React.ElementRef<typeof TabsPrimitive.List>,
10 |   React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
11 | >(({ className, ...props }, ref) => (
12 |   <TabsPrimitive.List
13 |     ref={ref}
14 |     className={cn(
15 |       "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
16 |       className,
17 |     )}
18 |     {...props}
19 |   />
20 | ));
21 | TabsList.displayName = TabsPrimitive.List.displayName;
22 | 
23 | const TabsTrigger = React.forwardRef<
24 |   React.ElementRef<typeof TabsPrimitive.Trigger>,
25 |   React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
26 | >(({ className, ...props }, ref) => (
27 |   <TabsPrimitive.Trigger
28 |     ref={ref}
29 |     className={cn(
30 |       "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-muted data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
31 |       className,
32 |     )}
33 |     {...props}
34 |   />
35 | ));
36 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
37 | 
38 | const TabsContent = React.forwardRef<
39 |   React.ElementRef<typeof TabsPrimitive.Content>,
40 |   React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
41 | >(({ className, ...props }, ref) => (
42 |   <TabsPrimitive.Content
43 |     ref={ref}
44 |     className={cn(
45 |       "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
46 |       className,
47 |     )}
48 |     {...props}
49 |   />
50 | ));
51 | TabsContent.displayName = TabsPrimitive.Content.displayName;
52 | 
53 | export { Tabs, TabsList, TabsTrigger, TabsContent };
54 | 


--------------------------------------------------------------------------------
/client/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | 
 3 | import { cn } from "@/lib/utils";
 4 | 
 5 | export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
 6 | 
 7 | const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
 8 |   ({ className, ...props }, ref) => {
 9 |     return (
10 |       <textarea
11 |         className={cn(
12 |           "flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
13 |           className,
14 |         )}
15 |         ref={ref}
16 |         {...props}
17 |       />
18 |     );
19 |   },
20 | );
21 | Textarea.displayName = "Textarea";
22 | 
23 | export { Textarea };
24 | 


--------------------------------------------------------------------------------
/client/src/components/ui/toast.tsx:
--------------------------------------------------------------------------------
  1 | import * as React from "react";
  2 | import * as ToastPrimitives from "@radix-ui/react-toast";
  3 | import { cva, type VariantProps } from "class-variance-authority";
  4 | import { cn } from "@/lib/utils";
  5 | import { Cross2Icon } from "@radix-ui/react-icons";
  6 | 
  7 | const ToastProvider = ToastPrimitives.Provider;
  8 | 
  9 | const ToastViewport = React.forwardRef<
 10 |   React.ElementRef<typeof ToastPrimitives.Viewport>,
 11 |   React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
 12 | >(({ className, ...props }, ref) => (
 13 |   <ToastPrimitives.Viewport
 14 |     ref={ref}
 15 |     className={cn(
 16 |       "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
 17 |       className,
 18 |     )}
 19 |     {...props}
 20 |   />
 21 | ));
 22 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
 23 | 
 24 | const toastVariants = cva(
 25 |   "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
 26 |   {
 27 |     variants: {
 28 |       variant: {
 29 |         default: "border bg-background text-foreground",
 30 |         destructive:
 31 |           "destructive group border-destructive bg-destructive text-destructive-foreground",
 32 |       },
 33 |     },
 34 |     defaultVariants: {
 35 |       variant: "default",
 36 |     },
 37 |   },
 38 | );
 39 | 
 40 | const Toast = React.forwardRef<
 41 |   React.ElementRef<typeof ToastPrimitives.Root>,
 42 |   React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
 43 |     VariantProps<typeof toastVariants>
 44 | >(({ className, variant, ...props }, ref) => {
 45 |   return (
 46 |     <ToastPrimitives.Root
 47 |       ref={ref}
 48 |       className={cn(toastVariants({ variant }), className)}
 49 |       {...props}
 50 |     />
 51 |   );
 52 | });
 53 | Toast.displayName = ToastPrimitives.Root.displayName;
 54 | 
 55 | const ToastAction = React.forwardRef<
 56 |   React.ElementRef<typeof ToastPrimitives.Action>,
 57 |   React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
 58 | >(({ className, ...props }, ref) => (
 59 |   <ToastPrimitives.Action
 60 |     ref={ref}
 61 |     className={cn(
 62 |       "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
 63 |       className,
 64 |     )}
 65 |     {...props}
 66 |   />
 67 | ));
 68 | ToastAction.displayName = ToastPrimitives.Action.displayName;
 69 | 
 70 | const ToastClose = React.forwardRef<
 71 |   React.ElementRef<typeof ToastPrimitives.Close>,
 72 |   React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
 73 | >(({ className, ...props }, ref) => (
 74 |   <ToastPrimitives.Close
 75 |     ref={ref}
 76 |     className={cn(
 77 |       "absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
 78 |       className,
 79 |     )}
 80 |     toast-close=""
 81 |     {...props}
 82 |   >
 83 |     <Cross2Icon className="h-4 w-4" />
 84 |   </ToastPrimitives.Close>
 85 | ));
 86 | ToastClose.displayName = ToastPrimitives.Close.displayName;
 87 | 
 88 | const ToastTitle = React.forwardRef<
 89 |   React.ElementRef<typeof ToastPrimitives.Title>,
 90 |   React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
 91 | >(({ className, ...props }, ref) => (
 92 |   <ToastPrimitives.Title
 93 |     ref={ref}
 94 |     className={cn("text-sm font-semibold [&+div]:text-xs", className)}
 95 |     {...props}
 96 |   />
 97 | ));
 98 | ToastTitle.displayName = ToastPrimitives.Title.displayName;
 99 | 
100 | const ToastDescription = React.forwardRef<
101 |   React.ElementRef<typeof ToastPrimitives.Description>,
102 |   React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
103 | >(({ className, ...props }, ref) => (
104 |   <ToastPrimitives.Description
105 |     ref={ref}
106 |     className={cn("text-sm opacity-90", className)}
107 |     {...props}
108 |   />
109 | ));
110 | ToastDescription.displayName = ToastPrimitives.Description.displayName;
111 | 
112 | type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
113 | 
114 | type ToastActionElement = React.ReactElement<typeof ToastAction>;
115 | 
116 | export {
117 |   type ToastProps,
118 |   type ToastActionElement,
119 |   ToastProvider,
120 |   ToastViewport,
121 |   Toast,
122 |   ToastTitle,
123 |   ToastDescription,
124 |   ToastClose,
125 |   ToastAction,
126 | };
127 | 


--------------------------------------------------------------------------------
/client/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
 1 | import { useToast } from "@/lib/hooks/useToast";
 2 | import {
 3 |   Toast,
 4 |   ToastClose,
 5 |   ToastDescription,
 6 |   ToastProvider,
 7 |   ToastTitle,
 8 |   ToastViewport,
 9 | } from "@/components/ui/toast";
10 | 
11 | export function Toaster() {
12 |   const { toasts } = useToast();
13 | 
14 |   return (
15 |     <ToastProvider>
16 |       {toasts.map(function ({ id, title, description, action, ...props }) {
17 |         return (
18 |           <Toast key={id} {...props}>
19 |             <div className="grid gap-1">
20 |               {title && <ToastTitle>{title}</ToastTitle>}
21 |               {description && (
22 |                 <ToastDescription>{description}</ToastDescription>
23 |               )}
24 |             </div>
25 |             {action}
26 |             <ToastClose />
27 |           </Toast>
28 |         );
29 |       })}
30 |       <ToastViewport />
31 |     </ToastProvider>
32 |   );
33 | }
34 | 


--------------------------------------------------------------------------------
/client/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
 1 | "use client";
 2 | 
 3 | import * as React from "react";
 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
 5 | 
 6 | import { cn } from "@/lib/utils";
 7 | 
 8 | const TooltipProvider = TooltipPrimitive.Provider;
 9 | 
10 | const Tooltip = TooltipPrimitive.Root;
11 | 
12 | const TooltipTrigger = TooltipPrimitive.Trigger;
13 | 
14 | const TooltipContent = React.forwardRef<
15 |   React.ElementRef<typeof TooltipPrimitive.Content>,
16 |   React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |   <TooltipPrimitive.Content
19 |     ref={ref}
20 |     sideOffset={sideOffset}
21 |     className={cn(
22 |       "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
23 |       className,
24 |     )}
25 |     {...props}
26 |   />
27 | ));
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29 | 
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
31 | 


--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
  1 | @tailwind base;
  2 | @tailwind components;
  3 | @tailwind utilities;
  4 | 
  5 | :root {
  6 |   font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  7 |   line-height: 1.5;
  8 |   font-weight: 400;
  9 | 
 10 |   color-scheme: light dark;
 11 |   color: rgba(255, 255, 255, 0.87);
 12 |   background-color: #242424;
 13 | 
 14 |   font-synthesis: none;
 15 |   text-rendering: optimizeLegibility;
 16 |   -webkit-font-smoothing: antialiased;
 17 |   -moz-osx-font-smoothing: grayscale;
 18 | }
 19 | 
 20 | a {
 21 |   font-weight: 500;
 22 |   color: #646cff;
 23 |   text-decoration: inherit;
 24 | }
 25 | a:hover {
 26 |   color: #535bf2;
 27 | }
 28 | 
 29 | body {
 30 |   margin: 0;
 31 |   place-items: center;
 32 |   min-width: 320px;
 33 |   min-height: 100vh;
 34 | }
 35 | 
 36 | h1 {
 37 |   font-size: 3.2em;
 38 |   line-height: 1.1;
 39 | }
 40 | 
 41 | @media (prefers-color-scheme: light) {
 42 |   :root {
 43 |     color: #213547;
 44 |     background-color: #ffffff;
 45 |   }
 46 |   a:hover {
 47 |     color: #747bff;
 48 |   }
 49 | }
 50 | 
 51 | @layer base {
 52 |   :root {
 53 |     --background: 0 0% 100%;
 54 |     --foreground: 222.2 84% 4.9%;
 55 |     --card: 0 0% 100%;
 56 |     --card-foreground: 222.2 84% 4.9%;
 57 |     --popover: 0 0% 100%;
 58 |     --popover-foreground: 222.2 84% 4.9%;
 59 |     --primary: 222.2 47.4% 11.2%;
 60 |     --primary-foreground: 210 40% 98%;
 61 |     --secondary: 210 40% 96.1%;
 62 |     --secondary-foreground: 222.2 47.4% 11.2%;
 63 |     --muted: 210 40% 96.1%;
 64 |     --muted-foreground: 215.4 16.3% 46.9%;
 65 |     --accent: 210 40% 96.1%;
 66 |     --accent-foreground: 222.2 47.4% 11.2%;
 67 |     --destructive: 0 84.2% 60.2%;
 68 |     --destructive-foreground: 210 40% 98%;
 69 |     --border: 214.3 31.8% 91.4%;
 70 |     --input: 214.3 31.8% 91.4%;
 71 |     --ring: 222.2 84% 4.9%;
 72 |     --chart-1: 12 76% 61%;
 73 |     --chart-2: 173 58% 39%;
 74 |     --chart-3: 197 37% 24%;
 75 |     --chart-4: 43 74% 66%;
 76 |     --chart-5: 27 87% 67%;
 77 |     --radius: 0.5rem;
 78 |   }
 79 |   .dark {
 80 |     --background: 222.2 84% 4.9%;
 81 |     --foreground: 210 40% 98%;
 82 |     --card: 222.2 84% 4.9%;
 83 |     --card-foreground: 210 40% 98%;
 84 |     --popover: 222.2 84% 4.9%;
 85 |     --popover-foreground: 210 40% 98%;
 86 |     --primary: 210 40% 98%;
 87 |     --primary-foreground: 222.2 47.4% 11.2%;
 88 |     --secondary: 217.2 32.6% 17.5%;
 89 |     --secondary-foreground: 210 40% 98%;
 90 |     --muted: 217.2 32.6% 17.5%;
 91 |     --muted-foreground: 215 20.2% 65.1%;
 92 |     --accent: 217.2 32.6% 17.5%;
 93 |     --accent-foreground: 210 40% 98%;
 94 |     --destructive: 0 62.8% 30.6%;
 95 |     --destructive-foreground: 210 40% 98%;
 96 |     --border: 217.2 24% 24%;
 97 |     --input: 217.2 24% 24%;
 98 |     --ring: 212.7 26.8% 83.9%;
 99 |     --chart-1: 220 70% 50%;
100 |     --chart-2: 160 60% 45%;
101 |     --chart-3: 30 80% 55%;
102 |     --chart-4: 280 65% 60%;
103 |     --chart-5: 340 75% 55%;
104 |   }
105 | }
106 | 
107 | @layer base {
108 |   * {
109 |     @apply border-border;
110 |   }
111 |   body {
112 |     @apply bg-background text-foreground;
113 |   }
114 | }
115 | 


--------------------------------------------------------------------------------
/client/src/lib/auth-types.ts:
--------------------------------------------------------------------------------
 1 | import {
 2 |   OAuthMetadata,
 3 |   OAuthClientInformationFull,
 4 |   OAuthClientInformation,
 5 |   OAuthTokens,
 6 |   OAuthProtectedResourceMetadata,
 7 | } from "@modelcontextprotocol/sdk/shared/auth.js";
 8 | 
 9 | // OAuth flow steps
10 | export type OAuthStep =
11 |   | "metadata_discovery"
12 |   | "client_registration"
13 |   | "authorization_redirect"
14 |   | "authorization_code"
15 |   | "token_request"
16 |   | "complete";
17 | 
18 | // Message types for inline feedback
19 | export type MessageType = "success" | "error" | "info";
20 | 
21 | export interface StatusMessage {
22 |   type: MessageType;
23 |   message: string;
24 | }
25 | 
26 | // Single state interface for OAuth state
27 | export interface AuthDebuggerState {
28 |   isInitiatingAuth: boolean;
29 |   oauthTokens: OAuthTokens | null;
30 |   oauthStep: OAuthStep;
31 |   resourceMetadata: OAuthProtectedResourceMetadata | null;
32 |   resourceMetadataError: Error | null;
33 |   resource: URL | null;
34 |   authServerUrl: URL | null;
35 |   oauthMetadata: OAuthMetadata | null;
36 |   oauthClientInfo: OAuthClientInformationFull | OAuthClientInformation | null;
37 |   authorizationUrl: string | null;
38 |   authorizationCode: string;
39 |   latestError: Error | null;
40 |   statusMessage: StatusMessage | null;
41 |   validationError: string | null;
42 | }
43 | 
44 | export const EMPTY_DEBUGGER_STATE: AuthDebuggerState = {
45 |   isInitiatingAuth: false,
46 |   oauthTokens: null,
47 |   oauthStep: "metadata_discovery",
48 |   oauthMetadata: null,
49 |   resourceMetadata: null,
50 |   resourceMetadataError: null,
51 |   resource: null,
52 |   authServerUrl: null,
53 |   oauthClientInfo: null,
54 |   authorizationUrl: null,
55 |   authorizationCode: "",
56 |   latestError: null,
57 |   statusMessage: null,
58 |   validationError: null,
59 | };
60 | 


--------------------------------------------------------------------------------
/client/src/lib/auth.ts:
--------------------------------------------------------------------------------
  1 | import { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
  2 | import {
  3 |   OAuthClientInformationSchema,
  4 |   OAuthClientInformation,
  5 |   OAuthTokens,
  6 |   OAuthTokensSchema,
  7 |   OAuthClientMetadata,
  8 |   OAuthMetadata,
  9 | } from "@modelcontextprotocol/sdk/shared/auth.js";
 10 | import { SESSION_KEYS, getServerSpecificKey } from "./constants";
 11 | 
 12 | export class InspectorOAuthClientProvider implements OAuthClientProvider {
 13 |   constructor(public serverUrl: string) {
 14 |     // Save the server URL to session storage
 15 |     sessionStorage.setItem(SESSION_KEYS.SERVER_URL, serverUrl);
 16 |   }
 17 | 
 18 |   get redirectUrl() {
 19 |     return window.location.origin + "/oauth/callback";
 20 |   }
 21 | 
 22 |   get clientMetadata(): OAuthClientMetadata {
 23 |     return {
 24 |       redirect_uris: [this.redirectUrl],
 25 |       token_endpoint_auth_method: "none",
 26 |       grant_types: ["authorization_code", "refresh_token"],
 27 |       response_types: ["code"],
 28 |       client_name: "MCP Inspector",
 29 |       client_uri: "https://github.com/modelcontextprotocol/inspector",
 30 |     };
 31 |   }
 32 | 
 33 |   async clientInformation() {
 34 |     const key = getServerSpecificKey(
 35 |       SESSION_KEYS.CLIENT_INFORMATION,
 36 |       this.serverUrl,
 37 |     );
 38 |     const value = sessionStorage.getItem(key);
 39 |     if (!value) {
 40 |       return undefined;
 41 |     }
 42 | 
 43 |     return await OAuthClientInformationSchema.parseAsync(JSON.parse(value));
 44 |   }
 45 | 
 46 |   saveClientInformation(clientInformation: OAuthClientInformation) {
 47 |     const key = getServerSpecificKey(
 48 |       SESSION_KEYS.CLIENT_INFORMATION,
 49 |       this.serverUrl,
 50 |     );
 51 |     sessionStorage.setItem(key, JSON.stringify(clientInformation));
 52 |   }
 53 | 
 54 |   async tokens() {
 55 |     const key = getServerSpecificKey(SESSION_KEYS.TOKENS, this.serverUrl);
 56 |     const tokens = sessionStorage.getItem(key);
 57 |     if (!tokens) {
 58 |       return undefined;
 59 |     }
 60 | 
 61 |     return await OAuthTokensSchema.parseAsync(JSON.parse(tokens));
 62 |   }
 63 | 
 64 |   saveTokens(tokens: OAuthTokens) {
 65 |     const key = getServerSpecificKey(SESSION_KEYS.TOKENS, this.serverUrl);
 66 |     sessionStorage.setItem(key, JSON.stringify(tokens));
 67 |   }
 68 | 
 69 |   redirectToAuthorization(authorizationUrl: URL) {
 70 |     window.location.href = authorizationUrl.href;
 71 |   }
 72 | 
 73 |   saveCodeVerifier(codeVerifier: string) {
 74 |     const key = getServerSpecificKey(
 75 |       SESSION_KEYS.CODE_VERIFIER,
 76 |       this.serverUrl,
 77 |     );
 78 |     sessionStorage.setItem(key, codeVerifier);
 79 |   }
 80 | 
 81 |   codeVerifier() {
 82 |     const key = getServerSpecificKey(
 83 |       SESSION_KEYS.CODE_VERIFIER,
 84 |       this.serverUrl,
 85 |     );
 86 |     const verifier = sessionStorage.getItem(key);
 87 |     if (!verifier) {
 88 |       throw new Error("No code verifier saved for session");
 89 |     }
 90 | 
 91 |     return verifier;
 92 |   }
 93 | 
 94 |   clear() {
 95 |     sessionStorage.removeItem(
 96 |       getServerSpecificKey(SESSION_KEYS.CLIENT_INFORMATION, this.serverUrl),
 97 |     );
 98 |     sessionStorage.removeItem(
 99 |       getServerSpecificKey(SESSION_KEYS.TOKENS, this.serverUrl),
100 |     );
101 |     sessionStorage.removeItem(
102 |       getServerSpecificKey(SESSION_KEYS.CODE_VERIFIER, this.serverUrl),
103 |     );
104 |   }
105 | }
106 | 
107 | // Overrides debug URL and allows saving server OAuth metadata to
108 | // display in debug UI.
109 | export class DebugInspectorOAuthClientProvider extends InspectorOAuthClientProvider {
110 |   get redirectUrl(): string {
111 |     return `${window.location.origin}/oauth/callback/debug`;
112 |   }
113 | 
114 |   saveServerMetadata(metadata: OAuthMetadata) {
115 |     const key = getServerSpecificKey(
116 |       SESSION_KEYS.SERVER_METADATA,
117 |       this.serverUrl,
118 |     );
119 |     sessionStorage.setItem(key, JSON.stringify(metadata));
120 |   }
121 | 
122 |   getServerMetadata(): OAuthMetadata | null {
123 |     const key = getServerSpecificKey(
124 |       SESSION_KEYS.SERVER_METADATA,
125 |       this.serverUrl,
126 |     );
127 |     const metadata = sessionStorage.getItem(key);
128 |     if (!metadata) {
129 |       return null;
130 |     }
131 |     return JSON.parse(metadata);
132 |   }
133 | 
134 |   clear() {
135 |     super.clear();
136 |     sessionStorage.removeItem(
137 |       getServerSpecificKey(SESSION_KEYS.SERVER_METADATA, this.serverUrl),
138 |     );
139 |   }
140 | }
141 | 


--------------------------------------------------------------------------------
/client/src/lib/configurationTypes.ts:
--------------------------------------------------------------------------------
 1 | export type ConfigItem = {
 2 |   label: string;
 3 |   description: string;
 4 |   value: string | number | boolean;
 5 |   is_session_item: boolean;
 6 | };
 7 | 
 8 | /**
 9 |  * Configuration interface for the MCP Inspector, including settings for the MCP Client,
10 |  * Proxy Server, and Inspector UI/UX.
11 |  *
12 |  * Note: Configuration related to which MCP Server to use or any other MCP Server
13 |  * specific settings are outside the scope of this interface as of now.
14 |  */
15 | export type InspectorConfig = {
16 |   /**
17 |    * Maximum time in milliseconds to wait for a response from the MCP server before timing out.
18 |    */
19 |   MCP_SERVER_REQUEST_TIMEOUT: ConfigItem;
20 | 
21 |   /**
22 |    * Whether to reset the timeout on progress notifications. Useful for long-running operations that send periodic progress updates.
23 |    * Refer: https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/utilities/progress/#progress-flow
24 |    */
25 |   MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS: ConfigItem;
26 | 
27 |   /**
28 |    * Maximum total time in milliseconds to wait for a response from the MCP server before timing out. Used in conjunction with MCP_SERVER_REQUEST_TIMEOUT_RESET_ON_PROGRESS.
29 |    * Refer: https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/utilities/progress/#progress-flow
30 |    */
31 |   MCP_REQUEST_MAX_TOTAL_TIMEOUT: ConfigItem;
32 | 
33 |   /**
34 |    * The full address of the MCP Proxy Server, in case it is running on a non-default address. Example: http://10.1.1.22:5577
35 |    */
36 |   MCP_PROXY_FULL_ADDRESS: ConfigItem;
37 | 
38 |   /**
39 |    * Session token for authenticating with the MCP Proxy Server. This token is displayed in the proxy server console on startup.
40 |    */
41 |   MCP_PROXY_AUTH_TOKEN: ConfigItem;
42 | };
43 | 


--------------------------------------------------------------------------------
/client/src/lib/constants.ts:
--------------------------------------------------------------------------------
 1 | import { InspectorConfig } from "./configurationTypes";
 2 | 
 3 | // OAuth-related session storage keys
 4 | export const SESSION_KEYS = {
 5 |   CODE_VERIFIER: "mcp_code_verifier",
 6 |   SERVER_URL: "mcp_server_url",
 7 |   TOKENS: "mcp_tokens",
 8 |   CLIENT_INFORMATION: "mcp_client_information",
 9 |   SERVER_METADATA: "mcp_server_metadata",
10 |   AUTH_DEBUGGER_STATE: "mcp_auth_debugger_state",
11 | } as const;
12 | 
13 | // Generate server-specific session storage keys
14 | export const getServerSpecificKey = (
15 |   baseKey: string,
16 |   serverUrl?: string,
17 | ): string => {
18 |   if (!serverUrl) return baseKey;
19 |   return `[${serverUrl}] ${baseKey}`;
20 | };
21 | 
22 | export type ConnectionStatus =
23 |   | "disconnected"
24 |   | "connected"
25 |   | "error"
26 |   | "error-connecting-to-proxy";
27 | 
28 | export const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277";
29 | 
30 | /**
31 |  * Default configuration for the MCP Inspector, Currently persisted in local_storage in the Browser.
32 |  * Future plans: Provide json config file + Browser local_storage to override default values
33 |  **/
34 | export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = {
35 |   MCP_SERVER_REQUEST_TIMEOUT: {
36 |     label: "Request Timeout",
37 |     description: "Timeout for requests to the MCP server (ms)",
38 |     value: 10000,
39 |     is_session_item: false,
40 |   },
41 |   MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS: {
42 |     label: "Reset Timeout on Progress",
43 |     description: "Reset timeout on progress notifications",
44 |     value: true,
45 |     is_session_item: false,
46 |   },
47 |   MCP_REQUEST_MAX_TOTAL_TIMEOUT: {
48 |     label: "Maximum Total Timeout",
49 |     description:
50 |       "Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)",
51 |     value: 60000,
52 |     is_session_item: false,
53 |   },
54 |   MCP_PROXY_FULL_ADDRESS: {
55 |     label: "Inspector Proxy Address",
56 |     description:
57 |       "Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
58 |     value: "",
59 |     is_session_item: false,
60 |   },
61 |   MCP_PROXY_AUTH_TOKEN: {
62 |     label: "Proxy Session Token",
63 |     description:
64 |       "Session token for authenticating with the MCP Proxy Server (displayed in proxy console on startup)",
65 |     value: "",
66 |     is_session_item: true,
67 |   },
68 | } as const;
69 | 


--------------------------------------------------------------------------------
/client/src/lib/hooks/useCompletionState.ts:
--------------------------------------------------------------------------------
  1 | import { useState, useCallback, useEffect, useRef, useMemo } from "react";
  2 | import {
  3 |   ResourceReference,
  4 |   PromptReference,
  5 | } from "@modelcontextprotocol/sdk/types.js";
  6 | 
  7 | interface CompletionState {
  8 |   completions: Record<string, string[]>;
  9 |   loading: Record<string, boolean>;
 10 | }
 11 | 
 12 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
 13 | function debounce<T extends (...args: any[]) => PromiseLike<void>>(
 14 |   func: T,
 15 |   wait: number,
 16 | ): (...args: Parameters<T>) => void {
 17 |   let timeout: ReturnType<typeof setTimeout>;
 18 |   return (...args: Parameters<T>) => {
 19 |     clearTimeout(timeout);
 20 |     timeout = setTimeout(() => {
 21 |       void func(...args);
 22 |     }, wait);
 23 |   };
 24 | }
 25 | 
 26 | export function useCompletionState(
 27 |   handleCompletion: (
 28 |     ref: ResourceReference | PromptReference,
 29 |     argName: string,
 30 |     value: string,
 31 |     signal?: AbortSignal,
 32 |   ) => Promise<string[]>,
 33 |   completionsSupported: boolean = true,
 34 |   debounceMs: number = 300,
 35 | ) {
 36 |   const [state, setState] = useState<CompletionState>({
 37 |     completions: {},
 38 |     loading: {},
 39 |   });
 40 | 
 41 |   const abortControllerRef = useRef<AbortController | null>(null);
 42 | 
 43 |   const cleanup = useCallback(() => {
 44 |     if (abortControllerRef.current) {
 45 |       abortControllerRef.current.abort();
 46 |       abortControllerRef.current = null;
 47 |     }
 48 |   }, []);
 49 | 
 50 |   // Cleanup on unmount
 51 |   useEffect(() => {
 52 |     return cleanup;
 53 |   }, [cleanup]);
 54 | 
 55 |   const clearCompletions = useCallback(() => {
 56 |     cleanup();
 57 |     setState({
 58 |       completions: {},
 59 |       loading: {},
 60 |     });
 61 |   }, [cleanup]);
 62 | 
 63 |   const requestCompletions = useMemo(() => {
 64 |     return debounce(
 65 |       async (
 66 |         ref: ResourceReference | PromptReference,
 67 |         argName: string,
 68 |         value: string,
 69 |       ) => {
 70 |         if (!completionsSupported) {
 71 |           return;
 72 |         }
 73 | 
 74 |         cleanup();
 75 | 
 76 |         const abortController = new AbortController();
 77 |         abortControllerRef.current = abortController;
 78 | 
 79 |         setState((prev) => ({
 80 |           ...prev,
 81 |           loading: { ...prev.loading, [argName]: true },
 82 |         }));
 83 | 
 84 |         try {
 85 |           const values = await handleCompletion(
 86 |             ref,
 87 |             argName,
 88 |             value,
 89 |             abortController.signal,
 90 |           );
 91 | 
 92 |           if (!abortController.signal.aborted) {
 93 |             setState((prev) => ({
 94 |               ...prev,
 95 |               completions: { ...prev.completions, [argName]: values },
 96 |               loading: { ...prev.loading, [argName]: false },
 97 |             }));
 98 |           }
 99 |         } catch {
100 |           if (!abortController.signal.aborted) {
101 |             setState((prev) => ({
102 |               ...prev,
103 |               loading: { ...prev.loading, [argName]: false },
104 |             }));
105 |           }
106 |         } finally {
107 |           if (abortControllerRef.current === abortController) {
108 |             abortControllerRef.current = null;
109 |           }
110 |         }
111 |       },
112 |       debounceMs,
113 |     );
114 |   }, [handleCompletion, completionsSupported, cleanup, debounceMs]);
115 | 
116 |   // Clear completions when support status changes
117 |   useEffect(() => {
118 |     if (!completionsSupported) {
119 |       clearCompletions();
120 |     }
121 |   }, [completionsSupported, clearCompletions]);
122 | 
123 |   return {
124 |     ...state,
125 |     clearCompletions,
126 |     requestCompletions,
127 |     completionsSupported,
128 |   };
129 | }
130 | 


--------------------------------------------------------------------------------
/client/src/lib/hooks/useDraggablePane.ts:
--------------------------------------------------------------------------------
  1 | import { useCallback, useEffect, useRef, useState } from "react";
  2 | 
  3 | export function useDraggablePane(initialHeight: number) {
  4 |   const [height, setHeight] = useState(initialHeight);
  5 |   const [isDragging, setIsDragging] = useState(false);
  6 |   const dragStartY = useRef<number>(0);
  7 |   const dragStartHeight = useRef<number>(0);
  8 | 
  9 |   const handleDragStart = useCallback(
 10 |     (e: React.MouseEvent) => {
 11 |       setIsDragging(true);
 12 |       dragStartY.current = e.clientY;
 13 |       dragStartHeight.current = height;
 14 |       document.body.style.userSelect = "none";
 15 |     },
 16 |     [height],
 17 |   );
 18 | 
 19 |   const handleDragMove = useCallback(
 20 |     (e: MouseEvent) => {
 21 |       if (!isDragging) return;
 22 |       const deltaY = dragStartY.current - e.clientY;
 23 |       const newHeight = Math.max(
 24 |         100,
 25 |         Math.min(800, dragStartHeight.current + deltaY),
 26 |       );
 27 |       setHeight(newHeight);
 28 |     },
 29 |     [isDragging],
 30 |   );
 31 | 
 32 |   const handleDragEnd = useCallback(() => {
 33 |     setIsDragging(false);
 34 |     document.body.style.userSelect = "";
 35 |   }, []);
 36 | 
 37 |   useEffect(() => {
 38 |     if (isDragging) {
 39 |       window.addEventListener("mousemove", handleDragMove);
 40 |       window.addEventListener("mouseup", handleDragEnd);
 41 |       return () => {
 42 |         window.removeEventListener("mousemove", handleDragMove);
 43 |         window.removeEventListener("mouseup", handleDragEnd);
 44 |       };
 45 |     }
 46 |   }, [isDragging, handleDragMove, handleDragEnd]);
 47 | 
 48 |   return {
 49 |     height,
 50 |     isDragging,
 51 |     handleDragStart,
 52 |   };
 53 | }
 54 | 
 55 | export function useDraggableSidebar(initialWidth: number) {
 56 |   const [width, setWidth] = useState(initialWidth);
 57 |   const [isDragging, setIsDragging] = useState(false);
 58 |   const dragStartX = useRef<number>(0);
 59 |   const dragStartWidth = useRef<number>(0);
 60 | 
 61 |   const handleDragStart = useCallback(
 62 |     (e: React.MouseEvent) => {
 63 |       setIsDragging(true);
 64 |       dragStartX.current = e.clientX;
 65 |       dragStartWidth.current = width;
 66 |       document.body.style.userSelect = "none";
 67 |     },
 68 |     [width],
 69 |   );
 70 | 
 71 |   const handleDragMove = useCallback(
 72 |     (e: MouseEvent) => {
 73 |       if (!isDragging) return;
 74 |       const deltaX = e.clientX - dragStartX.current;
 75 |       const newWidth = Math.max(
 76 |         200,
 77 |         Math.min(600, dragStartWidth.current + deltaX),
 78 |       );
 79 |       setWidth(newWidth);
 80 |     },
 81 |     [isDragging],
 82 |   );
 83 | 
 84 |   const handleDragEnd = useCallback(() => {
 85 |     setIsDragging(false);
 86 |     document.body.style.userSelect = "";
 87 |   }, []);
 88 | 
 89 |   useEffect(() => {
 90 |     if (isDragging) {
 91 |       window.addEventListener("mousemove", handleDragMove);
 92 |       window.addEventListener("mouseup", handleDragEnd);
 93 |       return () => {
 94 |         window.removeEventListener("mousemove", handleDragMove);
 95 |         window.removeEventListener("mouseup", handleDragEnd);
 96 |       };
 97 |     }
 98 |   }, [isDragging, handleDragMove, handleDragEnd]);
 99 | 
100 |   return {
101 |     width,
102 |     isDragging,
103 |     handleDragStart,
104 |   };
105 | }
106 | 


--------------------------------------------------------------------------------
/client/src/lib/hooks/useTheme.ts:
--------------------------------------------------------------------------------
 1 | import { useCallback, useEffect, useMemo, useState } from "react";
 2 | 
 3 | type Theme = "light" | "dark" | "system";
 4 | 
 5 | const useTheme = (): [Theme, (mode: Theme) => void] => {
 6 |   const [theme, setTheme] = useState<Theme>(() => {
 7 |     const savedTheme = localStorage.getItem("theme") as Theme;
 8 |     return savedTheme || "system";
 9 |   });
10 | 
11 |   useEffect(() => {
12 |     const darkModeMediaQuery = window.matchMedia(
13 |       "(prefers-color-scheme: dark)",
14 |     );
15 |     const handleDarkModeChange = (e: MediaQueryListEvent) => {
16 |       if (theme === "system") {
17 |         updateDocumentTheme(e.matches ? "dark" : "light");
18 |       }
19 |     };
20 | 
21 |     const updateDocumentTheme = (newTheme: "light" | "dark") => {
22 |       document.documentElement.classList.toggle("dark", newTheme === "dark");
23 |     };
24 | 
25 |     // Set initial theme based on current mode
26 |     if (theme === "system") {
27 |       updateDocumentTheme(darkModeMediaQuery.matches ? "dark" : "light");
28 |     } else {
29 |       updateDocumentTheme(theme);
30 |     }
31 | 
32 |     darkModeMediaQuery.addEventListener("change", handleDarkModeChange);
33 | 
34 |     return () => {
35 |       darkModeMediaQuery.removeEventListener("change", handleDarkModeChange);
36 |     };
37 |   }, [theme]);
38 | 
39 |   const setThemeWithSideEffect = useCallback((newTheme: Theme) => {
40 |     setTheme(newTheme);
41 |     localStorage.setItem("theme", newTheme);
42 |     if (newTheme !== "system") {
43 |       document.documentElement.classList.toggle("dark", newTheme === "dark");
44 |     }
45 |   }, []);
46 |   return useMemo(
47 |     () => [theme, setThemeWithSideEffect],
48 |     [theme, setThemeWithSideEffect],
49 |   );
50 | };
51 | 
52 | export default useTheme;
53 | 


--------------------------------------------------------------------------------
/client/src/lib/hooks/useToast.ts:
--------------------------------------------------------------------------------
  1 | "use client";
  2 | 
  3 | // Inspired by react-hot-toast library
  4 | import * as React from "react";
  5 | 
  6 | import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
  7 | 
  8 | const TOAST_LIMIT = 1;
  9 | const TOAST_REMOVE_DELAY = 1000000;
 10 | 
 11 | type ToasterToast = ToastProps & {
 12 |   id: string;
 13 |   title?: React.ReactNode;
 14 |   description?: React.ReactNode;
 15 |   action?: ToastActionElement;
 16 | };
 17 | 
 18 | let count = 0;
 19 | 
 20 | function genId() {
 21 |   count = (count + 1) % Number.MAX_SAFE_INTEGER;
 22 |   return count.toString();
 23 | }
 24 | 
 25 | const enum ActionType {
 26 |   ADD_TOAST = "ADD_TOAST",
 27 |   UPDATE_TOAST = "UPDATE_TOAST",
 28 |   DISMISS_TOAST = "DISMISS_TOAST",
 29 |   REMOVE_TOAST = "REMOVE_TOAST",
 30 | }
 31 | 
 32 | type Action =
 33 |   | {
 34 |       type: ActionType.ADD_TOAST;
 35 |       toast: ToasterToast;
 36 |     }
 37 |   | {
 38 |       type: ActionType.UPDATE_TOAST;
 39 |       toast: Partial<ToasterToast>;
 40 |     }
 41 |   | {
 42 |       type: ActionType.DISMISS_TOAST;
 43 |       toastId?: ToasterToast["id"];
 44 |     }
 45 |   | {
 46 |       type: ActionType.REMOVE_TOAST;
 47 |       toastId?: ToasterToast["id"];
 48 |     };
 49 | 
 50 | interface State {
 51 |   toasts: ToasterToast[];
 52 | }
 53 | 
 54 | const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
 55 | 
 56 | const addToRemoveQueue = (toastId: string) => {
 57 |   if (toastTimeouts.has(toastId)) {
 58 |     return;
 59 |   }
 60 | 
 61 |   const timeout = setTimeout(() => {
 62 |     toastTimeouts.delete(toastId);
 63 |     dispatch({
 64 |       type: ActionType.REMOVE_TOAST,
 65 |       toastId: toastId,
 66 |     });
 67 |   }, TOAST_REMOVE_DELAY);
 68 | 
 69 |   toastTimeouts.set(toastId, timeout);
 70 | };
 71 | 
 72 | export const reducer = (state: State, action: Action): State => {
 73 |   switch (action.type) {
 74 |     case ActionType.ADD_TOAST:
 75 |       return {
 76 |         ...state,
 77 |         toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
 78 |       };
 79 | 
 80 |     case ActionType.UPDATE_TOAST:
 81 |       return {
 82 |         ...state,
 83 |         toasts: state.toasts.map((t) =>
 84 |           t.id === action.toast.id ? { ...t, ...action.toast } : t,
 85 |         ),
 86 |       };
 87 | 
 88 |     case ActionType.DISMISS_TOAST: {
 89 |       const { toastId } = action;
 90 | 
 91 |       // ! Side effects ! - This could be extracted into a dismissToast() action,
 92 |       // but I'll keep it here for simplicity
 93 |       if (toastId) {
 94 |         addToRemoveQueue(toastId);
 95 |       } else {
 96 |         state.toasts.forEach((toast) => {
 97 |           addToRemoveQueue(toast.id);
 98 |         });
 99 |       }
100 | 
101 |       return {
102 |         ...state,
103 |         toasts: state.toasts.map((t) =>
104 |           t.id === toastId || toastId === undefined
105 |             ? {
106 |                 ...t,
107 |                 open: false,
108 |               }
109 |             : t,
110 |         ),
111 |       };
112 |     }
113 |     case ActionType.REMOVE_TOAST:
114 |       if (action.toastId === undefined) {
115 |         return {
116 |           ...state,
117 |           toasts: [],
118 |         };
119 |       }
120 |       return {
121 |         ...state,
122 |         toasts: state.toasts.filter((t) => t.id !== action.toastId),
123 |       };
124 |   }
125 | };
126 | 
127 | const listeners: Array<(state: State) => void> = [];
128 | 
129 | let memoryState: State = { toasts: [] };
130 | 
131 | function dispatch(action: Action) {
132 |   memoryState = reducer(memoryState, action);
133 |   listeners.forEach((listener) => {
134 |     listener(memoryState);
135 |   });
136 | }
137 | 
138 | type Toast = Omit<ToasterToast, "id">;
139 | 
140 | function toast({ ...props }: Toast) {
141 |   const id = genId();
142 | 
143 |   const update = (props: ToasterToast) =>
144 |     dispatch({
145 |       type: ActionType.UPDATE_TOAST,
146 |       toast: { ...props, id },
147 |     });
148 |   const dismiss = () =>
149 |     dispatch({ type: ActionType.DISMISS_TOAST, toastId: id });
150 | 
151 |   dispatch({
152 |     type: ActionType.ADD_TOAST,
153 |     toast: {
154 |       ...props,
155 |       id,
156 |       open: true,
157 |       onOpenChange: (open) => {
158 |         if (!open) dismiss();
159 |       },
160 |     },
161 |   });
162 | 
163 |   return {
164 |     id: id,
165 |     dismiss,
166 |     update,
167 |   };
168 | }
169 | 
170 | function useToast() {
171 |   const [state, setState] = React.useState<State>(memoryState);
172 | 
173 |   React.useEffect(() => {
174 |     listeners.push(setState);
175 |     return () => {
176 |       const index = listeners.indexOf(setState);
177 |       if (index > -1) {
178 |         listeners.splice(index, 1);
179 |       }
180 |     };
181 |   }, [state]);
182 | 
183 |   return {
184 |     ...state,
185 |     toast,
186 |     dismiss: (toastId?: string) =>
187 |       dispatch({ type: ActionType.DISMISS_TOAST, toastId }),
188 |   };
189 | }
190 | 
191 | export { useToast, toast };
192 | 


--------------------------------------------------------------------------------
/client/src/lib/notificationTypes.ts:
--------------------------------------------------------------------------------
 1 | import {
 2 |   NotificationSchema as BaseNotificationSchema,
 3 |   ClientNotificationSchema,
 4 |   ServerNotificationSchema,
 5 | } from "@modelcontextprotocol/sdk/types.js";
 6 | import { z } from "zod";
 7 | 
 8 | export const StdErrNotificationSchema = BaseNotificationSchema.extend({
 9 |   method: z.literal("notifications/stderr"),
10 |   params: z.object({
11 |     content: z.string(),
12 |   }),
13 | });
14 | 
15 | export const NotificationSchema = ClientNotificationSchema.or(
16 |   StdErrNotificationSchema,
17 | )
18 |   .or(ServerNotificationSchema)
19 |   .or(BaseNotificationSchema);
20 | 
21 | export type StdErrNotification = z.infer<typeof StdErrNotificationSchema>;
22 | export type Notification = z.infer<typeof NotificationSchema>;
23 | 


--------------------------------------------------------------------------------
/client/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 | 
4 | export function cn(...inputs: ClassValue[]) {
5 |   return twMerge(clsx(inputs));
6 | }
7 | 


--------------------------------------------------------------------------------
/client/src/main.tsx:
--------------------------------------------------------------------------------
 1 | import { StrictMode } from "react";
 2 | import { createRoot } from "react-dom/client";
 3 | import { Toaster } from "@/components/ui/toaster.tsx";
 4 | import App from "./App.tsx";
 5 | import "./index.css";
 6 | import { TooltipProvider } from "./components/ui/tooltip.tsx";
 7 | 
 8 | createRoot(document.getElementById("root")!).render(
 9 |   <StrictMode>
10 |     <TooltipProvider>
11 |       <App />
12 |     </TooltipProvider>
13 |     <Toaster />
14 |   </StrictMode>,
15 | );
16 | 


--------------------------------------------------------------------------------
/client/src/utils/__tests__/configUtils.test.ts:
--------------------------------------------------------------------------------
 1 | import { getMCPProxyAuthToken } from "../configUtils";
 2 | import { DEFAULT_INSPECTOR_CONFIG } from "../../lib/constants";
 3 | import { InspectorConfig } from "../../lib/configurationTypes";
 4 | 
 5 | describe("configUtils", () => {
 6 |   describe("getMCPProxyAuthToken", () => {
 7 |     test("returns token and default header name", () => {
 8 |       const config: InspectorConfig = {
 9 |         ...DEFAULT_INSPECTOR_CONFIG,
10 |         MCP_PROXY_AUTH_TOKEN: {
11 |           ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
12 |           value: "test-token-123",
13 |         },
14 |       };
15 | 
16 |       const result = getMCPProxyAuthToken(config);
17 | 
18 |       expect(result).toEqual({
19 |         token: "test-token-123",
20 |         header: "X-MCP-Proxy-Auth",
21 |       });
22 |     });
23 | 
24 |     test("returns empty token when not configured", () => {
25 |       const config: InspectorConfig = {
26 |         ...DEFAULT_INSPECTOR_CONFIG,
27 |         MCP_PROXY_AUTH_TOKEN: {
28 |           ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
29 |           value: "",
30 |         },
31 |       };
32 | 
33 |       const result = getMCPProxyAuthToken(config);
34 | 
35 |       expect(result).toEqual({
36 |         token: "",
37 |         header: "X-MCP-Proxy-Auth",
38 |       });
39 |     });
40 | 
41 |     test("always returns X-MCP-Proxy-Auth as header name", () => {
42 |       const config: InspectorConfig = {
43 |         ...DEFAULT_INSPECTOR_CONFIG,
44 |         MCP_PROXY_AUTH_TOKEN: {
45 |           ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
46 |           value: "any-token",
47 |         },
48 |       };
49 | 
50 |       const result = getMCPProxyAuthToken(config);
51 | 
52 |       expect(result.header).toBe("X-MCP-Proxy-Auth");
53 |     });
54 | 
55 |     test("handles null/undefined value gracefully", () => {
56 |       const config: InspectorConfig = {
57 |         ...DEFAULT_INSPECTOR_CONFIG,
58 |         MCP_PROXY_AUTH_TOKEN: {
59 |           ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
60 |           value: null as unknown as string,
61 |         },
62 |       };
63 | 
64 |       const result = getMCPProxyAuthToken(config);
65 | 
66 |       expect(result).toEqual({
67 |         token: null,
68 |         header: "X-MCP-Proxy-Auth",
69 |       });
70 |     });
71 |   });
72 | });
73 | 


--------------------------------------------------------------------------------
/client/src/utils/__tests__/escapeUnicode.test.ts:
--------------------------------------------------------------------------------
 1 | import { escapeUnicode } from "../escapeUnicode";
 2 | 
 3 | describe("escapeUnicode", () => {
 4 |   it("should escape Unicode characters in a string", () => {
 5 |     const input = { text: "你好世界" };
 6 |     const expected = '{\n  "text": "\\\\u4f60\\\\u597d\\\\u4e16\\\\u754c"\n}';
 7 |     expect(escapeUnicode(input)).toBe(expected);
 8 |   });
 9 | 
10 |   it("should handle empty strings", () => {
11 |     const input = { text: "" };
12 |     const expected = '{\n  "text": ""\n}';
13 |     expect(escapeUnicode(input)).toBe(expected);
14 |   });
15 | 
16 |   it("should handle null and undefined values", () => {
17 |     const input = { text: null, value: undefined };
18 |     const expected = '{\n  "text": null\n}';
19 |     expect(escapeUnicode(input)).toBe(expected);
20 |   });
21 | 
22 |   it("should handle numbers and booleans", () => {
23 |     const input = { number: 123, boolean: true };
24 |     const expected = '{\n  "number": 123,\n  "boolean": true\n}';
25 |     expect(escapeUnicode(input)).toBe(expected);
26 |   });
27 | });
28 | 


--------------------------------------------------------------------------------
/client/src/utils/__tests__/oauthUtils.ts:
--------------------------------------------------------------------------------
 1 | import {
 2 |   generateOAuthErrorDescription,
 3 |   parseOAuthCallbackParams,
 4 | } from "@/utils/oauthUtils.ts";
 5 | 
 6 | describe("parseOAuthCallbackParams", () => {
 7 |   it("Returns successful: true and code when present", () => {
 8 |     expect(parseOAuthCallbackParams("?code=fake-code")).toEqual({
 9 |       successful: true,
10 |       code: "fake-code",
11 |     });
12 |   });
13 |   it("Returns successful: false and error when error is present", () => {
14 |     expect(parseOAuthCallbackParams("?error=access_denied")).toEqual({
15 |       successful: false,
16 |       error: "access_denied",
17 |       error_description: null,
18 |       error_uri: null,
19 |     });
20 |   });
21 |   it("Returns optional error metadata fields when present", () => {
22 |     const search =
23 |       "?error=access_denied&" +
24 |       "error_description=User%20Denied%20Request&" +
25 |       "error_uri=https%3A%2F%2Fexample.com%2Ferror-docs";
26 |     expect(parseOAuthCallbackParams(search)).toEqual({
27 |       successful: false,
28 |       error: "access_denied",
29 |       error_description: "User Denied Request",
30 |       error_uri: "https://example.com/error-docs",
31 |     });
32 |   });
33 |   it("Returns error when nothing present", () => {
34 |     expect(parseOAuthCallbackParams("?")).toEqual({
35 |       successful: false,
36 |       error: "invalid_request",
37 |       error_description: "Missing code or error in response",
38 |       error_uri: null,
39 |     });
40 |   });
41 | });
42 | 
43 | describe("generateOAuthErrorDescription", () => {
44 |   it("When only error is present", () => {
45 |     expect(
46 |       generateOAuthErrorDescription({
47 |         successful: false,
48 |         error: "invalid_request",
49 |         error_description: null,
50 |         error_uri: null,
51 |       }),
52 |     ).toBe("Error: invalid_request.");
53 |   });
54 |   it("When error description is present", () => {
55 |     expect(
56 |       generateOAuthErrorDescription({
57 |         successful: false,
58 |         error: "invalid_request",
59 |         error_description: "The request could not be completed as dialed",
60 |         error_uri: null,
61 |       }),
62 |     ).toEqual(
63 |       "Error: invalid_request.\nDetails: The request could not be completed as dialed.",
64 |     );
65 |   });
66 |   it("When all fields present", () => {
67 |     expect(
68 |       generateOAuthErrorDescription({
69 |         successful: false,
70 |         error: "invalid_request",
71 |         error_description: "The request could not be completed as dialed",
72 |         error_uri: "https://example.com/error-docs",
73 |       }),
74 |     ).toEqual(
75 |       "Error: invalid_request.\nDetails: The request could not be completed as dialed.\nMore info: https://example.com/error-docs.",
76 |     );
77 |   });
78 | });
79 | 


--------------------------------------------------------------------------------
/client/src/utils/configUtils.ts:
--------------------------------------------------------------------------------
  1 | import { InspectorConfig } from "@/lib/configurationTypes";
  2 | import {
  3 |   DEFAULT_MCP_PROXY_LISTEN_PORT,
  4 |   DEFAULT_INSPECTOR_CONFIG,
  5 | } from "@/lib/constants";
  6 | 
  7 | const getSearchParam = (key: string): string | null => {
  8 |   try {
  9 |     const url = new URL(window.location.href);
 10 |     return url.searchParams.get(key);
 11 |   } catch {
 12 |     return null;
 13 |   }
 14 | };
 15 | 
 16 | export const getMCPProxyAddress = (config: InspectorConfig): string => {
 17 |   let proxyFullAddress = config.MCP_PROXY_FULL_ADDRESS.value as string;
 18 |   if (proxyFullAddress) {
 19 |     proxyFullAddress = proxyFullAddress.replace(/\/+$/, "");
 20 |     return proxyFullAddress;
 21 |   }
 22 | 
 23 |   // Check for proxy port from query params, fallback to default
 24 |   const proxyPort =
 25 |     getSearchParam("MCP_PROXY_PORT") || DEFAULT_MCP_PROXY_LISTEN_PORT;
 26 | 
 27 |   return `${window.location.protocol}//${window.location.hostname}:${proxyPort}`;
 28 | };
 29 | 
 30 | export const getMCPServerRequestTimeout = (config: InspectorConfig): number => {
 31 |   return config.MCP_SERVER_REQUEST_TIMEOUT.value as number;
 32 | };
 33 | 
 34 | export const resetRequestTimeoutOnProgress = (
 35 |   config: InspectorConfig,
 36 | ): boolean => {
 37 |   return config.MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS.value as boolean;
 38 | };
 39 | 
 40 | export const getMCPServerRequestMaxTotalTimeout = (
 41 |   config: InspectorConfig,
 42 | ): number => {
 43 |   return config.MCP_REQUEST_MAX_TOTAL_TIMEOUT.value as number;
 44 | };
 45 | 
 46 | export const getMCPProxyAuthToken = (
 47 |   config: InspectorConfig,
 48 | ): {
 49 |   token: string;
 50 |   header: string;
 51 | } => {
 52 |   return {
 53 |     token: config.MCP_PROXY_AUTH_TOKEN.value as string,
 54 |     header: "X-MCP-Proxy-Auth",
 55 |   };
 56 | };
 57 | 
 58 | export const getInitialTransportType = ():
 59 |   | "stdio"
 60 |   | "sse"
 61 |   | "streamable-http" => {
 62 |   const param = getSearchParam("transport");
 63 |   if (param === "stdio" || param === "sse" || param === "streamable-http") {
 64 |     return param;
 65 |   }
 66 |   return (
 67 |     (localStorage.getItem("lastTransportType") as
 68 |       | "stdio"
 69 |       | "sse"
 70 |       | "streamable-http") || "stdio"
 71 |   );
 72 | };
 73 | 
 74 | export const getInitialSseUrl = (): string => {
 75 |   const param = getSearchParam("serverUrl");
 76 |   if (param) return param;
 77 |   return localStorage.getItem("lastSseUrl") || "http://localhost:3001/sse";
 78 | };
 79 | 
 80 | export const getInitialCommand = (): string => {
 81 |   const param = getSearchParam("serverCommand");
 82 |   if (param) return param;
 83 |   return localStorage.getItem("lastCommand") || "mcp-server-everything";
 84 | };
 85 | 
 86 | export const getInitialArgs = (): string => {
 87 |   const param = getSearchParam("serverArgs");
 88 |   if (param) return param;
 89 |   return localStorage.getItem("lastArgs") || "";
 90 | };
 91 | 
 92 | // Returns a map of config key -> value from query params if present
 93 | export const getConfigOverridesFromQueryParams = (
 94 |   defaultConfig: InspectorConfig,
 95 | ): Partial<InspectorConfig> => {
 96 |   const url = new URL(window.location.href);
 97 |   const overrides: Partial<InspectorConfig> = {};
 98 |   for (const key of Object.keys(defaultConfig)) {
 99 |     const param = url.searchParams.get(key);
100 |     if (param !== null) {
101 |       // Try to coerce to correct type based on default value
102 |       const defaultValue = defaultConfig[key as keyof InspectorConfig].value;
103 |       let value: string | number | boolean = param;
104 |       if (typeof defaultValue === "number") {
105 |         value = Number(param);
106 |       } else if (typeof defaultValue === "boolean") {
107 |         value = param === "true";
108 |       }
109 |       overrides[key as keyof InspectorConfig] = {
110 |         ...defaultConfig[key as keyof InspectorConfig],
111 |         value,
112 |       };
113 |     }
114 |   }
115 |   return overrides;
116 | };
117 | 
118 | export const initializeInspectorConfig = (
119 |   localStorageKey: string,
120 | ): InspectorConfig => {
121 |   // Read persistent config from localStorage
122 |   const savedPersistentConfig = localStorage.getItem(localStorageKey);
123 |   // Read ephemeral config from sessionStorage
124 |   const savedEphemeralConfig = sessionStorage.getItem(
125 |     `${localStorageKey}_ephemeral`,
126 |   );
127 | 
128 |   // Start with default config
129 |   let baseConfig = { ...DEFAULT_INSPECTOR_CONFIG };
130 | 
131 |   // Apply saved persistent config
132 |   if (savedPersistentConfig) {
133 |     const parsedPersistentConfig = JSON.parse(savedPersistentConfig);
134 |     baseConfig = { ...baseConfig, ...parsedPersistentConfig };
135 |   }
136 | 
137 |   // Apply saved ephemeral config
138 |   if (savedEphemeralConfig) {
139 |     const parsedEphemeralConfig = JSON.parse(savedEphemeralConfig);
140 |     baseConfig = { ...baseConfig, ...parsedEphemeralConfig };
141 |   }
142 | 
143 |   // Ensure all config items have the latest labels/descriptions from defaults
144 |   for (const [key, value] of Object.entries(baseConfig)) {
145 |     baseConfig[key as keyof InspectorConfig] = {
146 |       ...value,
147 |       label: DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].label,
148 |       description:
149 |         DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].description,
150 |       is_session_item:
151 |         DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].is_session_item,
152 |     };
153 |   }
154 | 
155 |   // Apply query param overrides
156 |   const overrides = getConfigOverridesFromQueryParams(DEFAULT_INSPECTOR_CONFIG);
157 |   return { ...baseConfig, ...overrides };
158 | };
159 | 
160 | export const saveInspectorConfig = (
161 |   localStorageKey: string,
162 |   config: InspectorConfig,
163 | ): void => {
164 |   const persistentConfig: Partial<InspectorConfig> = {};
165 |   const ephemeralConfig: Partial<InspectorConfig> = {};
166 | 
167 |   // Split config based on is_session_item flag
168 |   for (const [key, value] of Object.entries(config)) {
169 |     if (value.is_session_item) {
170 |       ephemeralConfig[key as keyof InspectorConfig] = value;
171 |     } else {
172 |       persistentConfig[key as keyof InspectorConfig] = value;
173 |     }
174 |   }
175 | 
176 |   // Save persistent config to localStorage
177 |   localStorage.setItem(localStorageKey, JSON.stringify(persistentConfig));
178 | 
179 |   // Save ephemeral config to sessionStorage
180 |   sessionStorage.setItem(
181 |     `${localStorageKey}_ephemeral`,
182 |     JSON.stringify(ephemeralConfig),
183 |   );
184 | };
185 | 


--------------------------------------------------------------------------------
/client/src/utils/escapeUnicode.ts:
--------------------------------------------------------------------------------
 1 | // Utility function to escape Unicode characters
 2 | export function escapeUnicode(obj: unknown): string {
 3 |   return JSON.stringify(
 4 |     obj,
 5 |     (_key: string, value) => {
 6 |       if (typeof value === "string") {
 7 |         // Replace non-ASCII characters with their Unicode escape sequences
 8 |         return value.replace(/[^\0-\x7F]/g, (char) => {
 9 |           return "\\u" + ("0000" + char.charCodeAt(0).toString(16)).slice(-4);
10 |         });
11 |       }
12 |       return value;
13 |     },
14 |     2,
15 |   );
16 | }
17 | 


--------------------------------------------------------------------------------
/client/src/utils/jsonUtils.ts:
--------------------------------------------------------------------------------
  1 | export type JsonValue =
  2 |   | string
  3 |   | number
  4 |   | boolean
  5 |   | null
  6 |   | undefined
  7 |   | JsonValue[]
  8 |   | { [key: string]: JsonValue };
  9 | 
 10 | export type JsonSchemaType = {
 11 |   type:
 12 |     | "string"
 13 |     | "number"
 14 |     | "integer"
 15 |     | "boolean"
 16 |     | "array"
 17 |     | "object"
 18 |     | "null";
 19 |   description?: string;
 20 |   required?: string[];
 21 |   default?: JsonValue;
 22 |   properties?: Record<string, JsonSchemaType>;
 23 |   items?: JsonSchemaType;
 24 | };
 25 | 
 26 | export type JsonObject = { [key: string]: JsonValue };
 27 | 
 28 | export type DataType =
 29 |   | "string"
 30 |   | "number"
 31 |   | "bigint"
 32 |   | "boolean"
 33 |   | "symbol"
 34 |   | "undefined"
 35 |   | "object"
 36 |   | "function"
 37 |   | "array"
 38 |   | "null";
 39 | 
 40 | export function getDataType(value: JsonValue): DataType {
 41 |   if (Array.isArray(value)) return "array";
 42 |   if (value === null) return "null";
 43 |   return typeof value;
 44 | }
 45 | 
 46 | export function tryParseJson(str: string): {
 47 |   success: boolean;
 48 |   data: JsonValue;
 49 | } {
 50 |   const trimmed = str.trim();
 51 |   if (
 52 |     !(trimmed.startsWith("{") && trimmed.endsWith("}")) &&
 53 |     !(trimmed.startsWith("[") && trimmed.endsWith("]"))
 54 |   ) {
 55 |     return { success: false, data: str };
 56 |   }
 57 |   try {
 58 |     return { success: true, data: JSON.parse(str) };
 59 |   } catch {
 60 |     return { success: false, data: str };
 61 |   }
 62 | }
 63 | 
 64 | /**
 65 |  * Updates a value at a specific path in a nested JSON structure
 66 |  * @param obj The original JSON value
 67 |  * @param path Array of keys/indices representing the path to the value
 68 |  * @param value The new value to set
 69 |  * @returns A new JSON value with the updated path
 70 |  */
 71 | export function updateValueAtPath(
 72 |   obj: JsonValue,
 73 |   path: string[],
 74 |   value: JsonValue,
 75 | ): JsonValue {
 76 |   if (path.length === 0) return value;
 77 | 
 78 |   if (obj === null || obj === undefined) {
 79 |     obj = !isNaN(Number(path[0])) ? [] : {};
 80 |   }
 81 | 
 82 |   if (Array.isArray(obj)) {
 83 |     return updateArray(obj, path, value);
 84 |   } else if (typeof obj === "object" && obj !== null) {
 85 |     return updateObject(obj as JsonObject, path, value);
 86 |   } else {
 87 |     console.error(
 88 |       `Cannot update path ${path.join(".")} in non-object/array value:`,
 89 |       obj,
 90 |     );
 91 |     return obj;
 92 |   }
 93 | }
 94 | 
 95 | /**
 96 |  * Updates an array at a specific path
 97 |  */
 98 | function updateArray(
 99 |   array: JsonValue[],
100 |   path: string[],
101 |   value: JsonValue,
102 | ): JsonValue[] {
103 |   const [index, ...restPath] = path;
104 |   const arrayIndex = Number(index);
105 | 
106 |   if (isNaN(arrayIndex)) {
107 |     console.error(`Invalid array index: ${index}`);
108 |     return array;
109 |   }
110 | 
111 |   if (arrayIndex < 0) {
112 |     console.error(`Array index out of bounds: ${arrayIndex} < 0`);
113 |     return array;
114 |   }
115 | 
116 |   let newArray: JsonValue[] = [];
117 |   for (let i = 0; i < array.length; i++) {
118 |     newArray[i] = i in array ? array[i] : null;
119 |   }
120 | 
121 |   if (arrayIndex >= newArray.length) {
122 |     const extendedArray: JsonValue[] = new Array(arrayIndex).fill(null);
123 |     // Copy over the existing elements (now guaranteed to be dense)
124 |     for (let i = 0; i < newArray.length; i++) {
125 |       extendedArray[i] = newArray[i];
126 |     }
127 |     newArray = extendedArray;
128 |   }
129 | 
130 |   if (restPath.length === 0) {
131 |     newArray[arrayIndex] = value;
132 |   } else {
133 |     newArray[arrayIndex] = updateValueAtPath(
134 |       newArray[arrayIndex],
135 |       restPath,
136 |       value,
137 |     );
138 |   }
139 |   return newArray;
140 | }
141 | 
142 | /**
143 |  * Updates an object at a specific path
144 |  */
145 | function updateObject(
146 |   obj: JsonObject,
147 |   path: string[],
148 |   value: JsonValue,
149 | ): JsonObject {
150 |   const [key, ...restPath] = path;
151 | 
152 |   // Validate object key
153 |   if (typeof key !== "string") {
154 |     console.error(`Invalid object key: ${key}`);
155 |     return obj;
156 |   }
157 | 
158 |   const newObj = { ...obj };
159 | 
160 |   if (restPath.length === 0) {
161 |     newObj[key] = value;
162 |   } else {
163 |     // Ensure key exists
164 |     if (!(key in newObj)) {
165 |       newObj[key] = {};
166 |     }
167 |     newObj[key] = updateValueAtPath(newObj[key], restPath, value);
168 |   }
169 |   return newObj;
170 | }
171 | 
172 | /**
173 |  * Gets a value at a specific path in a nested JSON structure
174 |  * @param obj The JSON value to traverse
175 |  * @param path Array of keys/indices representing the path to the value
176 |  * @param defaultValue Value to return if path doesn't exist
177 |  * @returns The value at the path, or defaultValue if not found
178 |  */
179 | export function getValueAtPath(
180 |   obj: JsonValue,
181 |   path: string[],
182 |   defaultValue: JsonValue = null,
183 | ): JsonValue {
184 |   if (path.length === 0) return obj;
185 | 
186 |   const [first, ...rest] = path;
187 | 
188 |   if (obj === null || obj === undefined) {
189 |     return defaultValue;
190 |   }
191 | 
192 |   if (Array.isArray(obj)) {
193 |     const index = Number(first);
194 |     if (isNaN(index) || index < 0 || index >= obj.length) {
195 |       return defaultValue;
196 |     }
197 |     return getValueAtPath(obj[index], rest, defaultValue);
198 |   }
199 | 
200 |   if (typeof obj === "object" && obj !== null) {
201 |     if (!(first in obj)) {
202 |       return defaultValue;
203 |     }
204 |     return getValueAtPath((obj as JsonObject)[first], rest, defaultValue);
205 |   }
206 | 
207 |   return defaultValue;
208 | }
209 | 


--------------------------------------------------------------------------------
/client/src/utils/oauthUtils.ts:
--------------------------------------------------------------------------------
 1 | // The parsed query parameters returned by the Authorization Server
 2 | // representing either a valid authorization_code or an error
 3 | // ref: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-12#section-4.1.2
 4 | type CallbackParams =
 5 |   | {
 6 |       successful: true;
 7 |       // The authorization code is generated by the authorization server.
 8 |       code: string;
 9 |     }
10 |   | {
11 |       successful: false;
12 |       // The OAuth 2.1 Error Code.
13 |       // Usually one of:
14 |       //    ```
15 |       //    invalid_request, unauthorized_client, access_denied, unsupported_response_type,
16 |       //    invalid_scope, server_error, temporarily_unavailable
17 |       //    ```
18 |       error: string;
19 |       // Human-readable ASCII text providing additional information, used to assist the
20 |       // developer in understanding the error that occurred.
21 |       error_description: string | null;
22 |       // A URI identifying a human-readable web page with information about the error,
23 |       // used to provide the client developer with additional information about the error.
24 |       error_uri: string | null;
25 |     };
26 | 
27 | export const parseOAuthCallbackParams = (location: string): CallbackParams => {
28 |   const params = new URLSearchParams(location);
29 | 
30 |   const code = params.get("code");
31 |   if (code) {
32 |     return { successful: true, code };
33 |   }
34 | 
35 |   const error = params.get("error");
36 |   const error_description = params.get("error_description");
37 |   const error_uri = params.get("error_uri");
38 | 
39 |   if (error) {
40 |     return { successful: false, error, error_description, error_uri };
41 |   }
42 | 
43 |   return {
44 |     successful: false,
45 |     error: "invalid_request",
46 |     error_description: "Missing code or error in response",
47 |     error_uri: null,
48 |   };
49 | };
50 | 
51 | export const generateOAuthErrorDescription = (
52 |   params: Extract<CallbackParams, { successful: false }>,
53 | ): string => {
54 |   const error = params.error;
55 |   const errorDescription = params.error_description;
56 |   const errorUri = params.error_uri;
57 | 
58 |   return [
59 |     `Error: ${error}.`,
60 |     errorDescription ? `Details: ${errorDescription}.` : "",
61 |     errorUri ? `More info: ${errorUri}.` : "",
62 |   ]
63 |     .filter(Boolean)
64 |     .join("\n");
65 | };
66 | 


--------------------------------------------------------------------------------
/client/src/utils/schemaUtils.ts:
--------------------------------------------------------------------------------
  1 | import type { JsonValue, JsonSchemaType, JsonObject } from "./jsonUtils";
  2 | import Ajv from "ajv";
  3 | import type { ValidateFunction } from "ajv";
  4 | import type { Tool } from "@modelcontextprotocol/sdk/types.js";
  5 | 
  6 | const ajv = new Ajv();
  7 | 
  8 | // Cache for compiled validators
  9 | const toolOutputValidators = new Map<string, ValidateFunction>();
 10 | 
 11 | /**
 12 |  * Compiles and caches output schema validators for a list of tools
 13 |  * Following the same pattern as SDK's Client.cacheToolOutputSchemas
 14 |  * @param tools Array of tools that may have output schemas
 15 |  */
 16 | export function cacheToolOutputSchemas(tools: Tool[]): void {
 17 |   toolOutputValidators.clear();
 18 |   for (const tool of tools) {
 19 |     if (tool.outputSchema) {
 20 |       try {
 21 |         const validator = ajv.compile(tool.outputSchema);
 22 |         toolOutputValidators.set(tool.name, validator);
 23 |       } catch (error) {
 24 |         console.warn(
 25 |           `Failed to compile output schema for tool ${tool.name}:`,
 26 |           error,
 27 |         );
 28 |       }
 29 |     }
 30 |   }
 31 | }
 32 | 
 33 | /**
 34 |  * Gets the cached output schema validator for a tool
 35 |  * Following the same pattern as SDK's Client.getToolOutputValidator
 36 |  * @param toolName Name of the tool
 37 |  * @returns The compiled validator function, or undefined if not found
 38 |  */
 39 | export function getToolOutputValidator(
 40 |   toolName: string,
 41 | ): ValidateFunction | undefined {
 42 |   return toolOutputValidators.get(toolName);
 43 | }
 44 | 
 45 | /**
 46 |  * Validates structured content against a tool's output schema
 47 |  * Returns validation result with detailed error messages
 48 |  * @param toolName Name of the tool
 49 |  * @param structuredContent The structured content to validate
 50 |  * @returns An object with isValid boolean and optional error message
 51 |  */
 52 | export function validateToolOutput(
 53 |   toolName: string,
 54 |   structuredContent: unknown,
 55 | ): { isValid: boolean; error?: string } {
 56 |   const validator = getToolOutputValidator(toolName);
 57 |   if (!validator) {
 58 |     return { isValid: true }; // No validator means no schema to validate against
 59 |   }
 60 | 
 61 |   const isValid = validator(structuredContent);
 62 |   if (!isValid) {
 63 |     return {
 64 |       isValid: false,
 65 |       error: ajv.errorsText(validator.errors),
 66 |     };
 67 |   }
 68 | 
 69 |   return { isValid: true };
 70 | }
 71 | 
 72 | /**
 73 |  * Checks if a tool has an output schema
 74 |  * @param toolName Name of the tool
 75 |  * @returns true if the tool has an output schema
 76 |  */
 77 | export function hasOutputSchema(toolName: string): boolean {
 78 |   return toolOutputValidators.has(toolName);
 79 | }
 80 | 
 81 | /**
 82 |  * Generates a default value based on a JSON schema type
 83 |  * @param schema The JSON schema definition
 84 |  * @param propertyName Optional property name for checking if it's required in parent schema
 85 |  * @param parentSchema Optional parent schema to check required array
 86 |  * @returns A default value matching the schema type
 87 |  */
 88 | export function generateDefaultValue(
 89 |   schema: JsonSchemaType,
 90 |   propertyName?: string,
 91 |   parentSchema?: JsonSchemaType,
 92 | ): JsonValue {
 93 |   if ("default" in schema && schema.default !== undefined) {
 94 |     return schema.default;
 95 |   }
 96 | 
 97 |   // Check if this property is required in the parent schema
 98 |   const isRequired =
 99 |     propertyName && parentSchema
100 |       ? isPropertyRequired(propertyName, parentSchema)
101 |       : false;
102 | 
103 |   switch (schema.type) {
104 |     case "string":
105 |       return isRequired ? "" : undefined;
106 |     case "number":
107 |     case "integer":
108 |       return isRequired ? 0 : undefined;
109 |     case "boolean":
110 |       return isRequired ? false : undefined;
111 |     case "array":
112 |       return [];
113 |     case "object": {
114 |       if (!schema.properties) return {};
115 | 
116 |       const obj: JsonObject = {};
117 |       // Only include properties that are required according to the schema's required array
118 |       Object.entries(schema.properties).forEach(([key, prop]) => {
119 |         if (isPropertyRequired(key, schema)) {
120 |           const value = generateDefaultValue(prop, key, schema);
121 |           if (value !== undefined) {
122 |             obj[key] = value;
123 |           }
124 |         }
125 |       });
126 |       return obj;
127 |     }
128 |     case "null":
129 |       return null;
130 |     default:
131 |       return undefined;
132 |   }
133 | }
134 | 
135 | /**
136 |  * Helper function to check if a property is required in a schema
137 |  * @param propertyName The name of the property to check
138 |  * @param schema The parent schema containing the required array
139 |  * @returns true if the property is required, false otherwise
140 |  */
141 | export function isPropertyRequired(
142 |   propertyName: string,
143 |   schema: JsonSchemaType,
144 | ): boolean {
145 |   return schema.required?.includes(propertyName) ?? false;
146 | }
147 | 
148 | /**
149 |  * Formats a field key into a human-readable label
150 |  * @param key The field key to format
151 |  * @returns A formatted label string
152 |  */
153 | export function formatFieldLabel(key: string): string {
154 |   return key
155 |     .replace(/([A-Z])/g, " $1") // Insert space before capital letters
156 |     .replace(/_/g, " ") // Replace underscores with spaces
157 |     .replace(/^\w/, (c) => c.toUpperCase()); // Capitalize first letter
158 | }
159 | 


--------------------------------------------------------------------------------
/client/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | /// <reference types="vite/client" />
2 | 


--------------------------------------------------------------------------------
/client/tailwind.config.js:
--------------------------------------------------------------------------------
 1 | /** @type {import('tailwindcss').Config} */
 2 | import animate from "tailwindcss-animate";
 3 | export default {
 4 |   darkMode: ["class"],
 5 |   content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
 6 |   theme: {
 7 |     extend: {
 8 |       borderRadius: {
 9 |         lg: "var(--radius)",
10 |         md: "calc(var(--radius) - 2px)",
11 |         sm: "calc(var(--radius) - 4px)",
12 |       },
13 |       colors: {
14 |         background: "hsl(var(--background))",
15 |         foreground: "hsl(var(--foreground))",
16 |         card: {
17 |           DEFAULT: "hsl(var(--card))",
18 |           foreground: "hsl(var(--card-foreground))",
19 |         },
20 |         popover: {
21 |           DEFAULT: "hsl(var(--popover))",
22 |           foreground: "hsl(var(--popover-foreground))",
23 |         },
24 |         primary: {
25 |           DEFAULT: "hsl(var(--primary))",
26 |           foreground: "hsl(var(--primary-foreground))",
27 |         },
28 |         secondary: {
29 |           DEFAULT: "hsl(var(--secondary))",
30 |           foreground: "hsl(var(--secondary-foreground))",
31 |         },
32 |         muted: {
33 |           DEFAULT: "hsl(var(--muted))",
34 |           foreground: "hsl(var(--muted-foreground))",
35 |         },
36 |         accent: {
37 |           DEFAULT: "hsl(var(--accent))",
38 |           foreground: "hsl(var(--accent-foreground))",
39 |         },
40 |         destructive: {
41 |           DEFAULT: "hsl(var(--destructive))",
42 |           foreground: "hsl(var(--destructive-foreground))",
43 |         },
44 |         border: "hsl(var(--border))",
45 |         input: "hsl(var(--input))",
46 |         ring: "hsl(var(--ring))",
47 |         chart: {
48 |           1: "hsl(var(--chart-1))",
49 |           2: "hsl(var(--chart-2))",
50 |           3: "hsl(var(--chart-3))",
51 |           4: "hsl(var(--chart-4))",
52 |           5: "hsl(var(--chart-5))",
53 |         },
54 |       },
55 |     },
56 |   },
57 |   plugins: [animate],
58 | };
59 | 


--------------------------------------------------------------------------------
/client/tsconfig.app.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "baseUrl": ".",
 4 |     "paths": {
 5 |       "@/*": ["./src/*"]
 6 |     },
 7 | 
 8 |     "target": "ES2020",
 9 |     "useDefineForClassFields": true,
10 |     "lib": ["ES2020", "DOM", "DOM.Iterable"],
11 |     "module": "ESNext",
12 |     "skipLibCheck": true,
13 | 
14 |     /* Bundler mode */
15 |     "moduleResolution": "bundler",
16 |     "allowImportingTsExtensions": true,
17 |     "isolatedModules": true,
18 |     "moduleDetection": "force",
19 |     "noEmit": true,
20 |     "jsx": "react-jsx",
21 | 
22 |     /* Linting */
23 |     "strict": true,
24 |     "noUnusedLocals": true,
25 |     "noUnusedParameters": true,
26 |     "noFallthroughCasesInSwitch": true,
27 |     "resolveJsonModule": true,
28 |     "types": ["jest", "@testing-library/jest-dom", "node"]
29 |   },
30 |   "include": ["src"]
31 | }
32 | 


--------------------------------------------------------------------------------
/client/tsconfig.jest.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "extends": "./tsconfig.app.json",
 3 |   "compilerOptions": {
 4 |     "jsx": "react-jsx",
 5 |     "esModuleInterop": true,
 6 |     "module": "ESNext",
 7 |     "moduleResolution": "node"
 8 |   },
 9 |   "include": ["src"]
10 | }
11 | 


--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "files": [],
 3 |   "references": [
 4 |     { "path": "./tsconfig.app.json" },
 5 |     { "path": "./tsconfig.node.json" }
 6 |   ],
 7 |   "compilerOptions": {
 8 |     "baseUrl": ".",
 9 |     "paths": {
10 |       "@/*": ["./src/*"]
11 |     }
12 |   }
13 | }
14 | 


--------------------------------------------------------------------------------
/client/tsconfig.node.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "lib": ["ES2023"],
 5 |     "module": "ESNext",
 6 |     "skipLibCheck": true,
 7 | 
 8 |     /* Bundler mode */
 9 |     "moduleResolution": "bundler",
10 |     "allowImportingTsExtensions": true,
11 |     "isolatedModules": true,
12 |     "moduleDetection": "force",
13 |     "noEmit": true,
14 | 
15 |     /* Linting */
16 |     "strict": true,
17 |     "noUnusedLocals": true,
18 |     "noUnusedParameters": true,
19 |     "noFallthroughCasesInSwitch": true
20 |   },
21 |   "include": ["vite.config.ts"]
22 | }
23 | 


--------------------------------------------------------------------------------
/client/vite.config.ts:
--------------------------------------------------------------------------------
 1 | import react from "@vitejs/plugin-react";
 2 | import path from "path";
 3 | import { defineConfig } from "vite";
 4 | 
 5 | // https://vitejs.dev/config/
 6 | export default defineConfig({
 7 |   plugins: [react()],
 8 |   server: {
 9 |     host: true,
10 |   },
11 |   resolve: {
12 |     alias: {
13 |       "@": path.resolve(__dirname, "./src"),
14 |     },
15 |   },
16 |   build: {
17 |     minify: false,
18 |     rollupOptions: {
19 |       output: {
20 |         manualChunks: undefined,
21 |       },
22 |     },
23 |   },
24 | });
25 | 


--------------------------------------------------------------------------------
/mcp-inspector.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/modelcontextprotocol/inspector/1ea8e9a9395386b0e77efaa64ccaa4c28c571d10/mcp-inspector.png


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "@modelcontextprotocol/inspector",
 3 |   "version": "0.16.1",
 4 |   "description": "Model Context Protocol inspector",
 5 |   "license": "MIT",
 6 |   "author": "Anthropic, PBC (https://anthropic.com)",
 7 |   "homepage": "https://modelcontextprotocol.io",
 8 |   "bugs": "https://github.com/modelcontextprotocol/inspector/issues",
 9 |   "type": "module",
10 |   "bin": {
11 |     "mcp-inspector": "cli/build/cli.js"
12 |   },
13 |   "files": [
14 |     "client/bin",
15 |     "client/dist",
16 |     "server/build",
17 |     "cli/build"
18 |   ],
19 |   "workspaces": [
20 |     "client",
21 |     "server",
22 |     "cli"
23 |   ],
24 |   "scripts": {
25 |     "build": "npm run build-server && npm run build-client && npm run build-cli",
26 |     "build-server": "cd server && npm run build",
27 |     "build-client": "cd client && npm run build",
28 |     "build-cli": "cd cli && npm run build",
29 |     "clean": "rimraf ./node_modules ./client/node_modules ./cli/node_modules ./build ./client/dist ./server/build ./cli/build ./package-lock.json && npm install",
30 |     "dev": "node client/bin/start.js --dev",
31 |     "dev:windows": "node client/bin/start.js --dev",
32 |     "dev:sdk": "npm run link:sdk && concurrently \"npm run dev\" \"cd sdk && npm run build:esm:w\"",
33 |     "link:sdk": "(test -d sdk || ln -sf ${MCP_SDK:-$PWD/../typescript-sdk} sdk) && (cd sdk && npm link && (test -d node_modules || npm i)) && npm link @modelcontextprotocol/sdk",
34 |     "unlink:sdk": "(cd sdk && npm unlink -g) && rm sdk && npm unlink @modelcontextprotocol/sdk",
35 |     "start": "node client/bin/start.js",
36 |     "start-server": "cd server && npm run start",
37 |     "start-client": "cd client && npm run preview",
38 |     "test": "npm run prettier-check && cd client && npm test",
39 |     "test-cli": "cd cli && npm run test",
40 |     "test:e2e": "MCP_AUTO_OPEN_ENABLED=false npm run test:e2e --workspace=client",
41 |     "prettier-fix": "prettier --write .",
42 |     "prettier-check": "prettier --check .",
43 |     "lint": "prettier --check . && cd client && npm run lint",
44 |     "prepare": "npm run build",
45 |     "publish-all": "npm publish --workspaces --access public && npm publish --access public",
46 |     "update-version": "node scripts/update-version.js",
47 |     "check-version": "node scripts/check-version-consistency.js"
48 |   },
49 |   "dependencies": {
50 |     "@modelcontextprotocol/inspector-cli": "^0.16.1",
51 |     "@modelcontextprotocol/inspector-client": "^0.16.1",
52 |     "@modelcontextprotocol/inspector-server": "^0.16.1",
53 |     "@modelcontextprotocol/sdk": "^1.13.1",
54 |     "concurrently": "^9.0.1",
55 |     "open": "^10.1.0",
56 |     "shell-quote": "^1.8.2",
57 |     "spawn-rx": "^5.1.2",
58 |     "ts-node": "^10.9.2",
59 |     "zod": "^3.23.8"
60 |   },
61 |   "devDependencies": {
62 |     "@playwright/test": "^1.52.0",
63 |     "@types/jest": "^29.5.14",
64 |     "@types/node": "^22.7.5",
65 |     "@types/shell-quote": "^1.7.5",
66 |     "jest-fixed-jsdom": "^0.0.9",
67 |     "prettier": "3.3.3",
68 |     "rimraf": "^6.0.1",
69 |     "typescript": "^5.4.2"
70 |   },
71 |   "engines": {
72 |     "node": ">=22.7.5"
73 |   }
74 | }
75 | 


--------------------------------------------------------------------------------
/sample-config.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "mcpServers": {
 3 |     "everything": {
 4 |       "command": "npx",
 5 |       "args": ["@modelcontextprotocol/server-everything"],
 6 |       "env": {
 7 |         "HELLO": "Hello MCP!"
 8 |       }
 9 |     },
10 |     "myserver": {
11 |       "command": "node",
12 |       "args": ["build/index.js", "arg1", "arg2"],
13 |       "env": {
14 |         "KEY": "value",
15 |         "KEY2": "value2"
16 |       }
17 |     }
18 |   }
19 | }
20 | 


--------------------------------------------------------------------------------
/scripts/README.md:
--------------------------------------------------------------------------------
 1 | # Version Management Scripts
 2 | 
 3 | This directory contains scripts for managing version consistency across the monorepo.
 4 | 
 5 | ## Scripts
 6 | 
 7 | ### update-version.js
 8 | 
 9 | Updates the version across all package.json files in the monorepo and updates package-lock.json.
10 | 
11 | **Usage:**
12 | 
13 | ```bash
14 | npm run update-version <new-version>
15 | # Example:
16 | npm run update-version 0.14.3
17 | ```
18 | 
19 | This script will:
20 | 
21 | 1. Update the version in all package.json files (root, client, server, cli)
22 | 2. Update workspace dependencies in the root package.json
23 | 3. Run `npm install` to update package-lock.json
24 | 4. Provide next steps for committing and tagging
25 | 
26 | ### check-version-consistency.js
27 | 
28 | Checks that all packages have consistent versions and that package-lock.json is up to date.
29 | 
30 | **Usage:**
31 | 
32 | ```bash
33 | npm run check-version
34 | ```
35 | 
36 | This script checks:
37 | 
38 | 1. All package.json files have the same version
39 | 2. Workspace dependencies in root package.json match the current version
40 | 3. package-lock.json version matches package.json
41 | 4. Workspace packages in package-lock.json have the correct versions
42 | 
43 | This check runs automatically in CI on every PR and push to main.
44 | 
45 | ## CI Integration
46 | 
47 | The version consistency check is integrated into the GitHub Actions workflow (`.github/workflows/main.yml`) and will fail the build if:
48 | 
49 | - Package versions are inconsistent
50 | - package-lock.json is out of sync
51 | 
52 | ## Common Workflows
53 | 
54 | ### Bumping version for a release:
55 | 
56 | ```bash
57 | # Update to new version
58 | npm run update-version 0.15.0
59 | 
60 | # Verify everything is correct
61 | npm run check-version
62 | 
63 | # Commit the changes
64 | git add -A
65 | git commit -m "chore: bump version to 0.15.0"
66 | 
67 | # Create a tag
68 | git tag 0.15.0
69 | 
70 | # Push changes and tag
71 | git push && git push --tags
72 | ```
73 | 


--------------------------------------------------------------------------------
/scripts/check-version-consistency.js:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env node
  2 | 
  3 | import fs from "fs";
  4 | import path from "path";
  5 | import { fileURLToPath } from "url";
  6 | 
  7 | const __filename = fileURLToPath(import.meta.url);
  8 | const __dirname = path.dirname(__filename);
  9 | 
 10 | /**
 11 |  * Checks version consistency across all package.json files in the monorepo
 12 |  * Exits with code 1 if versions are inconsistent
 13 |  * Usage: node scripts/check-version-consistency.js
 14 |  */
 15 | 
 16 | console.log("🔍 Checking version consistency across packages...\n");
 17 | 
 18 | // List of package.json files to check
 19 | const packagePaths = [
 20 |   "package.json",
 21 |   "client/package.json",
 22 |   "server/package.json",
 23 |   "cli/package.json",
 24 | ];
 25 | 
 26 | const versions = new Map();
 27 | const errors = [];
 28 | 
 29 | // Read version from each package.json
 30 | packagePaths.forEach((packagePath) => {
 31 |   const fullPath = path.join(__dirname, "..", packagePath);
 32 | 
 33 |   if (!fs.existsSync(fullPath)) {
 34 |     console.warn(`⚠️  Skipping ${packagePath} - file not found`);
 35 |     return;
 36 |   }
 37 | 
 38 |   try {
 39 |     const packageJson = JSON.parse(fs.readFileSync(fullPath, "utf8"));
 40 |     const version = packageJson.version;
 41 |     const packageName = packageJson.name || packagePath;
 42 | 
 43 |     versions.set(packagePath, {
 44 |       name: packageName,
 45 |       version: version,
 46 |       dependencies: packageJson.dependencies || {},
 47 |     });
 48 | 
 49 |     console.log(`📦 ${packagePath}:`);
 50 |     console.log(`   Name: ${packageName}`);
 51 |     console.log(`   Version: ${version}`);
 52 |   } catch (error) {
 53 |     errors.push(`Failed to read ${packagePath}: ${error.message}`);
 54 |   }
 55 | });
 56 | 
 57 | if (errors.length > 0) {
 58 |   console.error("\n❌ Errors occurred while reading package files:");
 59 |   errors.forEach((error) => console.error(`   - ${error}`));
 60 |   process.exit(1);
 61 | }
 62 | 
 63 | // Check if all versions match
 64 | const allVersions = Array.from(versions.values()).map((v) => v.version);
 65 | const uniqueVersions = [...new Set(allVersions)];
 66 | 
 67 | console.log("\n📊 Version Summary:");
 68 | console.log(`   Total packages: ${versions.size}`);
 69 | console.log(`   Unique versions: ${uniqueVersions.length}`);
 70 | 
 71 | if (uniqueVersions.length > 1) {
 72 |   console.error("\n❌ Version mismatch detected!");
 73 |   console.error("   Found versions: " + uniqueVersions.join(", "));
 74 | 
 75 |   console.error("\n   Package versions:");
 76 |   versions.forEach((info, path) => {
 77 |     console.error(`   - ${path}: ${info.version}`);
 78 |   });
 79 | } else {
 80 |   console.log(`   ✅ All packages are at version: ${uniqueVersions[0]}`);
 81 | }
 82 | 
 83 | // Check workspace dependencies in root package.json
 84 | const rootPackage = versions.get("package.json");
 85 | if (rootPackage) {
 86 |   console.log("\n🔗 Checking workspace dependencies...");
 87 |   const expectedVersion = rootPackage.version;
 88 |   let dependencyErrors = false;
 89 | 
 90 |   Object.entries(rootPackage.dependencies).forEach(([dep, version]) => {
 91 |     if (dep.startsWith("@modelcontextprotocol/inspector-")) {
 92 |       const expectedDepVersion = `^${expectedVersion}`;
 93 |       if (version !== expectedDepVersion) {
 94 |         console.error(
 95 |           `   ❌ ${dep}: ${version} (expected ${expectedDepVersion})`,
 96 |         );
 97 |         dependencyErrors = true;
 98 |       } else {
 99 |         console.log(`   ✅ ${dep}: ${version}`);
100 |       }
101 |     }
102 |   });
103 | 
104 |   if (dependencyErrors) {
105 |     errors.push("Workspace dependency versions do not match package versions");
106 |   }
107 | }
108 | 
109 | // Check if package-lock.json is up to date
110 | console.log("\n🔒 Checking package-lock.json...");
111 | const lockPath = path.join(__dirname, "..", "package-lock.json");
112 | let lockFileError = false;
113 | 
114 | if (!fs.existsSync(lockPath)) {
115 |   console.error("   ❌ package-lock.json not found");
116 |   lockFileError = true;
117 | } else {
118 |   try {
119 |     const lockFile = JSON.parse(fs.readFileSync(lockPath, "utf8"));
120 |     const lockVersion = lockFile.version;
121 |     const expectedVersion = rootPackage?.version || uniqueVersions[0];
122 | 
123 |     if (lockVersion !== expectedVersion) {
124 |       console.error(
125 |         `   ❌ package-lock.json version (${lockVersion}) does not match package.json version (${expectedVersion})`,
126 |       );
127 |       lockFileError = true;
128 |     } else {
129 |       console.log(`   ✅ package-lock.json version matches: ${lockVersion}`);
130 |     }
131 | 
132 |     // Check workspace package versions in lock file
133 |     if (lockFile.packages) {
134 |       const workspacePackages = [
135 |         { path: "client", name: "@modelcontextprotocol/inspector-client" },
136 |         { path: "server", name: "@modelcontextprotocol/inspector-server" },
137 |         { path: "cli", name: "@modelcontextprotocol/inspector-cli" },
138 |       ];
139 | 
140 |       workspacePackages.forEach(({ path, name }) => {
141 |         const lockPkgPath = lockFile.packages[path];
142 |         if (lockPkgPath && lockPkgPath.version !== expectedVersion) {
143 |           console.error(
144 |             `   ❌ ${name} in lock file: ${lockPkgPath.version} (expected ${expectedVersion})`,
145 |           );
146 |           lockFileError = true;
147 |         }
148 |       });
149 |     }
150 |   } catch (error) {
151 |     console.error(`   ❌ Failed to parse package-lock.json: ${error.message}`);
152 |     lockFileError = true;
153 |   }
154 | }
155 | 
156 | // Final result
157 | console.log("\n🎯 Result:");
158 | if (uniqueVersions.length === 1 && errors.length === 0 && !lockFileError) {
159 |   console.log("   ✅ Version consistency check passed!");
160 |   process.exit(0);
161 | } else {
162 |   console.error("   ❌ Version consistency check failed!");
163 |   if (uniqueVersions.length > 1) {
164 |     console.error("   - Package versions are not consistent");
165 |   }
166 |   if (errors.length > 0) {
167 |     console.error("   - " + errors.join("\n   - "));
168 |   }
169 |   if (lockFileError) {
170 |     console.error("   - package-lock.json is out of sync");
171 |   }
172 |   console.error(
173 |     '\n💡 Run "npm run update-version <new-version>" to fix version inconsistencies',
174 |   );
175 |   console.error('   or run "npm install" to update package-lock.json');
176 |   process.exit(1);
177 | }
178 | 


--------------------------------------------------------------------------------
/scripts/update-version.js:
--------------------------------------------------------------------------------
  1 | #!/usr/bin/env node
  2 | 
  3 | import fs from "fs";
  4 | import path from "path";
  5 | import { execSync } from "child_process";
  6 | import { fileURLToPath } from "url";
  7 | 
  8 | const __filename = fileURLToPath(import.meta.url);
  9 | const __dirname = path.dirname(__filename);
 10 | 
 11 | /**
 12 |  * Updates version across all package.json files in the monorepo
 13 |  * Usage: node scripts/update-version.js <new-version>
 14 |  * Example: node scripts/update-version.js 0.14.2
 15 |  */
 16 | 
 17 | const newVersion = process.argv[2];
 18 | 
 19 | if (!newVersion) {
 20 |   console.error("❌ Please provide a version number");
 21 |   console.error("Usage: node scripts/update-version.js <new-version>");
 22 |   console.error("Example: node scripts/update-version.js 0.14.2");
 23 |   process.exit(1);
 24 | }
 25 | 
 26 | // Validate version format
 27 | const versionRegex = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
 28 | if (!versionRegex.test(newVersion)) {
 29 |   console.error(
 30 |     "❌ Invalid version format. Please use semantic versioning (e.g., 1.2.3 or 1.2.3-beta.1)",
 31 |   );
 32 |   process.exit(1);
 33 | }
 34 | 
 35 | console.log(`🔄 Updating all packages to version ${newVersion}...`);
 36 | 
 37 | // List of package.json files to update
 38 | const packagePaths = [
 39 |   "package.json",
 40 |   "client/package.json",
 41 |   "server/package.json",
 42 |   "cli/package.json",
 43 | ];
 44 | 
 45 | const updatedFiles = [];
 46 | 
 47 | // Update version in each package.json
 48 | packagePaths.forEach((packagePath) => {
 49 |   const fullPath = path.join(__dirname, "..", packagePath);
 50 | 
 51 |   if (!fs.existsSync(fullPath)) {
 52 |     console.warn(`⚠️  Skipping ${packagePath} - file not found`);
 53 |     return;
 54 |   }
 55 | 
 56 |   try {
 57 |     const packageJson = JSON.parse(fs.readFileSync(fullPath, "utf8"));
 58 |     const oldVersion = packageJson.version;
 59 |     packageJson.version = newVersion;
 60 | 
 61 |     // Update workspace dependencies in root package.json
 62 |     if (packagePath === "package.json" && packageJson.dependencies) {
 63 |       Object.keys(packageJson.dependencies).forEach((dep) => {
 64 |         if (dep.startsWith("@modelcontextprotocol/inspector-")) {
 65 |           packageJson.dependencies[dep] = `^${newVersion}`;
 66 |         }
 67 |       });
 68 |     }
 69 | 
 70 |     fs.writeFileSync(fullPath, JSON.stringify(packageJson, null, 2) + "\n");
 71 |     updatedFiles.push(packagePath);
 72 |     console.log(
 73 |       `✅ Updated ${packagePath} from ${oldVersion} to ${newVersion}`,
 74 |     );
 75 |   } catch (error) {
 76 |     console.error(`❌ Failed to update ${packagePath}:`, error.message);
 77 |     process.exit(1);
 78 |   }
 79 | });
 80 | 
 81 | console.log("\n📝 Summary:");
 82 | console.log(`Updated ${updatedFiles.length} files to version ${newVersion}`);
 83 | 
 84 | // Update package-lock.json
 85 | console.log("\n🔒 Updating package-lock.json...");
 86 | try {
 87 |   execSync("npm install", { stdio: "inherit" });
 88 |   console.log("✅ package-lock.json updated successfully");
 89 | } catch (error) {
 90 |   console.error("❌ Failed to update package-lock.json:", error.message);
 91 |   console.error('Please run "npm install" manually');
 92 |   process.exit(1);
 93 | }
 94 | 
 95 | console.log("\n✨ Version update complete!");
 96 | console.log("\nNext steps:");
 97 | console.log("1. Review the changes: git diff");
 98 | console.log(
 99 |   '2. Commit the changes: git add -A && git commit -m "chore: bump version to ' +
100 |     newVersion +
101 |     '"',
102 | );
103 | console.log("3. Create a git tag: git tag v" + newVersion);
104 | console.log("4. Push changes and tag: git push && git push --tags");
105 | 


--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "@modelcontextprotocol/inspector-server",
 3 |   "version": "0.16.1",
 4 |   "description": "Server-side application for the Model Context Protocol inspector",
 5 |   "license": "MIT",
 6 |   "author": "Anthropic, PBC (https://anthropic.com)",
 7 |   "homepage": "https://modelcontextprotocol.io",
 8 |   "bugs": "https://github.com/modelcontextprotocol/inspector/issues",
 9 |   "type": "module",
10 |   "bin": {
11 |     "mcp-inspector-server": "build/index.js"
12 |   },
13 |   "files": [
14 |     "build"
15 |   ],
16 |   "scripts": {
17 |     "build": "tsc",
18 |     "start": "node build/index.js",
19 |     "dev": "tsx watch --clear-screen=false src/index.ts",
20 |     "dev:windows": "tsx watch --clear-screen=false src/index.ts < NUL"
21 |   },
22 |   "devDependencies": {
23 |     "@types/cors": "^2.8.17",
24 |     "@types/express": "^4.17.21",
25 |     "@types/ws": "^8.5.12",
26 |     "tsx": "^4.19.0",
27 |     "typescript": "^5.6.2"
28 |   },
29 |   "dependencies": {
30 |     "@modelcontextprotocol/sdk": "^1.13.1",
31 |     "cors": "^2.8.5",
32 |     "express": "^5.1.0",
33 |     "ws": "^8.18.0",
34 |     "zod": "^3.23.8"
35 |   }
36 | }
37 | 


--------------------------------------------------------------------------------
/server/src/mcpProxy.ts:
--------------------------------------------------------------------------------
 1 | import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
 2 | import { isJSONRPCRequest } from "@modelcontextprotocol/sdk/types.js";
 3 | 
 4 | function onClientError(error: Error) {
 5 |   console.error("Error from inspector client:", error);
 6 | }
 7 | 
 8 | function onServerError(error: Error) {
 9 |   if (error?.cause && JSON.stringify(error.cause).includes("ECONNREFUSED")) {
10 |     console.error("Connection refused. Is the MCP server running?");
11 |   } else if (error.message && error.message.includes("404")) {
12 |     console.error("Error accessing endpoint (HTTP 404)");
13 |   } else {
14 |     console.error("Error from MCP server:", error);
15 |   }
16 | }
17 | 
18 | export default function mcpProxy({
19 |   transportToClient,
20 |   transportToServer,
21 | }: {
22 |   transportToClient: Transport;
23 |   transportToServer: Transport;
24 | }) {
25 |   let transportToClientClosed = false;
26 |   let transportToServerClosed = false;
27 | 
28 |   let reportedServerSession = false;
29 | 
30 |   transportToClient.onmessage = (message) => {
31 |     transportToServer.send(message).catch((error) => {
32 |       // Send error response back to client if it was a request (has id) and connection is still open
33 |       if (isJSONRPCRequest(message) && !transportToClientClosed) {
34 |         const errorResponse = {
35 |           jsonrpc: "2.0" as const,
36 |           id: message.id,
37 |           error: {
38 |             code: -32001,
39 |             message: error.message,
40 |             data: error,
41 |           },
42 |         };
43 |         transportToClient.send(errorResponse).catch(onClientError);
44 |       }
45 |     });
46 |   };
47 | 
48 |   transportToServer.onmessage = (message) => {
49 |     if (!reportedServerSession) {
50 |       if (transportToServer.sessionId) {
51 |         // Can only report for StreamableHttp
52 |         console.error(
53 |           "Proxy  <-> Server sessionId: " + transportToServer.sessionId,
54 |         );
55 |       }
56 |       reportedServerSession = true;
57 |     }
58 |     transportToClient.send(message).catch(onClientError);
59 |   };
60 | 
61 |   transportToClient.onclose = () => {
62 |     if (transportToServerClosed) {
63 |       return;
64 |     }
65 | 
66 |     transportToClientClosed = true;
67 |     transportToServer.close().catch(onServerError);
68 |   };
69 | 
70 |   transportToServer.onclose = () => {
71 |     if (transportToClientClosed) {
72 |       return;
73 |     }
74 |     transportToServerClosed = true;
75 |     transportToClient.close().catch(onClientError);
76 |   };
77 | 
78 |   transportToClient.onerror = onClientError;
79 |   transportToServer.onerror = onServerError;
80 | }
81 | 


--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "Node16",
 5 |     "moduleResolution": "Node16",
 6 |     "outDir": "./build",
 7 |     "rootDir": "./src",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true,
12 |     "resolveJsonModule": true
13 |   },
14 |   "include": ["src/**/*"],
15 |   "exclude": ["node_modules", "packages", "**/*.spec.ts"]
16 | }
17 | 


--------------------------------------------------------------------------------