```
├── .dockerignore (100 tokens)
├── .git-blame-ignore-revs
├── .gitattributes (omitted)
├── .github/
   ├── workflows/
      ├── e2e_tests.yml (500 tokens)
      ├── main.yml (600 tokens)
├── .gitignore
├── .node-version
├── .npmrc
├── .prettierignore (omitted)
├── .prettierrc
├── CLAUDE.md (200 tokens)
├── CODE_OF_CONDUCT.md (1000 tokens)
├── CONTRIBUTING.md (300 tokens)
├── Dockerfile (200 tokens)
├── LICENSE (omitted)
├── README.md (3.1k tokens)
├── SECURITY.md (200 tokens)
├── cli/
   ├── package.json (100 tokens)
   ├── scripts/
      ├── cli-tests.js (3.7k tokens)
      ├── make-executable.js (200 tokens)
   ├── src/
      ├── cli.ts (1400 tokens)
      ├── client/
         ├── connection.ts (300 tokens)
         ├── index.ts
         ├── prompts.ts (200 tokens)
         ├── resources.ts (200 tokens)
         ├── tools.ts (500 tokens)
         ├── types.ts
      ├── error-handler.ts (100 tokens)
      ├── index.ts (1600 tokens)
      ├── transport.ts (500 tokens)
   ├── tsconfig.json (100 tokens)
├── client/
   ├── .gitignore (100 tokens)
   ├── README.md (300 tokens)
   ├── bin/
      ├── client.js (300 tokens)
      ├── start.js (1400 tokens)
   ├── components.json (100 tokens)
   ├── e2e/
      ├── global-teardown.js (100 tokens)
      ├── transport-type-dropdown.spec.ts (800 tokens)
   ├── eslint.config.js (100 tokens)
   ├── index.html (100 tokens)
   ├── jest.config.cjs (200 tokens)
   ├── package.json (500 tokens)
   ├── playwright.config.ts (400 tokens)
   ├── postcss.config.js
   ├── public/
      ├── mcp.svg (200 tokens)
   ├── src/
      ├── App.css (100 tokens)
      ├── App.tsx (6.1k tokens)
      ├── __mocks__/
         ├── styleMock.js
      ├── __tests__/
         ├── App.config.test.tsx (1300 tokens)
      ├── components/
         ├── AuthDebugger.tsx (1900 tokens)
         ├── ConsoleTab.tsx (100 tokens)
         ├── DynamicJsonForm.tsx (3.1k tokens)
         ├── HistoryAndNotifications.tsx (1100 tokens)
         ├── JsonEditor.tsx (300 tokens)
         ├── JsonView.tsx (1700 tokens)
         ├── ListPane.tsx (300 tokens)
         ├── OAuthCallback.tsx (500 tokens)
         ├── OAuthDebugCallback.tsx (700 tokens)
         ├── OAuthFlowProgress.tsx (2.6k tokens)
         ├── PingTab.tsx (100 tokens)
         ├── PromptsTab.tsx (1100 tokens)
         ├── ResourcesTab.tsx (2000 tokens)
         ├── RootsTab.tsx (400 tokens)
         ├── SamplingRequest.tsx (900 tokens)
         ├── SamplingTab.tsx (300 tokens)
         ├── Sidebar.tsx (5.8k tokens)
         ├── ToolResults.tsx (1400 tokens)
         ├── ToolsTab.tsx (2.2k tokens)
         ├── __tests__/
            ├── AuthDebugger.test.tsx (3.8k tokens)
            ├── DynamicJsonForm.array.test.tsx (1700 tokens)
            ├── DynamicJsonForm.test.tsx (1300 tokens)
            ├── HistoryAndNotifications.test.tsx (1400 tokens)
            ├── ResourcesTab.test.tsx (1900 tokens)
            ├── Sidebar.test.tsx (5.3k tokens)
            ├── ToolsTab.test.tsx (2.2k tokens)
            ├── samplingRequest.test.tsx (400 tokens)
            ├── samplingTab.test.tsx (300 tokens)
         ├── ui/
            ├── alert.tsx (300 tokens)
            ├── button.tsx (400 tokens)
            ├── checkbox.tsx (200 tokens)
            ├── combobox.tsx (500 tokens)
            ├── command.tsx (1000 tokens)
            ├── dialog.tsx (800 tokens)
            ├── input.tsx (200 tokens)
            ├── label.tsx (100 tokens)
            ├── popover.tsx (300 tokens)
            ├── select.tsx (1100 tokens)
            ├── tabs.tsx (400 tokens)
            ├── textarea.tsx (100 tokens)
            ├── toast.tsx (1000 tokens)
            ├── toaster.tsx (200 tokens)
            ├── tooltip.tsx (200 tokens)
      ├── index.css (500 tokens)
      ├── lib/
         ├── auth-types.ts (300 tokens)
         ├── auth.ts (800 tokens)
         ├── configurationTypes.ts (300 tokens)
         ├── constants.ts (400 tokens)
         ├── hooks/
            ├── __tests__/
               ├── useConnection.test.tsx (2.7k tokens)
            ├── useCompletionState.ts (600 tokens)
            ├── useConnection.ts (3.4k tokens)
            ├── useDraggablePane.ts (600 tokens)
            ├── useTheme.ts (300 tokens)
            ├── useToast.ts (800 tokens)
         ├── notificationTypes.ts (100 tokens)
         ├── oauth-state-machine.ts (1300 tokens)
         ├── utils.ts
      ├── main.tsx (100 tokens)
      ├── utils/
         ├── __tests__/
            ├── configUtils.test.ts (400 tokens)
            ├── escapeUnicode.test.ts (200 tokens)
            ├── jsonUtils.test.ts (2.1k tokens)
            ├── oauthUtils.ts (500 tokens)
            ├── schemaUtils.test.ts (2k tokens)
         ├── configUtils.ts (1100 tokens)
         ├── escapeUnicode.ts (100 tokens)
         ├── jsonUtils.ts (900 tokens)
         ├── oauthUtils.ts (400 tokens)
         ├── schemaUtils.ts (1000 tokens)
      ├── vite-env.d.ts (omitted)
   ├── tailwind.config.js (300 tokens)
   ├── tsconfig.app.json (100 tokens)
   ├── tsconfig.jest.json
   ├── tsconfig.json
   ├── tsconfig.node.json (100 tokens)
   ├── vite.config.ts (100 tokens)
├── mcp-inspector.png
├── package-lock.json (omitted)
├── package.json (500 tokens)
├── sample-config.json (100 tokens)
├── scripts/
   ├── README.md (300 tokens)
   ├── check-version-consistency.js (1100 tokens)
   ├── update-version.js (600 tokens)
├── server/
   ├── package.json (200 tokens)
   ├── src/
      ├── index.ts (3.3k tokens)
      ├── mcpProxy.ts (500 tokens)
   ├── tsconfig.json (100 tokens)
```


## /.dockerignore

```dockerignore path="/.dockerignore" 
# Version control
.git
.gitignore

# Node.js
node_modules
npm-debug.log

# Build artifacts
client/dist
client/build
server/dist
server/build

# Environment variables
.env
.env.local
.env.development
.env.test
.env.production

# Editor files
.vscode
.idea

# Logs
logs
*.log

# Testing
coverage

# Docker
Dockerfile
.dockerignore
```

## /.git-blame-ignore-revs

```git-blame-ignore-revs path="/.git-blame-ignore-revs" 

```

## /.github/workflows/e2e_tests.yml

```yml path="/.github/workflows/e2e_tests.yml" 
name: Playwright Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    # Installing Playright dependencies can take quite awhile, and also depends on GitHub CI load.
    timeout-minutes: 15
    runs-on: ubuntu-latest

    steps:
      - name: Install dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y libwoff1

      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        id: setup_node
        with:
          node-version-file: package.json
          cache: npm

      # Cache Playwright browsers
      - name: Cache Playwright browsers
        id: cache-playwright
        uses: actions/cache@v4
        with:
          path: ~/.cache/ms-playwright # The default Playwright cache path
          key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }} # Cache key based on OS and package-lock.json
          restore-keys: |
            ${{ runner.os }}-playwright-

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright dependencies
        run: npx playwright install-deps

      - name: Install Playwright and browsers unless cached
        run: npx playwright install --with-deps
        if: steps.cache-playwright.outputs.cache-hit != 'true'

      - name: Run Playwright tests
        id: playwright-tests
        run: npm run test:e2e

      - name: Upload Playwright Report and Screenshots
        uses: actions/upload-artifact@v4
        if: steps.playwright-tests.conclusion != 'skipped'
        with:
          name: playwright-report
          path: |
            client/playwright-report/
            client/test-results/
            client/results.json
          retention-days: 2

      - name: Publish Playwright Test Summary
        uses: daun/playwright-report-summary@v3
        if: steps.playwright-tests.conclusion != 'skipped'
        with:
          create-comment: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
          report-file: client/results.json
          comment-title: "🎭 Playwright E2E Test Results"
          job-summary: true
          icon-style: "emojis"
          custom-info: |
            **Test Environment:** Ubuntu Latest, Node.js ${{ steps.setup_node.outputs.node-version }}
            **Browsers:** Chromium, Firefox

            📊 [View Detailed HTML Report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) (download artifacts)
          test-command: "npm run test:e2e"

```

## /.github/workflows/main.yml

```yml path="/.github/workflows/main.yml" 
on:
  push:
    branches:
      - main

  pull_request:
  release:
    types: [published]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Check formatting
        run: npx prettier --check .

      - uses: actions/setup-node@v4
        with:
          node-version-file: package.json
          cache: npm

      # Working around https://github.com/npm/cli/issues/4828
      # - run: npm ci
      - run: npm install --no-package-lock

      - name: Check version consistency
        run: npm run check-version

      - name: Check linting
        working-directory: ./client
        run: npm run lint

      - name: Run client tests
        working-directory: ./client
        run: npm test

      - run: npm run build

  publish:
    runs-on: ubuntu-latest
    if: github.event_name == 'release'
    environment: release
    needs: build

    permissions:
      contents: read
      id-token: write

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version-file: package.json
          cache: npm
          registry-url: "https://registry.npmjs.org"

      # Working around https://github.com/npm/cli/issues/4828
      # - run: npm ci
      - run: npm install --no-package-lock

      # TODO: Add --provenance once the repo is public
      - run: npm run publish-all
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

  publish-github-container-registry:
    runs-on: ubuntu-latest
    if: github.event_name == 'release'
    environment: release
    needs: build
    permissions:
      contents: read
      packages: write
      attestations: write
      id-token: write
    steps:
      - uses: actions/checkout@v4

      - name: Log in to the Container registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build and push Docker image
        id: push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          platforms: linux/amd64,linux/arm64
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

      - name: Generate artifact attestation
        uses: actions/attest-build-provenance@v2
        with:
          subject-name: ghcr.io/${{ github.repository }}
          subject-digest: ${{ steps.push.outputs.digest }}
          push-to-registry: true

```

## /.gitignore

```gitignore path="/.gitignore" 
.DS_Store
.vscode
.idea
node_modules/
*-workspace/
server/build
client/dist
client/tsconfig.app.tsbuildinfo
client/tsconfig.node.tsbuildinfo
cli/build
test-output
client/playwright-report/
client/results.json
client/test-results/


```

## /.node-version

```node-version path="/.node-version" 
22.x.x

```

## /.npmrc

```npmrc path="/.npmrc" 
registry="https://registry.npmjs.org/"
@modelcontextprotocol:registry="https://registry.npmjs.org/"

```

## /.prettierrc

```prettierrc path="/.prettierrc" 

```

## /CLAUDE.md

# MCP Inspector Development Guide

## Build Commands

- Build all: `npm run build`
- Build client: `npm run build-client`
- Build server: `npm run build-server`
- Development mode: `npm run dev` (use `npm run dev:windows` on Windows)
- Format code: `npm run prettier-fix`
- Client lint: `cd client && npm run lint`

## Code Style Guidelines

- Use TypeScript with proper type annotations
- Follow React functional component patterns with hooks
- Use ES modules (import/export) not CommonJS
- Use Prettier for formatting (auto-formatted on commit)
- Follow existing naming conventions:
  - camelCase for variables and functions
  - PascalCase for component names and types
  - kebab-case for file names
- Use async/await for asynchronous operations
- Implement proper error handling with try/catch blocks
- Use Tailwind CSS for styling in the client
- Keep components small and focused on a single responsibility

## Project Organization

The project is organized as a monorepo with workspaces:

- `client/`: React frontend with Vite, TypeScript and Tailwind
- `server/`: Express backend with TypeScript
- `cli/`: Command-line interface for testing and invoking MCP server methods directly


## /CODE_OF_CONDUCT.md

# Contributor Covenant Code of Conduct

## Our Pledge

We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.

## Our Standards

Examples of behavior that contributes to a positive environment for our
community include:

* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
  and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
  overall community

Examples of unacceptable behavior include:

* The use of sexualized language or imagery, and sexual attention or
  advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
  address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.

## Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
mcp-coc@anthropic.com.
All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.

## Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:

### 1. Correction

**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.

**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.

### 2. Warning

**Community Impact**: A violation through a single incident or series
of actions.

**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.

### 3. Temporary Ban

**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.

**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.

### 4. Permanent Ban

**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior,  harassment of an
individual, or aggression toward or disparagement of classes of individuals.

**Consequence**: A permanent ban from any sort of public interaction within
the community.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.

Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).

[homepage]: https://www.contributor-covenant.org

For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.


## /CONTRIBUTING.md

# Contributing to Model Context Protocol Inspector

Thanks for your interest in contributing! This guide explains how to get involved.

## Getting Started

1. Fork the repository and clone it locally
2. Install dependencies with `npm install`
3. Run `npm run dev` to start both client and server in development mode
4. Use the web UI at http://localhost:6274 to interact with the inspector

## Development Process & Pull Requests

1. Create a new branch for your changes
2. Make your changes following existing code style and conventions. You can run `npm run prettier-check` and `npm run prettier-fix` as applicable.
3. Test changes locally by running `npm test` and `npm run test:e2e`
4. Update documentation as needed
5. Use clear commit messages explaining your changes
6. Verify all changes work as expected
7. Submit a pull request
8. PRs will be reviewed by maintainers

## Code of Conduct

This project follows our [Code of Conduct](CODE_OF_CONDUCT.md). Please read it before contributing.

## Security

If you find a security vulnerability, please refer to our [Security Policy](SECURITY.md) for reporting instructions.

## Questions?

Feel free to [open an issue](https://github.com/modelcontextprotocol/mcp-inspector/issues) for questions or create a discussion for general topics.

## License

By contributing, you agree that your contributions will be licensed under the MIT license.


## /Dockerfile

``` path="/Dockerfile" 
# Build stage
FROM node:24-slim AS builder

# Set working directory
WORKDIR /app

# Copy package files for installation
COPY package*.json ./
COPY .npmrc ./
COPY client/package*.json ./client/
COPY server/package*.json ./server/
COPY cli/package*.json ./cli/

# Install dependencies
RUN npm ci --ignore-scripts

# Copy source files
COPY . .

# Build the application
RUN npm run build

# Production stage
FROM node:24-slim

WORKDIR /app

# Copy package files for production
COPY package*.json ./
COPY .npmrc ./
COPY client/package*.json ./client/
COPY server/package*.json ./server/
COPY cli/package*.json ./cli/

# Install only production dependencies
RUN npm ci --omit=dev --ignore-scripts

# Copy built files from builder stage
COPY --from=builder /app/client/dist ./client/dist
COPY --from=builder /app/client/bin ./client/bin
COPY --from=builder /app/server/build ./server/build
COPY --from=builder /app/cli/build ./cli/build

# Set default port values as environment variables
ENV CLIENT_PORT=6274
ENV SERVER_PORT=6277

# Document which ports the application uses internally
EXPOSE ${CLIENT_PORT} ${SERVER_PORT}

# Use ENTRYPOINT with CMD for arguments
ENTRYPOINT ["npm", "start"]
```

## /README.md

# MCP Inspector

The MCP inspector is a developer tool for testing and debugging MCP servers.

![MCP Inspector Screenshot](https://raw.githubusercontent.com/modelcontextprotocol/inspector/main/mcp-inspector.png)

## Architecture Overview

The MCP Inspector consists of two main components that work together:

- **MCP Inspector Client (MCPI)**: A React-based web UI that provides an interactive interface for testing and debugging MCP servers
- **MCP Proxy (MCPP)**: A Node.js server that acts as a protocol bridge, connecting the web UI to MCP servers via various transport methods (stdio, SSE, streamable-http)

Note that the proxy is not a network proxy for intercepting traffic. Instead, it functions as both an MCP client (connecting to your MCP server) and an HTTP server (serving the web UI), enabling browser-based interaction with MCP servers that use different transport protocols.

## Running the Inspector

### Requirements

- Node.js: ^22.7.5

### Quick Start (UI mode)

To get up and running right away with the UI, just execute the following:

```bash
npx @modelcontextprotocol/inspector
```

The server will start up and the UI will be accessible at `http://localhost:6274`.

### From an MCP server repository

To inspect an MCP server implementation, there's no need to clone this repo. Instead, use `npx`. For example, if your server is built at `build/index.js`:

```bash
npx @modelcontextprotocol/inspector node build/index.js
```

You can pass both arguments and environment variables to your MCP server. Arguments are passed directly to your server, while environment variables can be set using the `-e` flag:

```bash
# Pass arguments only
npx @modelcontextprotocol/inspector node build/index.js arg1 arg2

# Pass environment variables only
npx @modelcontextprotocol/inspector -e key=value -e key2=$VALUE2 node build/index.js

# Pass both environment variables and arguments
npx @modelcontextprotocol/inspector -e key=value -e key2=$VALUE2 node build/index.js arg1 arg2

# Use -- to separate inspector flags from server arguments
npx @modelcontextprotocol/inspector -e key=$VALUE -- node build/index.js -e server-flag
```

The inspector runs both an MCP Inspector (MCPI) client UI (default port 6274) and an MCP Proxy (MCPP) server (default port 6277). Open the MCPI client UI in your browser to use the inspector. (These ports are derived from the T9 dialpad mapping of MCPI and MCPP respectively, as a mnemonic). You can customize the ports if needed:

```bash
CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector node build/index.js
```

For more details on ways to use the inspector, see the [Inspector section of the MCP docs site](https://modelcontextprotocol.io/docs/tools/inspector). For help with debugging, see the [Debugging guide](https://modelcontextprotocol.io/docs/tools/debugging).

### Servers File Export

The MCP Inspector provides convenient buttons to export server launch configurations for use in clients such as Cursor, Claude Code, or the Inspector's CLI. The file is usually called `mcp.json`.

- **Server Entry** - Copies a single server configuration entry to your clipboard. This can be added to your `mcp.json` file inside the `mcpServers` object with your preferred server name.

  **STDIO transport example:**

  ```json
  {
    "command": "node",
    "args": ["build/index.js", "--debug"],
    "env": {
      "API_KEY": "your-api-key",
      "DEBUG": "true"
    }
  }
  ```

  **SSE transport example:**

  ```json
  {
    "type": "sse",
    "url": "http://localhost:3000/events",
    "note": "For SSE connections, add this URL directly in Client"
  }
  ```

- **Servers File** - Copies a complete MCP configuration file structure to your clipboard, with your current server configuration added as `default-server`. This can be saved directly as `mcp.json`.

  **STDIO transport example:**

  ```json
  {
    "mcpServers": {
      "default-server": {
        "command": "node",
        "args": ["build/index.js", "--debug"],
        "env": {
          "API_KEY": "your-api-key",
          "DEBUG": "true"
        }
      }
    }
  }
  ```

  **SSE transport example:**

  ```json
  {
    "mcpServers": {
      "default-server": {
        "type": "sse",
        "url": "http://localhost:3000/events",
        "note": "For SSE connections, add this URL directly in Client"
      }
    }
  }
  ```

These buttons appear in the Inspector UI after you've configured your server settings, making it easy to save and reuse your configurations.

For SSE transport connections, the Inspector provides similar functionality for both buttons. The "Server Entry" button copies the SSE URL configuration that can be added to your existing configuration file, while the "Servers File" button creates a complete configuration file containing the SSE URL for direct use in clients.

You can paste the Server Entry into your existing `mcp.json` file under your chosen server name, or use the complete Servers File payload to create a new configuration file.

### Authentication

The inspector supports bearer token authentication for SSE connections. Enter your token in the UI when connecting to an MCP server, and it will be sent in the Authorization header. You can override the header name using the input field in the sidebar.

### Security Considerations

The MCP Inspector includes a proxy server that can run and communicate with local MCP processes. The proxy server should not be exposed to untrusted networks as it has permissions to spawn local processes and can connect to any specified MCP server.

#### Authentication

The MCP Inspector proxy server requires authentication by default. When starting the server, a random session token is generated and printed to the console:

```
🔑 Session token: 3a1c267fad21f7150b7d624c160b7f09b0b8c4f623c7107bbf13378f051538d4

🔗 Open inspector with token pre-filled:
   http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=3a1c267fad21f7150b7d624c160b7f09b0b8c4f623c7107bbf13378f051538d4
```

This token must be included as a Bearer token in the Authorization header for all requests to the server. The inspector will automatically open your browser with the token pre-filled in the URL.

**Automatic browser opening** - The inspector now automatically opens your browser with the token pre-filled in the URL when authentication is enabled.

**Alternative: Manual configuration** - If you already have the inspector open:

1. Click the "Configuration" button in the sidebar
2. Find "Proxy Session Token" and enter the token displayed in the proxy console
3. Click "Save" to apply the configuration

The token will be saved in your browser's local storage for future use.

If you need to disable authentication (NOT RECOMMENDED), you can set the `DANGEROUSLY_OMIT_AUTH` environment variable:

```bash
DANGEROUSLY_OMIT_AUTH=true npm start
```

#### Local-only Binding

By default, both the MCP Inspector proxy server and client bind only to `localhost` to prevent network access. This ensures they are not accessible from other devices on the network. If you need to bind to all interfaces for development purposes, you can override this with the `HOST` environment variable:

```bash
HOST=0.0.0.0 npm start
```

**Warning:** Only bind to all interfaces in trusted network environments, as this exposes the proxy server's ability to execute local processes and both services to network access.

#### DNS Rebinding Protection

To prevent DNS rebinding attacks, the MCP Inspector validates the `Origin` header on incoming requests. By default, only requests from the client origin are allowed (respects `CLIENT_PORT` if set, defaulting to port 6274). You can configure additional allowed origins by setting the `ALLOWED_ORIGINS` environment variable (comma-separated list):

```bash
ALLOWED_ORIGINS=http://localhost:6274,http://localhost:8000 npm start
```

### Configuration

The MCP Inspector supports the following configuration settings. To change them, click on the `Configuration` button in the MCP Inspector UI:

| Setting                                 | Description                                                                                                                                       | Default |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `MCP_SERVER_REQUEST_TIMEOUT`            | Timeout for requests to the MCP server (ms)                                                                                                       | 10000   |
| `MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS` | Reset timeout on progress notifications                                                                                                           | true    |
| `MCP_REQUEST_MAX_TOTAL_TIMEOUT`         | Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)                                                  | 60000   |
| `MCP_PROXY_FULL_ADDRESS`                | Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577                                      | ""      |
| `MCP_AUTO_OPEN_ENABLED`                 | Enable automatic browser opening when inspector starts (works with authentication enabled). Only as environment var, not configurable in browser. | true    |

These settings can be adjusted in real-time through the UI and will persist across sessions.

The inspector also supports configuration files to store settings for different MCP servers. This is useful when working with multiple servers or complex configurations:

```bash
npx @modelcontextprotocol/inspector --config path/to/config.json --server everything
```

Example server configuration file:

```json
{
  "mcpServers": {
    "everything": {
      "command": "npx",
      "args": ["@modelcontextprotocol/server-everything"],
      "env": {
        "hello": "Hello MCP!"
      }
    },
    "my-server": {
      "command": "node",
      "args": ["build/index.js", "arg1", "arg2"],
      "env": {
        "key": "value",
        "key2": "value2"
      }
    }
  }
}
```

> **Tip:** You can easily generate this configuration format using the **Server Entry** and **Servers File** buttons in the Inspector UI, as described in the Servers File Export section above.

You can also set the initial `transport` type, `serverUrl`, `serverCommand`, and `serverArgs` via query params, for example:

```
http://localhost:6274/?transport=sse&serverUrl=http://localhost:8787/sse
http://localhost:6274/?transport=streamable-http&serverUrl=http://localhost:8787/mcp
http://localhost:6274/?transport=stdio&serverCommand=npx&serverArgs=arg1%20arg2
```

You can also set initial config settings via query params, for example:

```
http://localhost:6274/?MCP_SERVER_REQUEST_TIMEOUT=10000&MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS=false&MCP_PROXY_FULL_ADDRESS=http://10.1.1.22:5577
```

Note that if both the query param and the corresponding localStorage item are set, the query param will take precedence.

### From this repository

If you're working on the inspector itself:

Development mode:

```bash
npm run dev
```

> **Note for Windows users:**
> On Windows, use the following command instead:
>
> ```bash
> npm run dev:windows
> ```

Production mode:

```bash
npm run build
npm start
```

### CLI Mode

CLI mode enables programmatic interaction with MCP servers from the command line, ideal for scripting, automation, and integration with coding assistants. This creates an efficient feedback loop for MCP server development.

```bash
npx @modelcontextprotocol/inspector --cli node build/index.js
```

The CLI mode supports most operations across tools, resources, and prompts. A few examples:

```bash
# Basic usage
npx @modelcontextprotocol/inspector --cli node build/index.js

# With config file
npx @modelcontextprotocol/inspector --cli --config path/to/config.json --server myserver

# List available tools
npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list

# Call a specific tool
npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/call --tool-name mytool --tool-arg key=value --tool-arg another=value2

# List available resources
npx @modelcontextprotocol/inspector --cli node build/index.js --method resources/list

# List available prompts
npx @modelcontextprotocol/inspector --cli node build/index.js --method prompts/list

# Connect to a remote MCP server (default is SSE transport)
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com

# Connect to a remote MCP server (with Streamable HTTP transport)
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --transport http

# Call a tool on a remote server
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --method tools/call --tool-name remotetool --tool-arg param=value

# List resources from a remote server
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --method resources/list
```

### UI Mode vs CLI Mode: When to Use Each

| Use Case                 | UI Mode                                                                   | CLI Mode                                                                                                                                             |
| ------------------------ | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Server Development**   | Visual interface for interactive testing and debugging during development | Scriptable commands for quick testing and continuous integration; creates feedback loops with AI coding assistants like Cursor for rapid development |
| **Resource Exploration** | Interactive browser with hierarchical navigation and JSON visualization   | Programmatic listing and reading for automation and scripting                                                                                        |
| **Tool Testing**         | Form-based parameter input with real-time response visualization          | Command-line tool execution with JSON output for scripting                                                                                           |
| **Prompt Engineering**   | Interactive sampling with streaming responses and visual comparison       | Batch processing of prompts with machine-readable output                                                                                             |
| **Debugging**            | Request history, visualized errors, and real-time notifications           | Direct JSON output for log analysis and integration with other tools                                                                                 |
| **Automation**           | N/A                                                                       | Ideal for CI/CD pipelines, batch processing, and integration with coding assistants                                                                  |
| **Learning MCP**         | Rich visual interface helps new users understand server capabilities      | Simplified commands for focused learning of specific endpoints                                                                                       |

## License

This project is licensed under the MIT License—see the [LICENSE](LICENSE) file for details.


## /SECURITY.md

# Security Policy
Thank you for helping us keep the inspector secure.

## Reporting Security Issues

This project is maintained by [Anthropic](https://www.anthropic.com/) as part of the Model Context Protocol project.

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.

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).

## Vulnerability Disclosure Program

Our Vulnerability Program Guidelines are defined on our [HackerOne program page](https://hackerone.com/anthropic-vdp).


## /cli/package.json

```json path="/cli/package.json" 
{
  "name": "@modelcontextprotocol/inspector-cli",
  "version": "0.15.0",
  "description": "CLI for the Model Context Protocol inspector",
  "license": "MIT",
  "author": "Anthropic, PBC (https://anthropic.com)",
  "homepage": "https://modelcontextprotocol.io",
  "bugs": "https://github.com/modelcontextprotocol/inspector/issues",
  "main": "build/cli.js",
  "type": "module",
  "bin": {
    "mcp-inspector-cli": "build/cli.js"
  },
  "files": [
    "build"
  ],
  "scripts": {
    "build": "tsc",
    "postbuild": "node scripts/make-executable.js",
    "test": "node scripts/cli-tests.js"
  },
  "devDependencies": {},
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.13.1",
    "commander": "^13.1.0",
    "spawn-rx": "^5.1.2"
  }
}

```

## /cli/scripts/cli-tests.js

```js path="/cli/scripts/cli-tests.js" 
#!/usr/bin/env node

// Colors for output
const colors = {
  GREEN: "\x1b[32m",
  YELLOW: "\x1b[33m",
  RED: "\x1b[31m",
  BLUE: "\x1b[34m",
  ORANGE: "\x1b[33m",
  NC: "\x1b[0m", // No Color
};

import fs from "fs";
import path from "path";
import { execSync, spawn } from "child_process";
import os from "os";
import { fileURLToPath } from "url";

// Get directory paths with ESM compatibility
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Track test results
let PASSED_TESTS = 0;
let FAILED_TESTS = 0;
let SKIPPED_TESTS = 0;
let TOTAL_TESTS = 0;

console.log(
  `${colors.YELLOW}=== MCP Inspector CLI Test Script ===${colors.NC}`,
);
console.log(
  `${colors.BLUE}This script tests the MCP Inspector CLI's ability to handle various command line options:${colors.NC}`,
);
console.log(`${colors.BLUE}- Basic CLI mode${colors.NC}`);
console.log(`${colors.BLUE}- Environment variables (-e)${colors.NC}`);
console.log(`${colors.BLUE}- Config file (--config)${colors.NC}`);
console.log(`${colors.BLUE}- Server selection (--server)${colors.NC}`);
console.log(`${colors.BLUE}- Method selection (--method)${colors.NC}`);
console.log(
  `${colors.BLUE}- Tool-related options (--tool-name, --tool-arg)${colors.NC}`,
);
console.log(`${colors.BLUE}- Resource-related options (--uri)${colors.NC}`);
console.log(
  `${colors.BLUE}- Prompt-related options (--prompt-name, --prompt-args)${colors.NC}`,
);
console.log(`${colors.BLUE}- Logging options (--log-level)${colors.NC}`);
console.log(
  `${colors.BLUE}- Transport types (--transport http/sse/stdio)${colors.NC}`,
);
console.log(
  `${colors.BLUE}- Transport inference from URL suffixes (/mcp, /sse)${colors.NC}`,
);
console.log(`\n`);

// Get directory paths
const SCRIPTS_DIR = __dirname;
const PROJECT_ROOT = path.join(SCRIPTS_DIR, "../../");
const BUILD_DIR = path.resolve(SCRIPTS_DIR, "../build");

// Define the test server command using npx
const TEST_CMD = "npx";
const TEST_ARGS = ["@modelcontextprotocol/server-everything"];

// Create output directory for test results
const OUTPUT_DIR = path.join(SCRIPTS_DIR, "test-output");
if (!fs.existsSync(OUTPUT_DIR)) {
  fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}

// Create a temporary directory for test files
const TEMP_DIR = path.join(os.tmpdir(), "mcp-inspector-tests");
fs.mkdirSync(TEMP_DIR, { recursive: true });

// Track servers for cleanup
let runningServers = [];

process.on("exit", () => {
  try {
    fs.rmSync(TEMP_DIR, { recursive: true, force: true });
  } catch (err) {
    console.error(
      `${colors.RED}Failed to remove temp directory: ${err.message}${colors.NC}`,
    );
  }

  runningServers.forEach((server) => {
    try {
      process.kill(-server.pid);
    } catch (e) {}
  });
});

process.on("SIGINT", () => {
  runningServers.forEach((server) => {
    try {
      process.kill(-server.pid);
    } catch (e) {}
  });
  process.exit(1);
});

// Use the existing sample config file
console.log(
  `${colors.BLUE}Using existing sample config file: ${PROJECT_ROOT}/sample-config.json${colors.NC}`,
);
try {
  const sampleConfig = fs.readFileSync(
    path.join(PROJECT_ROOT, "sample-config.json"),
    "utf8",
  );
  console.log(sampleConfig);
} catch (error) {
  console.error(
    `${colors.RED}Error reading sample config: ${error.message}${colors.NC}`,
  );
}

// Create an invalid config file for testing
const invalidConfigPath = path.join(TEMP_DIR, "invalid-config.json");
fs.writeFileSync(invalidConfigPath, '{\n  "mcpServers": {\n    "invalid": {');

// Function to run a basic test
async function runBasicTest(testName, ...args) {
  const outputFile = path.join(
    OUTPUT_DIR,
    `${testName.replace(/\//g, "_")}.log`,
  );

  console.log(`\n${colors.YELLOW}Testing: ${testName}${colors.NC}`);
  TOTAL_TESTS++;

  // Run the command and capture output
  console.log(
    `${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`,
  );

  try {
    // Create a write stream for the output file
    const outputStream = fs.createWriteStream(outputFile);

    // Spawn the process
    return new Promise((resolve) => {
      const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], {
        stdio: ["ignore", "pipe", "pipe"],
      });

      const timeout = setTimeout(() => {
        console.log(`${colors.YELLOW}Test timed out: ${testName}${colors.NC}`);
        child.kill();
      }, 10000);

      // Pipe stdout and stderr to the output file
      child.stdout.pipe(outputStream);
      child.stderr.pipe(outputStream);

      // Also capture output for display
      let output = "";
      child.stdout.on("data", (data) => {
        output += data.toString();
      });
      child.stderr.on("data", (data) => {
        output += data.toString();
      });

      child.on("close", (code) => {
        clearTimeout(timeout);
        outputStream.end();

        if (code === 0) {
          console.log(`${colors.GREEN}✓ Test passed: ${testName}${colors.NC}`);
          console.log(`${colors.BLUE}First few lines of output:${colors.NC}`);
          const firstFewLines = output
            .split("\n")
            .slice(0, 5)
            .map((line) => `  ${line}`)
            .join("\n");
          console.log(firstFewLines);
          PASSED_TESTS++;
          resolve(true);
        } else {
          console.log(`${colors.RED}✗ Test failed: ${testName}${colors.NC}`);
          console.log(`${colors.RED}Error output:${colors.NC}`);
          console.log(
            output
              .split("\n")
              .map((line) => `  ${line}`)
              .join("\n"),
          );
          FAILED_TESTS++;

          // Stop after any error is encountered
          console.log(
            `${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`,
          );
          process.exit(1);
        }
      });
    });
  } catch (error) {
    console.error(
      `${colors.RED}Error running test: ${error.message}${colors.NC}`,
    );
    FAILED_TESTS++;
    process.exit(1);
  }
}

// Function to run an error test (expected to fail)
async function runErrorTest(testName, ...args) {
  const outputFile = path.join(
    OUTPUT_DIR,
    `${testName.replace(/\//g, "_")}.log`,
  );

  console.log(`\n${colors.YELLOW}Testing error case: ${testName}${colors.NC}`);
  TOTAL_TESTS++;

  // Run the command and capture output
  console.log(
    `${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`,
  );

  try {
    // Create a write stream for the output file
    const outputStream = fs.createWriteStream(outputFile);

    // Spawn the process
    return new Promise((resolve) => {
      const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], {
        stdio: ["ignore", "pipe", "pipe"],
      });

      const timeout = setTimeout(() => {
        console.log(
          `${colors.YELLOW}Error test timed out: ${testName}${colors.NC}`,
        );
        child.kill();
      }, 10000);

      // Pipe stdout and stderr to the output file
      child.stdout.pipe(outputStream);
      child.stderr.pipe(outputStream);

      // Also capture output for display
      let output = "";
      child.stdout.on("data", (data) => {
        output += data.toString();
      });
      child.stderr.on("data", (data) => {
        output += data.toString();
      });

      child.on("close", (code) => {
        clearTimeout(timeout);
        outputStream.end();

        // For error tests, we expect a non-zero exit code
        if (code !== 0) {
          console.log(
            `${colors.GREEN}✓ Error test passed: ${testName}${colors.NC}`,
          );
          console.log(`${colors.BLUE}Error output (expected):${colors.NC}`);
          const firstFewLines = output
            .split("\n")
            .slice(0, 5)
            .map((line) => `  ${line}`)
            .join("\n");
          console.log(firstFewLines);
          PASSED_TESTS++;
          resolve(true);
        } else {
          console.log(
            `${colors.RED}✗ Error test failed: ${testName} (expected error but got success)${colors.NC}`,
          );
          console.log(`${colors.RED}Output:${colors.NC}`);
          console.log(
            output
              .split("\n")
              .map((line) => `  ${line}`)
              .join("\n"),
          );
          FAILED_TESTS++;

          // Stop after any error is encountered
          console.log(
            `${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`,
          );
          process.exit(1);
        }
      });
    });
  } catch (error) {
    console.error(
      `${colors.RED}Error running test: ${error.message}${colors.NC}`,
    );
    FAILED_TESTS++;
    process.exit(1);
  }
}

// Run all tests
async function runTests() {
  console.log(
    `\n${colors.YELLOW}=== Running Basic CLI Mode Tests ===${colors.NC}`,
  );

  // Test 1: Basic CLI mode with method
  await runBasicTest(
    "basic_cli_mode",
    TEST_CMD,
    ...TEST_ARGS,
    "--cli",
    "--method",
    "tools/list",
  );

  // Test 2: CLI mode with non-existent method (should fail)
  await runErrorTest(
    "nonexistent_method",
    TEST_CMD,
    ...TEST_ARGS,
    "--cli",
    "--method",
    "nonexistent/method",
  );

  // Test 3: CLI mode without method (should fail)
  await runErrorTest("missing_method", TEST_CMD, ...TEST_ARGS, "--cli");

  console.log(
    `\n${colors.YELLOW}=== Running Environment Variable Tests ===${colors.NC}`,
  );

  // Test 4: CLI mode with environment variables
  await runBasicTest(
    "env_variables",
    TEST_CMD,
    ...TEST_ARGS,
    "-e",
    "KEY1=value1",
    "-e",
    "KEY2=value2",
    "--cli",
    "--method",
    "tools/list",
  );

  // Test 5: CLI mode with invalid environment variable format (should fail)
  await runErrorTest(
    "invalid_env_format",
    TEST_CMD,
    ...TEST_ARGS,
    "-e",
    "INVALID_FORMAT",
    "--cli",
    "--method",
    "tools/list",
  );

  // Test 5b: CLI mode with environment variable containing equals sign in value
  await runBasicTest(
    "env_variable_with_equals",
    TEST_CMD,
    ...TEST_ARGS,
    "-e",
    "API_KEY=abc123=xyz789==",
    "--cli",
    "--method",
    "tools/list",
  );

  // Test 5c: CLI mode with environment variable containing base64-encoded value
  await runBasicTest(
    "env_variable_with_base64",
    TEST_CMD,
    ...TEST_ARGS,
    "-e",
    "JWT_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=",
    "--cli",
    "--method",
    "tools/list",
  );

  console.log(
    `\n${colors.YELLOW}=== Running Config File Tests ===${colors.NC}`,
  );

  // Test 6: Using config file with CLI mode
  await runBasicTest(
    "config_file",
    "--config",
    path.join(PROJECT_ROOT, "sample-config.json"),
    "--server",
    "everything",
    "--cli",
    "--method",
    "tools/list",
  );

  // Test 7: Using config file without server name (should fail)
  await runErrorTest(
    "config_without_server",
    "--config",
    path.join(PROJECT_ROOT, "sample-config.json"),
    "--cli",
    "--method",
    "tools/list",
  );

  // Test 8: Using server name without config file (should fail)
  await runErrorTest(
    "server_without_config",
    "--server",
    "everything",
    "--cli",
    "--method",
    "tools/list",
  );

  // Test 9: Using non-existent config file (should fail)
  await runErrorTest(
    "nonexistent_config",
    "--config",
    "./nonexistent-config.json",
    "--server",
    "everything",
    "--cli",
    "--method",
    "tools/list",
  );

  // Test 10: Using invalid config file format (should fail)
  await runErrorTest(
    "invalid_config",
    "--config",
    invalidConfigPath,
    "--server",
    "everything",
    "--cli",
    "--method",
    "tools/list",
  );

  // Test 11: Using config file with non-existent server (should fail)
  await runErrorTest(
    "nonexistent_server",
    "--config",
    path.join(PROJECT_ROOT, "sample-config.json"),
    "--server",
    "nonexistent",
    "--cli",
    "--method",
    "tools/list",
  );

  console.log(
    `\n${colors.YELLOW}=== Running Tool-Related Tests ===${colors.NC}`,
  );

  // Test 12: CLI mode with tool call
  await runBasicTest(
    "tool_call",
    TEST_CMD,
    ...TEST_ARGS,
    "--cli",
    "--method",
    "tools/call",
    "--tool-name",
    "echo",
    "--tool-arg",
    "message=Hello",
  );

  // Test 13: CLI mode with tool call but missing tool name (should fail)
  await runErrorTest(
    "missing_tool_name",
    TEST_CMD,
    ...TEST_ARGS,
    "--cli",
    "--method",
    "tools/call",
    "--tool-arg",
    "message=Hello",
  );

  // Test 14: CLI mode with tool call but invalid tool args format (should fail)
  await runErrorTest(
    "invalid_tool_args",
    TEST_CMD,
    ...TEST_ARGS,
    "--cli",
    "--method",
    "tools/call",
    "--tool-name",
    "echo",
    "--tool-arg",
    "invalid_format",
  );

  // Test 15: CLI mode with multiple tool args
  await runBasicTest(
    "multiple_tool_args",
    TEST_CMD,
    ...TEST_ARGS,
    "--cli",
    "--method",
    "tools/call",
    "--tool-name",
    "add",
    "--tool-arg",
    "a=1",
    "b=2",
  );

  console.log(
    `\n${colors.YELLOW}=== Running Resource-Related Tests ===${colors.NC}`,
  );

  // Test 16: CLI mode with resource read
  await runBasicTest(
    "resource_read",
    TEST_CMD,
    ...TEST_ARGS,
    "--cli",
    "--method",
    "resources/read",
    "--uri",
    "test://static/resource/1",
  );

  // Test 17: CLI mode with resource read but missing URI (should fail)
  await runErrorTest(
    "missing_uri",
    TEST_CMD,
    ...TEST_ARGS,
    "--cli",
    "--method",
    "resources/read",
  );

  console.log(
    `\n${colors.YELLOW}=== Running Prompt-Related Tests ===${colors.NC}`,
  );

  // Test 18: CLI mode with prompt get
  await runBasicTest(
    "prompt_get",
    TEST_CMD,
    ...TEST_ARGS,
    "--cli",
    "--method",
    "prompts/get",
    "--prompt-name",
    "simple_prompt",
  );

  // Test 19: CLI mode with prompt get and args
  await runBasicTest(
    "prompt_get_with_args",
    TEST_CMD,
    ...TEST_ARGS,
    "--cli",
    "--method",
    "prompts/get",
    "--prompt-name",
    "complex_prompt",
    "--prompt-args",
    "temperature=0.7",
    "style=concise",
  );

  // Test 20: CLI mode with prompt get but missing prompt name (should fail)
  await runErrorTest(
    "missing_prompt_name",
    TEST_CMD,
    ...TEST_ARGS,
    "--cli",
    "--method",
    "prompts/get",
  );

  console.log(`\n${colors.YELLOW}=== Running Logging Tests ===${colors.NC}`);

  // Test 21: CLI mode with log level
  await runBasicTest(
    "log_level",
    TEST_CMD,
    ...TEST_ARGS,
    "--cli",
    "--method",
    "logging/setLevel",
    "--log-level",
    "debug",
  );

  // Test 22: CLI mode with invalid log level (should fail)
  await runErrorTest(
    "invalid_log_level",
    TEST_CMD,
    ...TEST_ARGS,
    "--cli",
    "--method",
    "logging/setLevel",
    "--log-level",
    "invalid",
  );

  console.log(
    `\n${colors.YELLOW}=== Running Combined Option Tests ===${colors.NC}`,
  );

  // Note about the combined options issue
  console.log(
    `${colors.BLUE}Testing combined options with environment variables and config file.${colors.NC}`,
  );

  // Test 23: CLI mode with config file, environment variables, and tool call
  await runBasicTest(
    "combined_options",
    "--config",
    path.join(PROJECT_ROOT, "sample-config.json"),
    "--server",
    "everything",
    "-e",
    "CLI_ENV_VAR=cli_value",
    "--cli",
    "--method",
    "tools/list",
  );

  // Test 24: CLI mode with all possible options (that make sense together)
  await runBasicTest(
    "all_options",
    "--config",
    path.join(PROJECT_ROOT, "sample-config.json"),
    "--server",
    "everything",
    "-e",
    "CLI_ENV_VAR=cli_value",
    "--cli",
    "--method",
    "tools/call",
    "--tool-name",
    "echo",
    "--tool-arg",
    "message=Hello",
    "--log-level",
    "debug",
  );

  console.log(
    `\n${colors.YELLOW}=== Running HTTP Transport Tests ===${colors.NC}`,
  );

  console.log(
    `${colors.BLUE}Starting server-everything in streamableHttp mode.${colors.NC}`,
  );
  const httpServer = spawn(
    "npx",
    ["@modelcontextprotocol/server-everything", "streamableHttp"],
    {
      detached: true,
      stdio: "ignore",
    },
  );
  runningServers.push(httpServer);

  await new Promise((resolve) => setTimeout(resolve, 3000));

  // Test 25: HTTP transport inferred from URL ending with /mcp
  await runBasicTest(
    "http_transport_inferred",
    "http://127.0.0.1:3001/mcp",
    "--cli",
    "--method",
    "tools/list",
  );

  // Test 26: HTTP transport with explicit --transport http flag
  await runBasicTest(
    "http_transport_with_explicit_flag",
    "http://127.0.0.1:3001",
    "--transport",
    "http",
    "--cli",
    "--method",
    "tools/list",
  );

  // Test 27: HTTP transport with suffix and --transport http flag
  await runBasicTest(
    "http_transport_with_explicit_flag_and_suffix",
    "http://127.0.0.1:3001/mcp",
    "--transport",
    "http",
    "--cli",
    "--method",
    "tools/list",
  );

  // Test 28: SSE transport given to HTTP server (should fail)
  await runErrorTest(
    "sse_transport_given_to_http_server",
    "http://127.0.0.1:3001",
    "--transport",
    "sse",
    "--cli",
    "--method",
    "tools/list",
  );

  // Kill HTTP server
  try {
    process.kill(-httpServer.pid);
    console.log(
      `${colors.BLUE}HTTP server killed, waiting for port to be released...${colors.NC}`,
    );
  } catch (e) {
    console.log(
      `${colors.RED}Error killing HTTP server: ${e.message}${colors.NC}`,
    );
  }

  // Print test summary
  console.log(`\n${colors.YELLOW}=== Test Summary ===${colors.NC}`);
  console.log(`${colors.GREEN}Passed: ${PASSED_TESTS}${colors.NC}`);
  console.log(`${colors.RED}Failed: ${FAILED_TESTS}${colors.NC}`);
  console.log(`${colors.ORANGE}Skipped: ${SKIPPED_TESTS}${colors.NC}`);
  console.log(`Total: ${TOTAL_TESTS}`);
  console.log(
    `${colors.BLUE}Detailed logs saved to: ${OUTPUT_DIR}${colors.NC}`,
  );

  console.log(`\n${colors.GREEN}All tests completed!${colors.NC}`);
}

// Run all tests
runTests().catch((error) => {
  console.error(
    `${colors.RED}Tests failed with error: ${error.message}${colors.NC}`,
  );
  process.exit(1);
});

```

## /cli/scripts/make-executable.js

```js path="/cli/scripts/make-executable.js" 
/**
 * Cross-platform script to make a file executable
 */
import { promises as fs } from "fs";
import { platform } from "os";
import { execSync } from "child_process";
import path from "path";

const TARGET_FILE = path.resolve("build/cli.js");

async function makeExecutable() {
  try {
    // On Unix-like systems (Linux, macOS), use chmod
    if (platform() !== "win32") {
      execSync(`chmod +x "${TARGET_FILE}"`);
      console.log("Made file executable with chmod");
    } else {
      // On Windows, no need to make files "executable" in the Unix sense
      // Just ensure the file exists
      await fs.access(TARGET_FILE);
      console.log("File exists and is accessible on Windows");
    }
  } catch (error) {
    console.error("Error making file executable:", error);
    process.exit(1);
  }
}

makeExecutable();

```

## /cli/src/cli.ts

```ts path="/cli/src/cli.ts" 
#!/usr/bin/env node

import { Command } from "commander";
import fs from "node:fs";
import path from "node:path";
import { dirname, resolve } from "path";
import { spawnPromise } from "spawn-rx";
import { fileURLToPath } from "url";

const __dirname = dirname(fileURLToPath(import.meta.url));

type Args = {
  command: string;
  args: string[];
  envArgs: Record<string, string>;
  cli: boolean;
};

type CliOptions = {
  e?: Record<string, string>;
  config?: string;
  server?: string;
  cli?: boolean;
};

type ServerConfig = {
  command: string;
  args?: string[];
  env?: Record<string, string>;
};

function handleError(error: unknown): never {
  let message: string;

  if (error instanceof Error) {
    message = error.message;
  } else if (typeof error === "string") {
    message = error;
  } else {
    message = "Unknown error";
  }

  console.error(message);

  process.exit(1);
}

function delay(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms, true));
}

async function runWebClient(args: Args): Promise<void> {
  const inspectorServerPath = resolve(
    __dirname,
    "../../",
    "server",
    "build",
    "index.js",
  );

  // Path to the client entry point
  const inspectorClientPath = resolve(
    __dirname,
    "../../",
    "client",
    "bin",
    "client.js",
  );

  const CLIENT_PORT: string = process.env.CLIENT_PORT ?? "6274";
  const SERVER_PORT: string = process.env.SERVER_PORT ?? "6277";

  console.log("Starting MCP inspector...");

  const abort = new AbortController();
  let cancelled: boolean = false;
  process.on("SIGINT", () => {
    cancelled = true;
    abort.abort();
  });

  let server: ReturnType<typeof spawnPromise>;
  let serverOk: unknown;

  try {
    server = spawnPromise(
      "node",
      [
        inspectorServerPath,
        ...(args.command ? [`--env`, args.command] : []),
        ...(args.args ? [`--args=${args.args.join(" ")}`] : []),
      ],
      {
        env: {
          ...process.env,
          PORT: SERVER_PORT,
          MCP_ENV_VARS: JSON.stringify(args.envArgs),
        },
        signal: abort.signal,
        echoOutput: true,
      },
    );

    // Make sure server started before starting client
    serverOk = await Promise.race([server, delay(2 * 1000)]);
  } catch (error) {}

  if (serverOk) {
    try {
      await spawnPromise("node", [inspectorClientPath], {
        env: { ...process.env, PORT: CLIENT_PORT },
        signal: abort.signal,
        echoOutput: true,
      });
    } catch (e) {
      if (!cancelled || process.env.DEBUG) throw e;
    }
  }
}

async function runCli(args: Args): Promise<void> {
  const projectRoot = resolve(__dirname, "..");
  const cliPath = resolve(projectRoot, "build", "index.js");

  const abort = new AbortController();

  let cancelled = false;

  process.on("SIGINT", () => {
    cancelled = true;
    abort.abort();
  });

  try {
    await spawnPromise("node", [cliPath, args.command, ...args.args], {
      env: { ...process.env, ...args.envArgs },
      signal: abort.signal,
      echoOutput: true,
    });
  } catch (e) {
    if (!cancelled || process.env.DEBUG) {
      throw e;
    }
  }
}

function loadConfigFile(configPath: string, serverName: string): ServerConfig {
  try {
    const resolvedConfigPath = path.isAbsolute(configPath)
      ? configPath
      : path.resolve(process.cwd(), configPath);

    if (!fs.existsSync(resolvedConfigPath)) {
      throw new Error(`Config file not found: ${resolvedConfigPath}`);
    }

    const configContent = fs.readFileSync(resolvedConfigPath, "utf8");
    const parsedConfig = JSON.parse(configContent);

    if (!parsedConfig.mcpServers || !parsedConfig.mcpServers[serverName]) {
      const availableServers = Object.keys(parsedConfig.mcpServers || {}).join(
        ", ",
      );
      throw new Error(
        `Server '${serverName}' not found in config file. Available servers: ${availableServers}`,
      );
    }

    const serverConfig = parsedConfig.mcpServers[serverName];

    return serverConfig;
  } catch (err: unknown) {
    if (err instanceof SyntaxError) {
      throw new Error(`Invalid JSON in config file: ${err.message}`);
    }

    throw err;
  }
}

function parseKeyValuePair(
  value: string,
  previous: Record<string, string> = {},
): Record<string, string> {
  const parts = value.split("=");
  const key = parts[0];
  const val = parts.slice(1).join("=");

  if (val === undefined || val === "") {
    throw new Error(
      `Invalid parameter format: ${value}. Use key=value format.`,
    );
  }

  return { ...previous, [key as string]: val };
}

function parseArgs(): Args {
  const program = new Command();

  const argSeparatorIndex = process.argv.indexOf("--");
  let preArgs = process.argv;
  let postArgs: string[] = [];

  if (argSeparatorIndex !== -1) {
    preArgs = process.argv.slice(0, argSeparatorIndex);
    postArgs = process.argv.slice(argSeparatorIndex + 1);
  }

  program
    .name("inspector-bin")
    .allowExcessArguments()
    .allowUnknownOption()
    .option(
      "-e <env>",
      "environment variables in KEY=VALUE format",
      parseKeyValuePair,
      {},
    )
    .option("--config <path>", "config file path")
    .option("--server <n>", "server name from config file")
    .option("--cli", "enable CLI mode");

  // Parse only the arguments before --
  program.parse(preArgs);

  const options = program.opts() as CliOptions;
  const remainingArgs = program.args;

  // Add back any arguments that came after --
  const finalArgs = [...remainingArgs, ...postArgs];

  // Validate that config and server are provided together
  if (
    (options.config && !options.server) ||
    (!options.config && options.server)
  ) {
    throw new Error(
      "Both --config and --server must be provided together. If you specify one, you must specify the other.",
    );
  }

  // If config file is specified, load and use the options from the file. We must merge the args
  // from the command line and the file together, or we will miss the method options (--method,
  // etc.)
  if (options.config && options.server) {
    const config = loadConfigFile(options.config, options.server);

    return {
      command: config.command,
      args: [...(config.args || []), ...finalArgs],
      envArgs: { ...(config.env || {}), ...(options.e || {}) },
      cli: options.cli || false,
    };
  }

  // Otherwise use command line arguments
  const command = finalArgs[0] || "";
  const args = finalArgs.slice(1);

  return {
    command,
    args,
    envArgs: options.e || {},
    cli: options.cli || false,
  };
}

async function main(): Promise<void> {
  process.on("uncaughtException", (error) => {
    handleError(error);
  });

  try {
    const args = parseArgs();

    if (args.cli) {
      runCli(args);
    } else {
      await runWebClient(args);
    }
  } catch (error) {
    handleError(error);
  }
}

main();

```

## /cli/src/client/connection.ts

```ts path="/cli/src/client/connection.ts" 
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import { McpResponse } from "./types.js";

export const validLogLevels = [
  "trace",
  "debug",
  "info",
  "warn",
  "error",
] as const;

export type LogLevel = (typeof validLogLevels)[number];

export async function connect(
  client: Client,
  transport: Transport,
): Promise<void> {
  try {
    await client.connect(transport);
  } catch (error) {
    throw new Error(
      `Failed to connect to MCP server: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

export async function disconnect(transport: Transport): Promise<void> {
  try {
    await transport.close();
  } catch (error) {
    throw new Error(
      `Failed to disconnect from MCP server: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

// Set logging level
export async function setLoggingLevel(
  client: Client,
  level: LogLevel,
): Promise<McpResponse> {
  try {
    const response = await client.setLoggingLevel(level as any);
    return response;
  } catch (error) {
    throw new Error(
      `Failed to set logging level: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

```

## /cli/src/client/index.ts

```ts path="/cli/src/client/index.ts" 
// Re-export everything from the client modules
export * from "./connection.js";
export * from "./prompts.js";
export * from "./resources.js";
export * from "./tools.js";
export * from "./types.js";

```

## /cli/src/client/prompts.ts

```ts path="/cli/src/client/prompts.ts" 
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { McpResponse } from "./types.js";

// List available prompts
export async function listPrompts(client: Client): Promise<McpResponse> {
  try {
    const response = await client.listPrompts();
    return response;
  } catch (error) {
    throw new Error(
      `Failed to list prompts: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

// Get a prompt
export async function getPrompt(
  client: Client,
  name: string,
  args?: Record<string, string>,
): Promise<McpResponse> {
  try {
    const response = await client.getPrompt({
      name,
      arguments: args || {},
    });

    return response;
  } catch (error) {
    throw new Error(
      `Failed to get prompt: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

```

## /cli/src/client/resources.ts

```ts path="/cli/src/client/resources.ts" 
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { McpResponse } from "./types.js";

// List available resources
export async function listResources(client: Client): Promise<McpResponse> {
  try {
    const response = await client.listResources();
    return response;
  } catch (error) {
    throw new Error(
      `Failed to list resources: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

// Read a resource
export async function readResource(
  client: Client,
  uri: string,
): Promise<McpResponse> {
  try {
    const response = await client.readResource({ uri });
    return response;
  } catch (error) {
    throw new Error(
      `Failed to read resource ${uri}: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

// List resource templates
export async function listResourceTemplates(
  client: Client,
): Promise<McpResponse> {
  try {
    const response = await client.listResourceTemplates();
    return response;
  } catch (error) {
    throw new Error(
      `Failed to list resource templates: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

```

## /cli/src/client/tools.ts

```ts path="/cli/src/client/tools.ts" 
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import { McpResponse } from "./types.js";

type JsonSchemaType = {
  type: "string" | "number" | "integer" | "boolean" | "array" | "object";
  description?: string;
  properties?: Record<string, JsonSchemaType>;
  items?: JsonSchemaType;
};

export async function listTools(client: Client): Promise<McpResponse> {
  try {
    const response = await client.listTools();
    return response;
  } catch (error) {
    throw new Error(
      `Failed to list tools: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

function convertParameterValue(value: string, schema: JsonSchemaType): unknown {
  if (!value) {
    return value;
  }

  if (schema.type === "number" || schema.type === "integer") {
    return Number(value);
  }

  if (schema.type === "boolean") {
    return value.toLowerCase() === "true";
  }

  if (schema.type === "object" || schema.type === "array") {
    try {
      return JSON.parse(value);
    } catch (error) {
      return value;
    }
  }

  return value;
}

function convertParameters(
  tool: Tool,
  params: Record<string, string>,
): Record<string, unknown> {
  const result: Record<string, unknown> = {};
  const properties = tool.inputSchema.properties || {};

  for (const [key, value] of Object.entries(params)) {
    const paramSchema = properties[key] as JsonSchemaType | undefined;

    if (paramSchema) {
      result[key] = convertParameterValue(value, paramSchema);
    } else {
      // If no schema is found for this parameter, keep it as string
      result[key] = value;
    }
  }

  return result;
}

export async function callTool(
  client: Client,
  name: string,
  args: Record<string, string>,
): Promise<McpResponse> {
  try {
    const toolsResponse = await listTools(client);
    const tools = toolsResponse.tools as Tool[];
    const tool = tools.find((t) => t.name === name);

    let convertedArgs: Record<string, unknown> = args;

    if (tool) {
      // Convert parameters based on the tool's schema
      convertedArgs = convertParameters(tool, args);
    }

    const response = await client.callTool({
      name: name,
      arguments: convertedArgs,
    });
    return response;
  } catch (error) {
    throw new Error(
      `Failed to call tool ${name}: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

```

## /cli/src/client/types.ts

```ts path="/cli/src/client/types.ts" 
export type McpResponse = Record<string, unknown>;

```

## /cli/src/error-handler.ts

```ts path="/cli/src/error-handler.ts" 
function formatError(error: unknown): string {
  let message: string;

  if (error instanceof Error) {
    message = error.message;
  } else if (typeof error === "string") {
    message = error;
  } else {
    message = "Unknown error";
  }

  return message;
}

export function handleError(error: unknown): never {
  const errorMessage = formatError(error);
  console.error(errorMessage);

  process.exit(1);
}

```

## /cli/src/index.ts

```ts path="/cli/src/index.ts" 
#!/usr/bin/env node

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { Command } from "commander";
import {
  callTool,
  connect,
  disconnect,
  getPrompt,
  listPrompts,
  listResources,
  listResourceTemplates,
  listTools,
  LogLevel,
  McpResponse,
  readResource,
  setLoggingLevel,
  validLogLevels,
} from "./client/index.js";
import { handleError } from "./error-handler.js";
import { createTransport, TransportOptions } from "./transport.js";

type Args = {
  target: string[];
  method?: string;
  promptName?: string;
  promptArgs?: Record<string, string>;
  uri?: string;
  logLevel?: LogLevel;
  toolName?: string;
  toolArg?: Record<string, string>;
  transport?: "sse" | "stdio" | "http";
};

function createTransportOptions(
  target: string[],
  transport?: "sse" | "stdio" | "http",
): TransportOptions {
  if (target.length === 0) {
    throw new Error(
      "Target is required. Specify a URL or a command to execute.",
    );
  }

  const [command, ...commandArgs] = target;

  if (!command) {
    throw new Error("Command is required.");
  }

  const isUrl = command.startsWith("http://") || command.startsWith("https://");

  if (isUrl && commandArgs.length > 0) {
    throw new Error("Arguments cannot be passed to a URL-based MCP server.");
  }

  let transportType: "sse" | "stdio" | "http";
  if (transport) {
    if (!isUrl && transport !== "stdio") {
      throw new Error("Only stdio transport can be used with local commands.");
    }
    if (isUrl && transport === "stdio") {
      throw new Error("stdio transport cannot be used with URLs.");
    }
    transportType = transport;
  } else if (isUrl) {
    const url = new URL(command);
    if (url.pathname.endsWith("/mcp")) {
      transportType = "http";
    } else if (url.pathname.endsWith("/sse")) {
      transportType = "sse";
    } else {
      transportType = "sse";
    }
  } else {
    transportType = "stdio";
  }

  return {
    transportType,
    command: isUrl ? undefined : command,
    args: isUrl ? undefined : commandArgs,
    url: isUrl ? command : undefined,
  };
}

async function callMethod(args: Args): Promise<void> {
  const transportOptions = createTransportOptions(args.target, args.transport);
  const transport = createTransport(transportOptions);
  const client = new Client({
    name: "inspector-cli",
    version: "0.5.1",
  });

  try {
    await connect(client, transport);

    let result: McpResponse;

    // Tools methods
    if (args.method === "tools/list") {
      result = await listTools(client);
    } else if (args.method === "tools/call") {
      if (!args.toolName) {
        throw new Error(
          "Tool name is required for tools/call method. Use --tool-name to specify the tool name.",
        );
      }

      result = await callTool(client, args.toolName, args.toolArg || {});
    }
    // Resources methods
    else if (args.method === "resources/list") {
      result = await listResources(client);
    } else if (args.method === "resources/read") {
      if (!args.uri) {
        throw new Error(
          "URI is required for resources/read method. Use --uri to specify the resource URI.",
        );
      }

      result = await readResource(client, args.uri);
    } else if (args.method === "resources/templates/list") {
      result = await listResourceTemplates(client);
    }
    // Prompts methods
    else if (args.method === "prompts/list") {
      result = await listPrompts(client);
    } else if (args.method === "prompts/get") {
      if (!args.promptName) {
        throw new Error(
          "Prompt name is required for prompts/get method. Use --prompt-name to specify the prompt name.",
        );
      }

      result = await getPrompt(client, args.promptName, args.promptArgs || {});
    }
    // Logging methods
    else if (args.method === "logging/setLevel") {
      if (!args.logLevel) {
        throw new Error(
          "Log level is required for logging/setLevel method. Use --log-level to specify the log level.",
        );
      }

      result = await setLoggingLevel(client, args.logLevel);
    } else {
      throw new Error(
        `Unsupported method: ${args.method}. Supported methods include: tools/list, tools/call, resources/list, resources/read, resources/templates/list, prompts/list, prompts/get, logging/setLevel`,
      );
    }

    console.log(JSON.stringify(result, null, 2));
  } finally {
    try {
      await disconnect(transport);
    } catch (disconnectError) {
      throw disconnectError;
    }
  }
}

function parseKeyValuePair(
  value: string,
  previous: Record<string, string> = {},
): Record<string, string> {
  const parts = value.split("=");
  const key = parts[0];
  const val = parts.slice(1).join("=");

  if (val === undefined || val === "") {
    throw new Error(
      `Invalid parameter format: ${value}. Use key=value format.`,
    );
  }

  return { ...previous, [key as string]: val };
}

function parseArgs(): Args {
  const program = new Command();

  // Find if there's a -- in the arguments and split them
  const argSeparatorIndex = process.argv.indexOf("--");
  let preArgs = process.argv;
  let postArgs: string[] = [];

  if (argSeparatorIndex !== -1) {
    preArgs = process.argv.slice(0, argSeparatorIndex);
    postArgs = process.argv.slice(argSeparatorIndex + 1);
  }

  program
    .name("inspector-cli")
    .allowUnknownOption()
    .argument("<target...>", "Command and arguments or URL of the MCP server")
    //
    // Method selection
    //
    .option("--method <method>", "Method to invoke")
    //
    // Tool-related options
    //
    .option("--tool-name <toolName>", "Tool name (for tools/call method)")
    .option(
      "--tool-arg <pairs...>",
      "Tool argument as key=value pair",
      parseKeyValuePair,
      {},
    )
    //
    // Resource-related options
    //
    .option("--uri <uri>", "URI of the resource (for resources/read method)")
    //
    // Prompt-related options
    //
    .option(
      "--prompt-name <promptName>",
      "Name of the prompt (for prompts/get method)",
    )
    .option(
      "--prompt-args <pairs...>",
      "Prompt arguments as key=value pairs",
      parseKeyValuePair,
      {},
    )
    //
    // Logging options
    //
    .option(
      "--log-level <level>",
      "Logging level (for logging/setLevel method)",
      (value: string) => {
        if (!validLogLevels.includes(value as any)) {
          throw new Error(
            `Invalid log level: ${value}. Valid levels are: ${validLogLevels.join(", ")}`,
          );
        }

        return value as LogLevel;
      },
    )
    //
    // Transport options
    //
    .option(
      "--transport <type>",
      "Transport type (sse, http, or stdio). Auto-detected from URL: /mcp → http, /sse → sse, commands → stdio",
      (value: string) => {
        const validTransports = ["sse", "http", "stdio"];
        if (!validTransports.includes(value)) {
          throw new Error(
            `Invalid transport type: ${value}. Valid types are: ${validTransports.join(", ")}`,
          );
        }
        return value as "sse" | "http" | "stdio";
      },
    );

  // Parse only the arguments before --
  program.parse(preArgs);

  const options = program.opts() as Omit<Args, "target">;
  let remainingArgs = program.args;

  // Add back any arguments that came after --
  const finalArgs = [...remainingArgs, ...postArgs];

  if (!options.method) {
    throw new Error(
      "Method is required. Use --method to specify the method to invoke.",
    );
  }

  return {
    target: finalArgs,
    ...options,
  };
}

async function main(): Promise<void> {
  process.on("uncaughtException", (error) => {
    handleError(error);
  });

  try {
    const args = parseArgs();
    await callMethod(args);
  } catch (error) {
    handleError(error);
  }
}

main();

```

## /cli/src/transport.ts

```ts path="/cli/src/transport.ts" 
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import {
  getDefaultEnvironment,
  StdioClientTransport,
} from "@modelcontextprotocol/sdk/client/stdio.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import { findActualExecutable } from "spawn-rx";

export type TransportOptions = {
  transportType: "sse" | "stdio" | "http";
  command?: string;
  args?: string[];
  url?: string;
};

function createSSETransport(options: TransportOptions): Transport {
  const baseUrl = new URL(options.url ?? "");
  const sseUrl = baseUrl.pathname.endsWith("/sse")
    ? baseUrl
    : new URL("/sse", baseUrl);

  return new SSEClientTransport(sseUrl);
}

function createHTTPTransport(options: TransportOptions): Transport {
  const baseUrl = new URL(options.url ?? "");
  const mcpUrl = baseUrl.pathname.endsWith("/mcp")
    ? baseUrl
    : new URL("/mcp", baseUrl);

  return new StreamableHTTPClientTransport(mcpUrl);
}

function createStdioTransport(options: TransportOptions): Transport {
  let args: string[] = [];

  if (options.args !== undefined) {
    args = options.args;
  }

  const processEnv: Record<string, string> = {};

  for (const [key, value] of Object.entries(process.env)) {
    if (value !== undefined) {
      processEnv[key] = value;
    }
  }

  const defaultEnv = getDefaultEnvironment();

  const env: Record<string, string> = {
    ...processEnv,
    ...defaultEnv,
  };

  const { cmd: actualCommand, args: actualArgs } = findActualExecutable(
    options.command ?? "",
    args,
  );

  return new StdioClientTransport({
    command: actualCommand,
    args: actualArgs,
    env,
    stderr: "pipe",
  });
}

export function createTransport(options: TransportOptions): Transport {
  const { transportType } = options;

  try {
    if (transportType === "stdio") {
      return createStdioTransport(options);
    }

    if (transportType === "sse") {
      return createSSETransport(options);
    }

    if (transportType === "http") {
      return createHTTPTransport(options);
    }

    throw new Error(`Unsupported transport type: ${transportType}`);
  } catch (error) {
    throw new Error(
      `Failed to create transport: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

```

## /cli/tsconfig.json

```json path="/cli/tsconfig.json" 
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "noUncheckedIndexedAccess": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "packages", "**/*.spec.ts", "build"]
}

```

## /client/.gitignore

```gitignore path="/client/.gitignore" 
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

```

## /client/README.md

# React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

## Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:

- Configure the top-level `parserOptions` property like this:

```js
export default tseslint.config({
  languageOptions: {
    // other options...
    parserOptions: {
      project: ["./tsconfig.node.json", "./tsconfig.app.json"],
      tsconfigRootDir: import.meta.dirname,
    },
  },
});
```

- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:

```js
// eslint.config.js
import react from "eslint-plugin-react";

export default tseslint.config({
  // Set the react version
  settings: { react: { version: "18.3" } },
  plugins: {
    // Add the react plugin
    react,
  },
  rules: {
    // other rules...
    // Enable its recommended rules
    ...react.configs.recommended.rules,
    ...react.configs["jsx-runtime"].rules,
  },
});
```


## /client/bin/client.js

```js path="/client/bin/client.js" 
#!/usr/bin/env node

import open from "open";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import handler from "serve-handler";
import http from "http";

const __dirname = dirname(fileURLToPath(import.meta.url));
const distPath = join(__dirname, "../dist");

const server = http.createServer((request, response) => {
  const handlerOptions = {
    public: distPath,
    rewrites: [{ source: "/**", destination: "/index.html" }],
    headers: [
      {
        // Ensure index.html is never cached
        source: "index.html",
        headers: [
          {
            key: "Cache-Control",
            value: "no-cache, no-store, max-age=0",
          },
        ],
      },
      {
        // Allow long-term caching for hashed assets
        source: "assets/**",
        headers: [
          {
            key: "Cache-Control",
            value: "public, max-age=31536000, immutable",
          },
        ],
      },
    ],
  };

  return handler(request, response, handlerOptions);
});

const port = parseInt(process.env.CLIENT_PORT || "6274", 10);
const host = process.env.HOST || "localhost";
server.on("listening", () => {
  const url = process.env.INSPECTOR_URL || `http://${host}:${port}`;
  console.log(`\n🚀 MCP Inspector is up and running at:\n   ${url}\n`);
  if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") {
    console.log(`🌐 Opening browser...`);
    open(url);
  }
});
server.on("error", (err) => {
  if (err.message.includes(`EADDRINUSE`)) {
    console.error(
      `❌  MCP Inspector PORT IS IN USE at http://${host}:${port} ❌ `,
    );
  } else {
    throw err;
  }
});
server.listen(port, host);

```

## /client/bin/start.js

```js path="/client/bin/start.js" 
#!/usr/bin/env node

import open from "open";
import { resolve, dirname } from "path";
import { spawnPromise, spawn } from "spawn-rx";
import { fileURLToPath } from "url";
import { randomBytes } from "crypto";

const __dirname = dirname(fileURLToPath(import.meta.url));
const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277";

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms, true));
}

function getClientUrl(port, authDisabled, sessionToken, serverPort) {
  const host = process.env.HOST || "localhost";
  const baseUrl = `http://${host}:${port}`;

  const params = new URLSearchParams();
  if (serverPort && serverPort !== DEFAULT_MCP_PROXY_LISTEN_PORT) {
    params.set("MCP_PROXY_PORT", serverPort);
  }
  if (!authDisabled) {
    params.set("MCP_PROXY_AUTH_TOKEN", sessionToken);
  }
  return params.size > 0 ? `${baseUrl}/?${params.toString()}` : baseUrl;
}

async function startDevServer(serverOptions) {
  const { SERVER_PORT, CLIENT_PORT, sessionToken, envVars, abort } =
    serverOptions;
  const serverCommand = "npx";
  const serverArgs = ["tsx", "watch", "--clear-screen=false", "src/index.ts"];
  const isWindows = process.platform === "win32";

  const spawnOptions = {
    cwd: resolve(__dirname, "../..", "server"),
    env: {
      ...process.env,
      SERVER_PORT,
      CLIENT_PORT,
      MCP_PROXY_TOKEN: sessionToken,
      MCP_ENV_VARS: JSON.stringify(envVars),
    },
    signal: abort.signal,
    echoOutput: true,
  };

  // For Windows, we need to use stdin: 'ignore' to simulate < NUL
  if (isWindows) {
    spawnOptions.stdin = "ignore";
  }

  const server = spawn(serverCommand, serverArgs, spawnOptions);

  // Give server time to start
  const serverOk = await Promise.race([
    new Promise((resolve) => {
      server.subscribe({
        complete: () => resolve(false),
        error: () => resolve(false),
        next: () => {}, // We're using echoOutput
      });
    }),
    delay(3000).then(() => true),
  ]);

  return { server, serverOk };
}

async function startProdServer(serverOptions) {
  const {
    SERVER_PORT,
    CLIENT_PORT,
    sessionToken,
    envVars,
    abort,
    command,
    mcpServerArgs,
  } = serverOptions;
  const inspectorServerPath = resolve(
    __dirname,
    "../..",
    "server",
    "build",
    "index.js",
  );

  const server = spawnPromise(
    "node",
    [
      inspectorServerPath,
      ...(command ? [`--env`, command] : []),
      ...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []),
    ],
    {
      env: {
        ...process.env,
        SERVER_PORT,
        CLIENT_PORT,
        MCP_PROXY_TOKEN: sessionToken,
        MCP_ENV_VARS: JSON.stringify(envVars),
      },
      signal: abort.signal,
      echoOutput: true,
    },
  );

  // Make sure server started before starting client
  const serverOk = await Promise.race([server, delay(2 * 1000)]);

  return { server, serverOk };
}

async function startDevClient(clientOptions) {
  const {
    CLIENT_PORT,
    SERVER_PORT,
    authDisabled,
    sessionToken,
    abort,
    cancelled,
  } = clientOptions;
  const clientCommand = "npx";
  const host = process.env.HOST || "localhost";
  const clientArgs = ["vite", "--port", CLIENT_PORT, "--host", host];

  const client = spawn(clientCommand, clientArgs, {
    cwd: resolve(__dirname, ".."),
    env: { ...process.env, CLIENT_PORT },
    signal: abort.signal,
    echoOutput: true,
  });

  const url = getClientUrl(
    CLIENT_PORT,
    authDisabled,
    sessionToken,
    SERVER_PORT,
  );

  // Give vite time to start before opening or logging the URL
  setTimeout(() => {
    console.log(`\n🚀 MCP Inspector is up and running at:\n   ${url}\n`);
    if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") {
      console.log("🌐 Opening browser...");
      open(url);
    }
  }, 3000);

  await new Promise((resolve) => {
    client.subscribe({
      complete: resolve,
      error: (err) => {
        if (!cancelled || process.env.DEBUG) {
          console.error("Client error:", err);
        }
        resolve(null);
      },
      next: () => {}, // We're using echoOutput
    });
  });
}

async function startProdClient(clientOptions) {
  const {
    CLIENT_PORT,
    SERVER_PORT,
    authDisabled,
    sessionToken,
    abort,
    cancelled,
  } = clientOptions;
  const inspectorClientPath = resolve(
    __dirname,
    "../..",
    "client",
    "bin",
    "client.js",
  );

  const url = getClientUrl(
    CLIENT_PORT,
    authDisabled,
    sessionToken,
    SERVER_PORT,
  );

  await spawnPromise("node", [inspectorClientPath], {
    env: {
      ...process.env,
      CLIENT_PORT,
      INSPECTOR_URL: url,
    },
    signal: abort.signal,
    echoOutput: true,
  });
}

async function main() {
  // Parse command line arguments
  const args = process.argv.slice(2);
  const envVars = {};
  const mcpServerArgs = [];
  let command = null;
  let parsingFlags = true;
  let isDev = false;

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];

    if (parsingFlags && arg === "--") {
      parsingFlags = false;
      continue;
    }

    if (parsingFlags && arg === "--dev") {
      isDev = true;
      continue;
    }

    if (parsingFlags && arg === "-e" && i + 1 < args.length) {
      const envVar = args[++i];
      const equalsIndex = envVar.indexOf("=");

      if (equalsIndex !== -1) {
        const key = envVar.substring(0, equalsIndex);
        const value = envVar.substring(equalsIndex + 1);
        envVars[key] = value;
      } else {
        envVars[envVar] = "";
      }
    } else if (!command && !isDev) {
      command = arg;
    } else if (!isDev) {
      mcpServerArgs.push(arg);
    }
  }

  const CLIENT_PORT = process.env.CLIENT_PORT ?? "6274";
  const SERVER_PORT = process.env.SERVER_PORT ?? DEFAULT_MCP_PROXY_LISTEN_PORT;

  console.log(
    isDev
      ? "Starting MCP inspector in development mode..."
      : "Starting MCP inspector...",
  );

  // Generate session token for authentication
  const sessionToken = randomBytes(32).toString("hex");
  const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH;

  const abort = new AbortController();

  let cancelled = false;
  process.on("SIGINT", () => {
    cancelled = true;
    abort.abort();
  });

  let server, serverOk;

  try {
    const serverOptions = {
      SERVER_PORT,
      CLIENT_PORT,
      sessionToken,
      envVars,
      abort,
      command,
      mcpServerArgs,
    };

    const result = isDev
      ? await startDevServer(serverOptions)
      : await startProdServer(serverOptions);

    server = result.server;
    serverOk = result.serverOk;
  } catch (error) {}

  if (serverOk) {
    try {
      const clientOptions = {
        CLIENT_PORT,
        SERVER_PORT,
        authDisabled,
        sessionToken,
        abort,
        cancelled,
      };

      await (isDev
        ? startDevClient(clientOptions)
        : startProdClient(clientOptions));
    } catch (e) {
      if (!cancelled || process.env.DEBUG) throw e;
    }
  }

  return 0;
}

main()
  .then((_) => process.exit(0))
  .catch((e) => {
    console.error(e);
    process.exit(1);
  });

```

## /client/components.json

```json path="/client/components.json" 
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": false,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.js",
    "css": "src/index.css",
    "baseColor": "slate",
    "cssVariables": true,
    "prefix": ""
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  }
}

```

## /client/e2e/global-teardown.js

```js path="/client/e2e/global-teardown.js" 
import { rimraf } from "rimraf";

async function globalTeardown() {
  if (!process.env.CI) {
    console.log("Cleaning up test-results directory...");
    // Add a small delay to ensure all Playwright files are written
    await new Promise((resolve) => setTimeout(resolve, 100));
    await rimraf("./e2e/test-results");
    console.log("Test-results directory cleaned up.");
  }
}

export default globalTeardown;

// Call the function when this script is run directly
if (import.meta.url === `file://${process.argv[1]}`) {
  globalTeardown().catch(console.error);
}

```

## /client/e2e/transport-type-dropdown.spec.ts

```ts path="/client/e2e/transport-type-dropdown.spec.ts" 
import { test, expect } from "@playwright/test";

// Adjust the URL if your dev server runs on a different port
const APP_URL = "http://localhost:6274/";

test.describe("Transport Type Dropdown", () => {
  test("should have options for STDIO, SSE, and Streamable HTTP", async ({
    page,
  }) => {
    await page.goto(APP_URL);

    // Wait for the Transport Type dropdown to be visible
    const selectTrigger = page.getByLabel("Transport Type");
    await expect(selectTrigger).toBeVisible();

    // Open the dropdown
    await selectTrigger.click();

    // Check for the three options
    await expect(page.getByRole("option", { name: "STDIO" })).toBeVisible();
    await expect(page.getByRole("option", { name: "SSE" })).toBeVisible();
    await expect(
      page.getByRole("option", { name: "Streamable HTTP" }),
    ).toBeVisible();
  });

  test("should show Command and Arguments fields and hide URL field when Transport Type is STDIO", async ({
    page,
  }) => {
    await page.goto(APP_URL);

    // Wait for the Transport Type dropdown to be visible
    const selectTrigger = page.getByLabel("Transport Type");
    await expect(selectTrigger).toBeVisible();

    // Open the dropdown and select STDIO
    await selectTrigger.click();
    await page.getByRole("option", { name: "STDIO" }).click();

    // Wait for the form to update
    await page.waitForTimeout(100);

    // Check that Command and Arguments fields are visible
    await expect(page.locator("#command-input")).toBeVisible();
    await expect(page.locator("#arguments-input")).toBeVisible();

    // Check that URL field is not visible
    await expect(page.locator("#sse-url-input")).not.toBeVisible();

    // Also verify the labels are present
    await expect(page.getByText("Command")).toBeVisible();
    await expect(page.getByText("Arguments")).toBeVisible();
    await expect(page.getByText("URL")).not.toBeVisible();
  });

  test("should show URL field and hide Command and Arguments fields when Transport Type is SSE", async ({
    page,
  }) => {
    await page.goto(APP_URL);

    // Wait for the Transport Type dropdown to be visible
    const selectTrigger = page.getByLabel("Transport Type");
    await expect(selectTrigger).toBeVisible();

    // Open the dropdown and select SSE
    await selectTrigger.click();
    await page.getByRole("option", { name: "SSE" }).click();

    // Wait for the form to update
    await page.waitForTimeout(100);

    // Check that URL field is visible
    await expect(page.locator("#sse-url-input")).toBeVisible();

    // Check that Command and Arguments fields are not visible
    await expect(page.locator("#command-input")).not.toBeVisible();
    await expect(page.locator("#arguments-input")).not.toBeVisible();

    // Also verify the labels are present/absent
    await expect(page.getByText("URL")).toBeVisible();
    await expect(page.getByText("Command")).not.toBeVisible();
    await expect(page.getByText("Arguments")).not.toBeVisible();
  });

  test("should show URL field and hide Command and Arguments fields when Transport Type is Streamable HTTP", async ({
    page,
  }) => {
    await page.goto(APP_URL);

    // Wait for the Transport Type dropdown to be visible
    const selectTrigger = page.getByLabel("Transport Type");
    await expect(selectTrigger).toBeVisible();

    // Open the dropdown and select Streamable HTTP
    await selectTrigger.click();
    await page.getByRole("option", { name: "Streamable HTTP" }).click();

    // Wait for the form to update
    await page.waitForTimeout(100);

    // Check that URL field is visible
    await expect(page.locator("#sse-url-input")).toBeVisible();

    // Check that Command and Arguments fields are not visible
    await expect(page.locator("#command-input")).not.toBeVisible();
    await expect(page.locator("#arguments-input")).not.toBeVisible();

    // Also verify the labels are present/absent
    await expect(page.getByText("URL")).toBeVisible();
    await expect(page.getByText("Command")).not.toBeVisible();
    await expect(page.getByText("Arguments")).not.toBeVisible();
  });
});

```

## /client/eslint.config.js

```js path="/client/eslint.config.js" 
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";

export default tseslint.config(
  { ignores: ["dist"] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
    files: ["**/*.{ts,tsx}"],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
    plugins: {
      "react-hooks": reactHooks,
      "react-refresh": reactRefresh,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      "react-refresh/only-export-components": [
        "warn",
        { allowConstantExport: true },
      ],
    },
  },
);

```

## /client/index.html

```html path="/client/index.html" 
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/mcp.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>MCP Inspector</title>
  </head>
  <body>
    <div id="root" class="w-full"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

## /client/jest.config.cjs

```cjs path="/client/jest.config.cjs" 
module.exports = {
  preset: "ts-jest",
  testEnvironment: "jest-fixed-jsdom",
  moduleNameMapper: {
    "^@/(.*)$": "<rootDir>/src/$1",
    "\\.css$": "<rootDir>/src/__mocks__/styleMock.js",
  },
  transform: {
    "^.+\\.tsx?$": [
      "ts-jest",
      {
        jsx: "react-jsx",
        tsconfig: "tsconfig.jest.json",
      },
    ],
  },
  extensionsToTreatAsEsm: [".ts", ".tsx"],
  testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
  // Exclude directories and files that don't need to be tested
  testPathIgnorePatterns: [
    "/node_modules/",
    "/dist/",
    "/bin/",
    "/e2e/",
    "\\.config\\.(js|ts|cjs|mjs)$",
  ],
  // Exclude the same patterns from coverage reports
  coveragePathIgnorePatterns: [
    "/node_modules/",
    "/dist/",
    "/bin/",
    "/e2e/",
    "\\.config\\.(js|ts|cjs|mjs)$",
  ],
};

```

## /client/package.json

```json path="/client/package.json" 
{
  "name": "@modelcontextprotocol/inspector-client",
  "version": "0.15.0",
  "description": "Client-side application for the Model Context Protocol inspector",
  "license": "MIT",
  "author": "Anthropic, PBC (https://anthropic.com)",
  "homepage": "https://modelcontextprotocol.io",
  "bugs": "https://github.com/modelcontextprotocol/inspector/issues",
  "type": "module",
  "bin": {
    "mcp-inspector-client": "./bin/start.js"
  },
  "files": [
    "bin",
    "dist"
  ],
  "scripts": {
    "dev": "vite --port 6274",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview --port 6274",
    "test": "jest --config jest.config.cjs",
    "test:watch": "jest --config jest.config.cjs --watch",
    "test:e2e": "playwright test e2e && npm run cleanup:e2e",
    "cleanup:e2e": "node e2e/global-teardown.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.13.1",
    "@radix-ui/react-checkbox": "^1.1.4",
    "ajv": "^6.12.6",
    "@radix-ui/react-dialog": "^1.1.3",
    "@radix-ui/react-icons": "^1.3.0",
    "@radix-ui/react-label": "^2.1.0",
    "@radix-ui/react-popover": "^1.1.3",
    "@radix-ui/react-select": "^2.1.2",
    "@radix-ui/react-slot": "^1.1.0",
    "@radix-ui/react-tabs": "^1.1.1",
    "@radix-ui/react-toast": "^1.2.6",
    "@radix-ui/react-tooltip": "^1.1.8",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.1",
    "cmdk": "^1.0.4",
    "lucide-react": "^0.523.0",
    "pkce-challenge": "^4.1.0",
    "prismjs": "^1.30.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-simple-code-editor": "^0.14.1",
    "serve-handler": "^6.1.6",
    "tailwind-merge": "^2.5.3",
    "tailwindcss-animate": "^1.0.7",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@eslint/js": "^9.11.1",
    "@testing-library/jest-dom": "^6.6.3",
    "@testing-library/react": "^16.2.0",
    "@types/jest": "^29.5.14",
    "@types/node": "^22.7.5",
    "@types/prismjs": "^1.26.5",
    "@types/react": "^18.3.10",
    "@types/react-dom": "^18.3.0",
    "@types/serve-handler": "^6.1.4",
    "@vitejs/plugin-react": "^4.3.2",
    "autoprefixer": "^10.4.20",
    "co": "^4.6.0",
    "eslint": "^9.11.1",
    "eslint-plugin-react-hooks": "^5.1.0-rc.0",
    "eslint-plugin-react-refresh": "^0.4.12",
    "globals": "^15.9.0",
    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0",
    "postcss": "^8.4.47",
    "tailwindcss": "^3.4.13",
    "ts-jest": "^29.2.6",
    "typescript": "^5.5.3",
    "typescript-eslint": "^8.7.0",
    "vite": "^6.3.0"
  }
}

```

## /client/playwright.config.ts

```ts path="/client/playwright.config.ts" 
import { defineConfig, devices } from "@playwright/test";

/**
 * @see https://playwright.dev/docs/test-configuration
 */
export default defineConfig({
  /* Run your local dev server before starting the tests */
  webServer: {
    cwd: "..",
    command: "npm run dev",
    url: "http://localhost:6274",
    reuseExistingServer: !process.env.CI,
  },

  testDir: "./e2e",
  outputDir: "./e2e/test-results",
  /* Run tests in files in parallel */
  fullyParallel: true,
  /* Fail the build on CI if you accidentally left test.only in the source code. */
  forbidOnly: !!process.env.CI,
  /* Retry on CI only */
  retries: process.env.CI ? 2 : 0,
  /* Opt out of parallel tests on CI. */
  workers: process.env.CI ? 1 : undefined,
  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
  reporter: process.env.CI
    ? [
        ["html", { outputFolder: "playwright-report" }],
        ["json", { outputFile: "results.json" }],
        ["line"],
      ]
    : [["line"]],
  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
  use: {
    /* Base URL to use in actions like `await page.goto('/')`. */
    baseURL: "http://localhost:6274",

    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
    trace: "on-first-retry",

    /* Take screenshots on failure */
    screenshot: "only-on-failure",

    /* Record video on failure */
    video: "retain-on-failure",
  },

  /* Configure projects for major browsers */
  projects: [
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"] },
    },

    {
      name: "firefox",
      use: { ...devices["Desktop Firefox"] },
    },

    // Skip WebKit on macOS due to compatibility issues
    ...(process.platform !== "darwin"
      ? [
          {
            name: "webkit",
            use: { ...devices["Desktop Safari"] },
          },
        ]
      : []),
  ],
});

```

## /client/postcss.config.js

```js path="/client/postcss.config.js" 
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

```

## /client/public/mcp.svg

```svg path="/client/public/mcp.svg" 
<svg width="180" height="180" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_19_13)">
<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"/>
<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"/>
<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"/>
</g>
<defs>
<clipPath id="clip0_19_13">
<rect width="180" height="180" fill="white"/>
</clipPath>
</defs>
</svg>

```

## /client/src/App.css

```css path="/client/src/App.css" 
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
  filter: drop-shadow(0 0 2em #61dafbaa);
}

@keyframes logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: no-preference) {
  a:nth-of-type(2) .logo {
    animation: logo-spin infinite 20s linear;
  }
}

.card {
  padding: 2em;
}

.read-the-docs {
  color: #888;
}

```

## /client/src/App.tsx

```tsx path="/client/src/App.tsx" 
import {
  ClientRequest,
  CompatibilityCallToolResult,
  CompatibilityCallToolResultSchema,
  CreateMessageResult,
  EmptyResultSchema,
  GetPromptResultSchema,
  ListPromptsResultSchema,
  ListResourcesResultSchema,
  ListResourceTemplatesResultSchema,
  ListToolsResultSchema,
  ReadResourceResultSchema,
  Resource,
  ResourceTemplate,
  Root,
  ServerNotification,
  Tool,
  LoggingLevel,
} from "@modelcontextprotocol/sdk/types.js";
import { OAuthTokensSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
import { SESSION_KEYS, getServerSpecificKey } from "./lib/constants";
import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "./lib/auth-types";
import { OAuthStateMachine } from "./lib/oauth-state-machine";
import { cacheToolOutputSchemas } from "./utils/schemaUtils";
import React, {
  Suspense,
  useCallback,
  useEffect,
  useRef,
  useState,
} from "react";
import { useConnection } from "./lib/hooks/useConnection";
import {
  useDraggablePane,
  useDraggableSidebar,
} from "./lib/hooks/useDraggablePane";
import { StdErrNotification } from "./lib/notificationTypes";

import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import {
  Bell,
  Files,
  FolderTree,
  Hammer,
  Hash,
  Key,
  MessageSquare,
} from "lucide-react";

import { z } from "zod";
import "./App.css";
import AuthDebugger from "./components/AuthDebugger";
import ConsoleTab from "./components/ConsoleTab";
import HistoryAndNotifications from "./components/HistoryAndNotifications";
import PingTab from "./components/PingTab";
import PromptsTab, { Prompt } from "./components/PromptsTab";
import ResourcesTab from "./components/ResourcesTab";
import RootsTab from "./components/RootsTab";
import SamplingTab, { PendingRequest } from "./components/SamplingTab";
import Sidebar from "./components/Sidebar";
import ToolsTab from "./components/ToolsTab";
import { InspectorConfig } from "./lib/configurationTypes";
import {
  getMCPProxyAddress,
  getMCPProxyAuthToken,
  getInitialSseUrl,
  getInitialTransportType,
  getInitialCommand,
  getInitialArgs,
  initializeInspectorConfig,
  saveInspectorConfig,
} from "./utils/configUtils";

const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1";

const App = () => {
  const [resources, setResources] = useState<Resource[]>([]);
  const [resourceTemplates, setResourceTemplates] = useState<
    ResourceTemplate[]
  >([]);
  const [resourceContent, setResourceContent] = useState<string>("");
  const [prompts, setPrompts] = useState<Prompt[]>([]);
  const [promptContent, setPromptContent] = useState<string>("");
  const [tools, setTools] = useState<Tool[]>([]);
  const [toolResult, setToolResult] =
    useState<CompatibilityCallToolResult | null>(null);
  const [errors, setErrors] = useState<Record<string, string | null>>({
    resources: null,
    prompts: null,
    tools: null,
  });
  const [command, setCommand] = useState<string>(getInitialCommand);
  const [args, setArgs] = useState<string>(getInitialArgs);

  const [sseUrl, setSseUrl] = useState<string>(getInitialSseUrl);
  const [transportType, setTransportType] = useState<
    "stdio" | "sse" | "streamable-http"
  >(getInitialTransportType);
  const [logLevel, setLogLevel] = useState<LoggingLevel>("debug");
  const [notifications, setNotifications] = useState<ServerNotification[]>([]);
  const [stdErrNotifications, setStdErrNotifications] = useState<
    StdErrNotification[]
  >([]);
  const [roots, setRoots] = useState<Root[]>([]);
  const [env, setEnv] = useState<Record<string, string>>({});

  const [config, setConfig] = useState<InspectorConfig>(() =>
    initializeInspectorConfig(CONFIG_LOCAL_STORAGE_KEY),
  );
  const [bearerToken, setBearerToken] = useState<string>(() => {
    return localStorage.getItem("lastBearerToken") || "";
  });

  const [headerName, setHeaderName] = useState<string>(() => {
    return localStorage.getItem("lastHeaderName") || "";
  });

  const [pendingSampleRequests, setPendingSampleRequests] = useState<
    Array<
      PendingRequest & {
        resolve: (result: CreateMessageResult) => void;
        reject: (error: Error) => void;
      }
    >
  >([]);
  const [isAuthDebuggerVisible, setIsAuthDebuggerVisible] = useState(false);

  // Auth debugger state
  const [authState, setAuthState] =
    useState<AuthDebuggerState>(EMPTY_DEBUGGER_STATE);

  // Helper function to update specific auth state properties
  const updateAuthState = (updates: Partial<AuthDebuggerState>) => {
    setAuthState((prev) => ({ ...prev, ...updates }));
  };
  const nextRequestId = useRef(0);
  const rootsRef = useRef<Root[]>([]);

  const [selectedResource, setSelectedResource] = useState<Resource | null>(
    null,
  );
  const [resourceSubscriptions, setResourceSubscriptions] = useState<
    Set<string>
  >(new Set<string>());

  const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
  const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
  const [nextResourceCursor, setNextResourceCursor] = useState<
    string | undefined
  >();
  const [nextResourceTemplateCursor, setNextResourceTemplateCursor] = useState<
    string | undefined
  >();
  const [nextPromptCursor, setNextPromptCursor] = useState<
    string | undefined
  >();
  const [nextToolCursor, setNextToolCursor] = useState<string | undefined>();
  const progressTokenRef = useRef(0);

  const { height: historyPaneHeight, handleDragStart } = useDraggablePane(300);
  const {
    width: sidebarWidth,
    isDragging: isSidebarDragging,
    handleDragStart: handleSidebarDragStart,
  } = useDraggableSidebar(320);

  const {
    connectionStatus,
    serverCapabilities,
    mcpClient,
    requestHistory,
    makeRequest,
    sendNotification,
    handleCompletion,
    completionsSupported,
    connect: connectMcpServer,
    disconnect: disconnectMcpServer,
  } = useConnection({
    transportType,
    command,
    args,
    sseUrl,
    env,
    bearerToken,
    headerName,
    config,
    onNotification: (notification) => {
      setNotifications((prev) => [...prev, notification as ServerNotification]);
    },
    onStdErrNotification: (notification) => {
      setStdErrNotifications((prev) => [
        ...prev,
        notification as StdErrNotification,
      ]);
    },
    onPendingRequest: (request, resolve, reject) => {
      setPendingSampleRequests((prev) => [
        ...prev,
        { id: nextRequestId.current++, request, resolve, reject },
      ]);
    },
    getRoots: () => rootsRef.current,
  });

  useEffect(() => {
    localStorage.setItem("lastCommand", command);
  }, [command]);

  useEffect(() => {
    localStorage.setItem("lastArgs", args);
  }, [args]);

  useEffect(() => {
    localStorage.setItem("lastSseUrl", sseUrl);
  }, [sseUrl]);

  useEffect(() => {
    localStorage.setItem("lastTransportType", transportType);
  }, [transportType]);

  useEffect(() => {
    localStorage.setItem("lastBearerToken", bearerToken);
  }, [bearerToken]);

  useEffect(() => {
    localStorage.setItem("lastHeaderName", headerName);
  }, [headerName]);

  useEffect(() => {
    saveInspectorConfig(CONFIG_LOCAL_STORAGE_KEY, config);
  }, [config]);

  // Auto-connect to previously saved serverURL after OAuth callback
  const onOAuthConnect = useCallback(
    (serverUrl: string) => {
      setSseUrl(serverUrl);
      setIsAuthDebuggerVisible(false);
      void connectMcpServer();
    },
    [connectMcpServer],
  );

  // Update OAuth debug state during debug callback
  const onOAuthDebugConnect = useCallback(
    async ({
      authorizationCode,
      errorMsg,
      restoredState,
    }: {
      authorizationCode?: string;
      errorMsg?: string;
      restoredState?: AuthDebuggerState;
    }) => {
      setIsAuthDebuggerVisible(true);

      if (errorMsg) {
        updateAuthState({
          latestError: new Error(errorMsg),
        });
        return;
      }

      if (restoredState && authorizationCode) {
        // Restore the previous auth state and continue the OAuth flow
        let currentState: AuthDebuggerState = {
          ...restoredState,
          authorizationCode,
          oauthStep: "token_request",
          isInitiatingAuth: true,
          statusMessage: null,
          latestError: null,
        };

        try {
          // Create a new state machine instance to continue the flow
          const stateMachine = new OAuthStateMachine(sseUrl, (updates) => {
            currentState = { ...currentState, ...updates };
          });

          // Continue stepping through the OAuth flow from where we left off
          while (
            currentState.oauthStep !== "complete" &&
            currentState.oauthStep !== "authorization_code"
          ) {
            await stateMachine.executeStep(currentState);
          }

          if (currentState.oauthStep === "complete") {
            // After the flow completes or reaches a user-input step, update the app state
            updateAuthState({
              ...currentState,
              statusMessage: {
                type: "success",
                message: "Authentication completed successfully",
              },
              isInitiatingAuth: false,
            });
          }
        } catch (error) {
          console.error("OAuth continuation error:", error);
          updateAuthState({
            latestError:
              error instanceof Error ? error : new Error(String(error)),
            statusMessage: {
              type: "error",
              message: `Failed to complete OAuth flow: ${error instanceof Error ? error.message : String(error)}`,
            },
            isInitiatingAuth: false,
          });
        }
      } else if (authorizationCode) {
        // Fallback to the original behavior if no state was restored
        updateAuthState({
          authorizationCode,
          oauthStep: "token_request",
        });
      }
    },
    [sseUrl],
  );

  // Load OAuth tokens when sseUrl changes
  useEffect(() => {
    const loadOAuthTokens = async () => {
      try {
        if (sseUrl) {
          const key = getServerSpecificKey(SESSION_KEYS.TOKENS, sseUrl);
          const tokens = sessionStorage.getItem(key);
          if (tokens) {
            const parsedTokens = await OAuthTokensSchema.parseAsync(
              JSON.parse(tokens),
            );
            updateAuthState({
              oauthTokens: parsedTokens,
              oauthStep: "complete",
            });
          }
        }
      } catch (error) {
        console.error("Error loading OAuth tokens:", error);
      }
    };

    loadOAuthTokens();
  }, [sseUrl]);

  useEffect(() => {
    const headers: HeadersInit = {};
    const { token: proxyAuthToken, header: proxyAuthTokenHeader } =
      getMCPProxyAuthToken(config);
    if (proxyAuthToken) {
      headers[proxyAuthTokenHeader] = `Bearer ${proxyAuthToken}`;
    }

    fetch(`${getMCPProxyAddress(config)}/config`, { headers })
      .then((response) => response.json())
      .then((data) => {
        setEnv(data.defaultEnvironment);
        if (data.defaultCommand) {
          setCommand(data.defaultCommand);
        }
        if (data.defaultArgs) {
          setArgs(data.defaultArgs);
        }
      })
      .catch((error) =>
        console.error("Error fetching default environment:", error),
      );
  }, [config]);

  useEffect(() => {
    rootsRef.current = roots;
  }, [roots]);

  useEffect(() => {
    if (!window.location.hash) {
      window.location.hash = "resources";
    }
  }, []);

  const handleApproveSampling = (id: number, result: CreateMessageResult) => {
    setPendingSampleRequests((prev) => {
      const request = prev.find((r) => r.id === id);
      request?.resolve(result);
      return prev.filter((r) => r.id !== id);
    });
  };

  const handleRejectSampling = (id: number) => {
    setPendingSampleRequests((prev) => {
      const request = prev.find((r) => r.id === id);
      request?.reject(new Error("Sampling request rejected"));
      return prev.filter((r) => r.id !== id);
    });
  };

  const clearError = (tabKey: keyof typeof errors) => {
    setErrors((prev) => ({ ...prev, [tabKey]: null }));
  };

  const sendMCPRequest = async <T extends z.ZodType>(
    request: ClientRequest,
    schema: T,
    tabKey?: keyof typeof errors,
  ) => {
    try {
      const response = await makeRequest(request, schema);
      if (tabKey !== undefined) {
        clearError(tabKey);
      }
      return response;
    } catch (e) {
      const errorString = (e as Error).message ?? String(e);
      if (tabKey !== undefined) {
        setErrors((prev) => ({
          ...prev,
          [tabKey]: errorString,
        }));
      }
      throw e;
    }
  };

  const listResources = async () => {
    const response = await sendMCPRequest(
      {
        method: "resources/list" as const,
        params: nextResourceCursor ? { cursor: nextResourceCursor } : {},
      },
      ListResourcesResultSchema,
      "resources",
    );
    setResources(resources.concat(response.resources ?? []));
    setNextResourceCursor(response.nextCursor);
  };

  const listResourceTemplates = async () => {
    const response = await sendMCPRequest(
      {
        method: "resources/templates/list" as const,
        params: nextResourceTemplateCursor
          ? { cursor: nextResourceTemplateCursor }
          : {},
      },
      ListResourceTemplatesResultSchema,
      "resources",
    );
    setResourceTemplates(
      resourceTemplates.concat(response.resourceTemplates ?? []),
    );
    setNextResourceTemplateCursor(response.nextCursor);
  };

  const readResource = async (uri: string) => {
    const response = await sendMCPRequest(
      {
        method: "resources/read" as const,
        params: { uri },
      },
      ReadResourceResultSchema,
      "resources",
    );
    setResourceContent(JSON.stringify(response, null, 2));
  };

  const subscribeToResource = async (uri: string) => {
    if (!resourceSubscriptions.has(uri)) {
      await sendMCPRequest(
        {
          method: "resources/subscribe" as const,
          params: { uri },
        },
        z.object({}),
        "resources",
      );
      const clone = new Set(resourceSubscriptions);
      clone.add(uri);
      setResourceSubscriptions(clone);
    }
  };

  const unsubscribeFromResource = async (uri: string) => {
    if (resourceSubscriptions.has(uri)) {
      await sendMCPRequest(
        {
          method: "resources/unsubscribe" as const,
          params: { uri },
        },
        z.object({}),
        "resources",
      );
      const clone = new Set(resourceSubscriptions);
      clone.delete(uri);
      setResourceSubscriptions(clone);
    }
  };

  const listPrompts = async () => {
    const response = await sendMCPRequest(
      {
        method: "prompts/list" as const,
        params: nextPromptCursor ? { cursor: nextPromptCursor } : {},
      },
      ListPromptsResultSchema,
      "prompts",
    );
    setPrompts(response.prompts);
    setNextPromptCursor(response.nextCursor);
  };

  const getPrompt = async (name: string, args: Record<string, string> = {}) => {
    const response = await sendMCPRequest(
      {
        method: "prompts/get" as const,
        params: { name, arguments: args },
      },
      GetPromptResultSchema,
      "prompts",
    );
    setPromptContent(JSON.stringify(response, null, 2));
  };

  const listTools = async () => {
    const response = await sendMCPRequest(
      {
        method: "tools/list" as const,
        params: nextToolCursor ? { cursor: nextToolCursor } : {},
      },
      ListToolsResultSchema,
      "tools",
    );
    setTools(response.tools);
    setNextToolCursor(response.nextCursor);
    // Cache output schemas for validation
    cacheToolOutputSchemas(response.tools);
  };

  const callTool = async (name: string, params: Record<string, unknown>) => {
    try {
      const response = await sendMCPRequest(
        {
          method: "tools/call" as const,
          params: {
            name,
            arguments: params,
            _meta: {
              progressToken: progressTokenRef.current++,
            },
          },
        },
        CompatibilityCallToolResultSchema,
        "tools",
      );
      setToolResult(response);
    } catch (e) {
      const toolResult: CompatibilityCallToolResult = {
        content: [
          {
            type: "text",
            text: (e as Error).message ?? String(e),
          },
        ],
        isError: true,
      };
      setToolResult(toolResult);
    }
  };

  const handleRootsChange = async () => {
    await sendNotification({ method: "notifications/roots/list_changed" });
  };

  const sendLogLevelRequest = async (level: LoggingLevel) => {
    await sendMCPRequest(
      {
        method: "logging/setLevel" as const,
        params: { level },
      },
      z.object({}),
    );
    setLogLevel(level);
  };

  const clearStdErrNotifications = () => {
    setStdErrNotifications([]);
  };

  // Helper component for rendering the AuthDebugger
  const AuthDebuggerWrapper = () => (
    <TabsContent value="auth">
      <AuthDebugger
        serverUrl={sseUrl}
        onBack={() => setIsAuthDebuggerVisible(false)}
        authState={authState}
        updateAuthState={updateAuthState}
      />
    </TabsContent>
  );

  // Helper function to render OAuth callback components
  if (window.location.pathname === "/oauth/callback") {
    const OAuthCallback = React.lazy(
      () => import("./components/OAuthCallback"),
    );
    return (
      <Suspense fallback={<div>Loading...</div>}>
        <OAuthCallback onConnect={onOAuthConnect} />
      </Suspense>
    );
  }

  if (window.location.pathname === "/oauth/callback/debug") {
    const OAuthDebugCallback = React.lazy(
      () => import("./components/OAuthDebugCallback"),
    );
    return (
      <Suspense fallback={<div>Loading...</div>}>
        <OAuthDebugCallback onConnect={onOAuthDebugConnect} />
      </Suspense>
    );
  }

  return (
    <div className="flex h-screen bg-background">
      <div
        style={{
          width: sidebarWidth,
          minWidth: 200,
          maxWidth: 600,
          transition: isSidebarDragging ? "none" : "width 0.15s",
        }}
        className="bg-card border-r border-border flex flex-col h-full relative"
      >
        <Sidebar
          connectionStatus={connectionStatus}
          transportType={transportType}
          setTransportType={setTransportType}
          command={command}
          setCommand={setCommand}
          args={args}
          setArgs={setArgs}
          sseUrl={sseUrl}
          setSseUrl={setSseUrl}
          env={env}
          setEnv={setEnv}
          config={config}
          setConfig={setConfig}
          bearerToken={bearerToken}
          setBearerToken={setBearerToken}
          headerName={headerName}
          setHeaderName={setHeaderName}
          onConnect={connectMcpServer}
          onDisconnect={disconnectMcpServer}
          stdErrNotifications={stdErrNotifications}
          logLevel={logLevel}
          sendLogLevelRequest={sendLogLevelRequest}
          loggingSupported={!!serverCapabilities?.logging || false}
          clearStdErrNotifications={clearStdErrNotifications}
        />
        {/* Drag handle for resizing sidebar */}
        <div
          onMouseDown={handleSidebarDragStart}
          style={{
            cursor: "col-resize",
            position: "absolute",
            top: 0,
            right: 0,
            width: 6,
            height: "100%",
            zIndex: 10,
            background: isSidebarDragging ? "rgba(0,0,0,0.08)" : "transparent",
          }}
          aria-label="Resize sidebar"
          data-testid="sidebar-drag-handle"
        />
      </div>
      <div className="flex-1 flex flex-col overflow-hidden">
        <div className="flex-1 overflow-auto">
          {mcpClient ? (
            <Tabs
              defaultValue={
                Object.keys(serverCapabilities ?? {}).includes(
                  window.location.hash.slice(1),
                )
                  ? window.location.hash.slice(1)
                  : serverCapabilities?.resources
                    ? "resources"
                    : serverCapabilities?.prompts
                      ? "prompts"
                      : serverCapabilities?.tools
                        ? "tools"
                        : "ping"
              }
              className="w-full p-4"
              onValueChange={(value) => (window.location.hash = value)}
            >
              <TabsList className="mb-4 py-0">
                <TabsTrigger
                  value="resources"
                  disabled={!serverCapabilities?.resources}
                >
                  <Files className="w-4 h-4 mr-2" />
                  Resources
                </TabsTrigger>
                <TabsTrigger
                  value="prompts"
                  disabled={!serverCapabilities?.prompts}
                >
                  <MessageSquare className="w-4 h-4 mr-2" />
                  Prompts
                </TabsTrigger>
                <TabsTrigger
                  value="tools"
                  disabled={!serverCapabilities?.tools}
                >
                  <Hammer className="w-4 h-4 mr-2" />
                  Tools
                </TabsTrigger>
                <TabsTrigger value="ping">
                  <Bell className="w-4 h-4 mr-2" />
                  Ping
                </TabsTrigger>
                <TabsTrigger value="sampling" className="relative">
                  <Hash className="w-4 h-4 mr-2" />
                  Sampling
                  {pendingSampleRequests.length > 0 && (
                    <span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
                      {pendingSampleRequests.length}
                    </span>
                  )}
                </TabsTrigger>
                <TabsTrigger value="roots">
                  <FolderTree className="w-4 h-4 mr-2" />
                  Roots
                </TabsTrigger>
                <TabsTrigger value="auth">
                  <Key className="w-4 h-4 mr-2" />
                  Auth
                </TabsTrigger>
              </TabsList>

              <div className="w-full">
                {!serverCapabilities?.resources &&
                !serverCapabilities?.prompts &&
                !serverCapabilities?.tools ? (
                  <>
                    <div className="flex items-center justify-center p-4">
                      <p className="text-lg text-gray-500 dark:text-gray-400">
                        The connected server does not support any MCP
                        capabilities
                      </p>
                    </div>
                    <PingTab
                      onPingClick={() => {
                        void sendMCPRequest(
                          {
                            method: "ping" as const,
                          },
                          EmptyResultSchema,
                        );
                      }}
                    />
                  </>
                ) : (
                  <>
                    <ResourcesTab
                      resources={resources}
                      resourceTemplates={resourceTemplates}
                      listResources={() => {
                        clearError("resources");
                        listResources();
                      }}
                      clearResources={() => {
                        setResources([]);
                        setNextResourceCursor(undefined);
                      }}
                      listResourceTemplates={() => {
                        clearError("resources");
                        listResourceTemplates();
                      }}
                      clearResourceTemplates={() => {
                        setResourceTemplates([]);
                        setNextResourceTemplateCursor(undefined);
                      }}
                      readResource={(uri) => {
                        clearError("resources");
                        readResource(uri);
                      }}
                      selectedResource={selectedResource}
                      setSelectedResource={(resource) => {
                        clearError("resources");
                        setSelectedResource(resource);
                      }}
                      resourceSubscriptionsSupported={
                        serverCapabilities?.resources?.subscribe || false
                      }
                      resourceSubscriptions={resourceSubscriptions}
                      subscribeToResource={(uri) => {
                        clearError("resources");
                        subscribeToResource(uri);
                      }}
                      unsubscribeFromResource={(uri) => {
                        clearError("resources");
                        unsubscribeFromResource(uri);
                      }}
                      handleCompletion={handleCompletion}
                      completionsSupported={completionsSupported}
                      resourceContent={resourceContent}
                      nextCursor={nextResourceCursor}
                      nextTemplateCursor={nextResourceTemplateCursor}
                      error={errors.resources}
                    />
                    <PromptsTab
                      prompts={prompts}
                      listPrompts={() => {
                        clearError("prompts");
                        listPrompts();
                      }}
                      clearPrompts={() => {
                        setPrompts([]);
                        setNextPromptCursor(undefined);
                      }}
                      getPrompt={(name, args) => {
                        clearError("prompts");
                        getPrompt(name, args);
                      }}
                      selectedPrompt={selectedPrompt}
                      setSelectedPrompt={(prompt) => {
                        clearError("prompts");
                        setSelectedPrompt(prompt);
                        setPromptContent("");
                      }}
                      handleCompletion={handleCompletion}
                      completionsSupported={completionsSupported}
                      promptContent={promptContent}
                      nextCursor={nextPromptCursor}
                      error={errors.prompts}
                    />
                    <ToolsTab
                      tools={tools}
                      listTools={() => {
                        clearError("tools");
                        listTools();
                      }}
                      clearTools={() => {
                        setTools([]);
                        setNextToolCursor(undefined);
                        // Clear cached output schemas
                        cacheToolOutputSchemas([]);
                      }}
                      callTool={async (name, params) => {
                        clearError("tools");
                        setToolResult(null);
                        await callTool(name, params);
                      }}
                      selectedTool={selectedTool}
                      setSelectedTool={(tool) => {
                        clearError("tools");
                        setSelectedTool(tool);
                        setToolResult(null);
                      }}
                      toolResult={toolResult}
                      nextCursor={nextToolCursor}
                      error={errors.tools}
                    />
                    <ConsoleTab />
                    <PingTab
                      onPingClick={() => {
                        void sendMCPRequest(
                          {
                            method: "ping" as const,
                          },
                          EmptyResultSchema,
                        );
                      }}
                    />
                    <SamplingTab
                      pendingRequests={pendingSampleRequests}
                      onApprove={handleApproveSampling}
                      onReject={handleRejectSampling}
                    />
                    <RootsTab
                      roots={roots}
                      setRoots={setRoots}
                      onRootsChange={handleRootsChange}
                    />
                    <AuthDebuggerWrapper />
                  </>
                )}
              </div>
            </Tabs>
          ) : isAuthDebuggerVisible ? (
            <Tabs
              defaultValue={"auth"}
              className="w-full p-4"
              onValueChange={(value) => (window.location.hash = value)}
            >
              <AuthDebuggerWrapper />
            </Tabs>
          ) : (
            <div className="flex flex-col items-center justify-center h-full gap-4">
              <p className="text-lg text-gray-500 dark:text-gray-400">
                Connect to an MCP server to start inspecting
              </p>
              <div className="flex items-center gap-2">
                <p className="text-sm text-muted-foreground">
                  Need to configure authentication?
                </p>
                <Button
                  variant="outline"
                  size="sm"
                  onClick={() => setIsAuthDebuggerVisible(true)}
                >
                  Open Auth Settings
                </Button>
              </div>
            </div>
          )}
        </div>
        <div
          className="relative border-t border-border"
          style={{
            height: `${historyPaneHeight}px`,
          }}
        >
          <div
            className="absolute w-full h-4 -top-2 cursor-row-resize flex items-center justify-center hover:bg-accent/50 dark:hover:bg-input/40"
            onMouseDown={handleDragStart}
          >
            <div className="w-8 h-1 rounded-full bg-border" />
          </div>
          <div className="h-full overflow-auto">
            <HistoryAndNotifications
              requestHistory={requestHistory}
              serverNotifications={notifications}
            />
          </div>
        </div>
      </div>
    </div>
  );
};

export default App;

```

## /client/src/__mocks__/styleMock.js

```js path="/client/src/__mocks__/styleMock.js" 
module.exports = {};

```

## /client/src/__tests__/App.config.test.tsx

```tsx path="/client/src/__tests__/App.config.test.tsx" 
import { render, waitFor } from "@testing-library/react";
import App from "../App";
import { DEFAULT_INSPECTOR_CONFIG } from "../lib/constants";
import { InspectorConfig } from "../lib/configurationTypes";
import * as configUtils from "../utils/configUtils";

// Mock auth dependencies first
jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
  auth: jest.fn(),
}));

jest.mock("../lib/oauth-state-machine", () => ({
  OAuthStateMachine: jest.fn(),
}));

jest.mock("../lib/auth", () => ({
  InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({
    tokens: jest.fn().mockResolvedValue(null),
    clear: jest.fn(),
  })),
  DebugInspectorOAuthClientProvider: jest.fn(),
}));

// Mock the config utils
jest.mock("../utils/configUtils", () => ({
  ...jest.requireActual("../utils/configUtils"),
  getMCPProxyAddress: jest.fn(() => "http://localhost:6277"),
  getMCPProxyAuthToken: jest.fn((config: InspectorConfig) => ({
    token: config.MCP_PROXY_AUTH_TOKEN.value,
    header: "X-MCP-Proxy-Auth",
  })),
  getInitialTransportType: jest.fn(() => "stdio"),
  getInitialSseUrl: jest.fn(() => "http://localhost:3001/sse"),
  getInitialCommand: jest.fn(() => "mcp-server-everything"),
  getInitialArgs: jest.fn(() => ""),
  initializeInspectorConfig: jest.fn(() => DEFAULT_INSPECTOR_CONFIG),
  saveInspectorConfig: jest.fn(),
}));

// Get references to the mocked functions
const mockGetMCPProxyAuthToken = configUtils.getMCPProxyAuthToken as jest.Mock;
const mockInitializeInspectorConfig =
  configUtils.initializeInspectorConfig as jest.Mock;

// Mock other dependencies
jest.mock("../lib/hooks/useConnection", () => ({
  useConnection: () => ({
    connectionStatus: "disconnected",
    serverCapabilities: null,
    mcpClient: null,
    requestHistory: [],
    makeRequest: jest.fn(),
    sendNotification: jest.fn(),
    handleCompletion: jest.fn(),
    completionsSupported: false,
    connect: jest.fn(),
    disconnect: jest.fn(),
  }),
}));

jest.mock("../lib/hooks/useDraggablePane", () => ({
  useDraggablePane: () => ({
    height: 300,
    handleDragStart: jest.fn(),
  }),
  useDraggableSidebar: () => ({
    width: 320,
    isDragging: false,
    handleDragStart: jest.fn(),
  }),
}));

jest.mock("../components/Sidebar", () => ({
  __esModule: true,
  default: () => <div>Sidebar</div>,
}));

// Mock fetch
global.fetch = jest.fn();

describe("App - Config Endpoint", () => {
  beforeEach(() => {
    jest.clearAllMocks();
    (global.fetch as jest.Mock).mockResolvedValue({
      json: () =>
        Promise.resolve({
          defaultEnvironment: { TEST_ENV: "test" },
          defaultCommand: "test-command",
          defaultArgs: "test-args",
        }),
    });
  });

  afterEach(() => {
    jest.clearAllMocks();

    // Reset getMCPProxyAuthToken to default behavior
    mockGetMCPProxyAuthToken.mockImplementation((config: InspectorConfig) => ({
      token: config.MCP_PROXY_AUTH_TOKEN.value,
      header: "X-MCP-Proxy-Auth",
    }));
  });

  test("sends X-MCP-Proxy-Auth header when fetching config with proxy auth token", async () => {
    const mockConfig = {
      ...DEFAULT_INSPECTOR_CONFIG,
      MCP_PROXY_AUTH_TOKEN: {
        ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
        value: "test-proxy-token",
      },
    };

    // Mock initializeInspectorConfig to return our test config
    mockInitializeInspectorConfig.mockReturnValue(mockConfig);

    render(<App />);

    await waitFor(() => {
      expect(global.fetch).toHaveBeenCalledWith(
        "http://localhost:6277/config",
        {
          headers: {
            "X-MCP-Proxy-Auth": "Bearer test-proxy-token",
          },
        },
      );
    });
  });

  test("does not send auth header when proxy auth token is empty", async () => {
    const mockConfig = {
      ...DEFAULT_INSPECTOR_CONFIG,
      MCP_PROXY_AUTH_TOKEN: {
        ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
        value: "",
      },
    };

    // Mock initializeInspectorConfig to return our test config
    mockInitializeInspectorConfig.mockReturnValue(mockConfig);

    render(<App />);

    await waitFor(() => {
      expect(global.fetch).toHaveBeenCalledWith(
        "http://localhost:6277/config",
        {
          headers: {},
        },
      );
    });
  });

  test("uses custom header name if getMCPProxyAuthToken returns different header", async () => {
    const mockConfig = {
      ...DEFAULT_INSPECTOR_CONFIG,
      MCP_PROXY_AUTH_TOKEN: {
        ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
        value: "test-proxy-token",
      },
    };

    // Mock to return a custom header name
    mockGetMCPProxyAuthToken.mockReturnValue({
      token: "test-proxy-token",
      header: "X-Custom-Auth",
    });
    mockInitializeInspectorConfig.mockReturnValue(mockConfig);

    render(<App />);

    await waitFor(() => {
      expect(global.fetch).toHaveBeenCalledWith(
        "http://localhost:6277/config",
        {
          headers: {
            "X-Custom-Auth": "Bearer test-proxy-token",
          },
        },
      );
    });
  });

  test("config endpoint response updates app state", async () => {
    const mockConfig = {
      ...DEFAULT_INSPECTOR_CONFIG,
      MCP_PROXY_AUTH_TOKEN: {
        ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
        value: "test-proxy-token",
      },
    };

    mockInitializeInspectorConfig.mockReturnValue(mockConfig);

    render(<App />);

    await waitFor(() => {
      expect(global.fetch).toHaveBeenCalledTimes(1);
    });

    // Verify the fetch was called with correct parameters
    expect(global.fetch).toHaveBeenCalledWith(
      "http://localhost:6277/config",
      expect.objectContaining({
        headers: expect.objectContaining({
          "X-MCP-Proxy-Auth": "Bearer test-proxy-token",
        }),
      }),
    );
  });

  test("handles config endpoint errors gracefully", async () => {
    const mockConfig = {
      ...DEFAULT_INSPECTOR_CONFIG,
      MCP_PROXY_AUTH_TOKEN: {
        ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
        value: "test-proxy-token",
      },
    };

    mockInitializeInspectorConfig.mockReturnValue(mockConfig);

    // Mock fetch to reject
    (global.fetch as jest.Mock).mockRejectedValue(new Error("Network error"));

    // Spy on console.error
    const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation();

    render(<App />);

    await waitFor(() => {
      expect(consoleErrorSpy).toHaveBeenCalledWith(
        "Error fetching default environment:",
        expect.any(Error),
      );
    });

    consoleErrorSpy.mockRestore();
  });
});

```

## /client/src/components/AuthDebugger.tsx

```tsx path="/client/src/components/AuthDebugger.tsx" 
import { useCallback, useMemo, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { DebugInspectorOAuthClientProvider } from "../lib/auth";
import { AlertCircle } from "lucide-react";
import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "../lib/auth-types";
import { OAuthFlowProgress } from "./OAuthFlowProgress";
import { OAuthStateMachine } from "../lib/oauth-state-machine";
import { SESSION_KEYS } from "../lib/constants";

export interface AuthDebuggerProps {
  serverUrl: string;
  onBack: () => void;
  authState: AuthDebuggerState;
  updateAuthState: (updates: Partial<AuthDebuggerState>) => void;
}

interface StatusMessageProps {
  message: { type: "error" | "success" | "info"; message: string };
}

const StatusMessage = ({ message }: StatusMessageProps) => {
  let bgColor: string;
  let textColor: string;
  let borderColor: string;

  switch (message.type) {
    case "error":
      bgColor = "bg-red-50";
      textColor = "text-red-700";
      borderColor = "border-red-200";
      break;
    case "success":
      bgColor = "bg-green-50";
      textColor = "text-green-700";
      borderColor = "border-green-200";
      break;
    case "info":
    default:
      bgColor = "bg-blue-50";
      textColor = "text-blue-700";
      borderColor = "border-blue-200";
      break;
  }

  return (
    <div
      className={`p-3 rounded-md border ${bgColor} ${borderColor} ${textColor} mb-4`}
    >
      <div className="flex items-center gap-2">
        <AlertCircle className="h-4 w-4" />
        <p className="text-sm">{message.message}</p>
      </div>
    </div>
  );
};

const AuthDebugger = ({
  serverUrl: serverUrl,
  onBack,
  authState,
  updateAuthState,
}: AuthDebuggerProps) => {
  // Check for existing tokens on mount
  useEffect(() => {
    if (serverUrl && !authState.oauthTokens) {
      const checkTokens = async () => {
        try {
          const provider = new DebugInspectorOAuthClientProvider(serverUrl);
          const existingTokens = await provider.tokens();
          if (existingTokens) {
            updateAuthState({
              oauthTokens: existingTokens,
              oauthStep: "complete",
            });
          }
        } catch (error) {
          console.error("Failed to load existing OAuth tokens:", error);
        }
      };
      checkTokens();
    }
  }, [serverUrl, updateAuthState, authState.oauthTokens]);

  const startOAuthFlow = useCallback(() => {
    if (!serverUrl) {
      updateAuthState({
        statusMessage: {
          type: "error",
          message:
            "Please enter a server URL in the sidebar before authenticating",
        },
      });
      return;
    }

    updateAuthState({
      oauthStep: "metadata_discovery",
      authorizationUrl: null,
      statusMessage: null,
      latestError: null,
    });
  }, [serverUrl, updateAuthState]);

  const stateMachine = useMemo(
    () => new OAuthStateMachine(serverUrl, updateAuthState),
    [serverUrl, updateAuthState],
  );

  const proceedToNextStep = useCallback(async () => {
    if (!serverUrl) return;

    try {
      updateAuthState({
        isInitiatingAuth: true,
        statusMessage: null,
        latestError: null,
      });

      await stateMachine.executeStep(authState);
    } catch (error) {
      console.error("OAuth flow error:", error);
      updateAuthState({
        latestError: error instanceof Error ? error : new Error(String(error)),
      });
    } finally {
      updateAuthState({ isInitiatingAuth: false });
    }
  }, [serverUrl, authState, updateAuthState, stateMachine]);

  const handleQuickOAuth = useCallback(async () => {
    if (!serverUrl) {
      updateAuthState({
        statusMessage: {
          type: "error",
          message:
            "Please enter a server URL in the sidebar before authenticating",
        },
      });
      return;
    }

    updateAuthState({ isInitiatingAuth: true, statusMessage: null });
    try {
      // Step through the OAuth flow using the state machine instead of the auth() function
      let currentState: AuthDebuggerState = {
        ...authState,
        oauthStep: "metadata_discovery",
        authorizationUrl: null,
        latestError: null,
      };

      const oauthMachine = new OAuthStateMachine(serverUrl, (updates) => {
        // Update our temporary state during the process
        currentState = { ...currentState, ...updates };
        // But don't call updateAuthState yet
      });

      // Manually step through each stage of the OAuth flow
      while (currentState.oauthStep !== "complete") {
        await oauthMachine.executeStep(currentState);
        // In quick mode, we'll just redirect to the authorization URL
        if (
          currentState.oauthStep === "authorization_code" &&
          currentState.authorizationUrl
        ) {
          // Store the current auth state before redirecting
          sessionStorage.setItem(
            SESSION_KEYS.AUTH_DEBUGGER_STATE,
            JSON.stringify(currentState),
          );
          // Open the authorization URL automatically
          window.location.href = currentState.authorizationUrl;
          break;
        }
      }

      // After the flow completes or reaches a user-input step, update the app state
      updateAuthState({
        ...currentState,
        statusMessage: {
          type: "info",
          message:
            currentState.oauthStep === "complete"
              ? "Authentication completed successfully"
              : "Please complete authentication in the opened window and enter the code",
        },
      });
    } catch (error) {
      console.error("OAuth initialization error:", error);
      updateAuthState({
        statusMessage: {
          type: "error",
          message: `Failed to start OAuth flow: ${error instanceof Error ? error.message : String(error)}`,
        },
      });
    } finally {
      updateAuthState({ isInitiatingAuth: false });
    }
  }, [serverUrl, updateAuthState, authState]);

  const handleClearOAuth = useCallback(() => {
    if (serverUrl) {
      const serverAuthProvider = new DebugInspectorOAuthClientProvider(
        serverUrl,
      );
      serverAuthProvider.clear();
      updateAuthState({
        ...EMPTY_DEBUGGER_STATE,
        statusMessage: {
          type: "success",
          message: "OAuth tokens cleared successfully",
        },
      });

      // Clear success message after 3 seconds
      setTimeout(() => {
        updateAuthState({ statusMessage: null });
      }, 3000);
    }
  }, [serverUrl, updateAuthState]);

  return (
    <div className="w-full p-4">
      <div className="flex justify-between items-center mb-6">
        <h2 className="text-2xl font-bold">Authentication Settings</h2>
        <Button variant="outline" onClick={onBack}>
          Back to Connect
        </Button>
      </div>

      <div className="w-full space-y-6">
        <div className="flex flex-col gap-6">
          <div className="grid w-full gap-2">
            <p className="text-muted-foreground mb-4">
              Configure authentication settings for your MCP server connection.
            </p>

            <div className="rounded-md border p-6 space-y-6">
              <h3 className="text-lg font-medium">OAuth Authentication</h3>
              <p className="text-sm text-muted-foreground mb-2">
                Use OAuth to securely authenticate with the MCP server.
              </p>

              {authState.statusMessage && (
                <StatusMessage message={authState.statusMessage} />
              )}

              <div className="space-y-4">
                {authState.oauthTokens && (
                  <div className="space-y-2">
                    <p className="text-sm font-medium">Access Token:</p>
                    <div className="bg-muted p-2 rounded-md text-xs overflow-x-auto">
                      {authState.oauthTokens.access_token.substring(0, 25)}...
                    </div>
                  </div>
                )}

                <div className="flex gap-4">
                  <Button
                    variant="outline"
                    onClick={startOAuthFlow}
                    disabled={authState.isInitiatingAuth}
                  >
                    {authState.oauthTokens
                      ? "Guided Token Refresh"
                      : "Guided OAuth Flow"}
                  </Button>

                  <Button
                    onClick={handleQuickOAuth}
                    disabled={authState.isInitiatingAuth}
                  >
                    {authState.isInitiatingAuth
                      ? "Initiating..."
                      : authState.oauthTokens
                        ? "Quick Refresh"
                        : "Quick OAuth Flow"}
                  </Button>

                  <Button variant="outline" onClick={handleClearOAuth}>
                    Clear OAuth State
                  </Button>
                </div>

                <p className="text-xs text-muted-foreground">
                  Choose "Guided" for step-by-step instructions or "Quick" for
                  the standard automatic flow.
                </p>
              </div>
            </div>

            <OAuthFlowProgress
              serverUrl={serverUrl}
              authState={authState}
              updateAuthState={updateAuthState}
              proceedToNextStep={proceedToNextStep}
            />
          </div>
        </div>
      </div>
    </div>
  );
};

export default AuthDebugger;

```

## /client/src/components/ConsoleTab.tsx

```tsx path="/client/src/components/ConsoleTab.tsx" 
import { TabsContent } from "@/components/ui/tabs";

const ConsoleTab = () => (
  <TabsContent value="console" className="h-96">
    <div className="bg-gray-900 text-gray-100 p-4 rounded-lg h-full font-mono text-sm overflow-auto">
      <div className="opacity-50">Welcome to MCP Client Console</div>
      {/* Console output would go here */}
    </div>
  </TabsContent>
);

export default ConsoleTab;

```

## /client/src/components/DynamicJsonForm.tsx

```tsx path="/client/src/components/DynamicJsonForm.tsx" 
import { useState, useEffect, useCallback, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import JsonEditor from "./JsonEditor";
import { updateValueAtPath } from "@/utils/jsonUtils";
import { generateDefaultValue } from "@/utils/schemaUtils";
import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils";
import { useToast } from "@/lib/hooks/useToast";
import { CheckCheck, Copy } from "lucide-react";

interface DynamicJsonFormProps {
  schema: JsonSchemaType;
  value: JsonValue;
  onChange: (value: JsonValue) => void;
  maxDepth?: number;
}

const isSimpleObject = (schema: JsonSchemaType): boolean => {
  const supportedTypes = ["string", "number", "integer", "boolean", "null"];
  if (supportedTypes.includes(schema.type)) return true;
  if (schema.type === "object") {
    return Object.values(schema.properties ?? {}).every((prop) =>
      supportedTypes.includes(prop.type),
    );
  }
  if (schema.type === "array") {
    return !!schema.items && isSimpleObject(schema.items);
  }
  return false;
};

const getArrayItemDefault = (schema: JsonSchemaType): JsonValue => {
  if ("default" in schema && schema.default !== undefined) {
    return schema.default;
  }

  switch (schema.type) {
    case "string":
      return "";
    case "number":
    case "integer":
      return 0;
    case "boolean":
      return false;
    case "array":
      return [];
    case "object":
      return {};
    case "null":
      return null;
    default:
      return null;
  }
};

const DynamicJsonForm = ({
  schema,
  value,
  onChange,
  maxDepth = 3,
}: DynamicJsonFormProps) => {
  const isOnlyJSON = !isSimpleObject(schema);
  const [isJsonMode, setIsJsonMode] = useState(isOnlyJSON);
  const [jsonError, setJsonError] = useState<string>();
  const [copiedJson, setCopiedJson] = useState<boolean>(false);
  const { toast } = useToast();

  // Store the raw JSON string to allow immediate feedback during typing
  // while deferring parsing until the user stops typing
  const [rawJsonValue, setRawJsonValue] = useState<string>(
    JSON.stringify(value ?? generateDefaultValue(schema), null, 2),
  );

  // Use a ref to manage debouncing timeouts to avoid parsing JSON
  // on every keystroke which would be inefficient and error-prone
  const timeoutRef = useRef<ReturnType<typeof setTimeout>>();

  // Debounce JSON parsing and parent updates to handle typing gracefully
  const debouncedUpdateParent = useCallback(
    (jsonString: string) => {
      // Clear any existing timeout
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }

      // Set a new timeout
      timeoutRef.current = setTimeout(() => {
        try {
          const parsed = JSON.parse(jsonString);
          onChange(parsed);
          setJsonError(undefined);
        } catch {
          // Don't set error during normal typing
        }
      }, 300);
    },
    [onChange, setJsonError],
  );

  // Update rawJsonValue when value prop changes
  useEffect(() => {
    if (!isJsonMode) {
      setRawJsonValue(
        JSON.stringify(value ?? generateDefaultValue(schema), null, 2),
      );
    }
  }, [value, schema, isJsonMode]);

  const handleSwitchToFormMode = () => {
    if (isJsonMode) {
      // When switching to Form mode, ensure we have valid JSON
      try {
        const parsed = JSON.parse(rawJsonValue);
        // Update the parent component's state with the parsed value
        onChange(parsed);
        // Switch to form mode
        setIsJsonMode(false);
      } catch (err) {
        setJsonError(err instanceof Error ? err.message : "Invalid JSON");
      }
    } else {
      // Update raw JSON value when switching to JSON mode
      setRawJsonValue(
        JSON.stringify(value ?? generateDefaultValue(schema), null, 2),
      );
      setIsJsonMode(true);
    }
  };

  const formatJson = () => {
    try {
      const jsonStr = rawJsonValue.trim();
      if (!jsonStr) {
        return;
      }
      const formatted = JSON.stringify(JSON.parse(jsonStr), null, 2);
      setRawJsonValue(formatted);
      debouncedUpdateParent(formatted);
      setJsonError(undefined);
    } catch (err) {
      setJsonError(err instanceof Error ? err.message : "Invalid JSON");
    }
  };

  const renderFormFields = (
    propSchema: JsonSchemaType,
    currentValue: JsonValue,
    path: string[] = [],
    depth: number = 0,
    parentSchema?: JsonSchemaType,
    propertyName?: string,
  ) => {
    if (
      depth >= maxDepth &&
      (propSchema.type === "object" || propSchema.type === "array")
    ) {
      // Render as JSON editor when max depth is reached
      return (
        <JsonEditor
          value={JSON.stringify(
            currentValue ??
              generateDefaultValue(propSchema, propertyName, parentSchema),
            null,
            2,
          )}
          onChange={(newValue) => {
            try {
              const parsed = JSON.parse(newValue);
              handleFieldChange(path, parsed);
              setJsonError(undefined);
            } catch (err) {
              setJsonError(err instanceof Error ? err.message : "Invalid JSON");
            }
          }}
          error={jsonError}
        />
      );
    }

    // Check if this property is required in the parent schema
    const isRequired =
      parentSchema?.required?.includes(propertyName || "") ?? false;

    switch (propSchema.type) {
      case "string":
        return (
          <Input
            type="text"
            value={(currentValue as string) ?? ""}
            onChange={(e) => {
              const val = e.target.value;
              // Always allow setting string values, including empty strings
              handleFieldChange(path, val);
            }}
            placeholder={propSchema.description}
            required={isRequired}
          />
        );
      case "number":
        return (
          <Input
            type="number"
            value={(currentValue as number)?.toString() ?? ""}
            onChange={(e) => {
              const val = e.target.value;
              if (!val && !isRequired) {
                handleFieldChange(path, undefined);
              } else {
                const num = Number(val);
                if (!isNaN(num)) {
                  handleFieldChange(path, num);
                }
              }
            }}
            placeholder={propSchema.description}
            required={isRequired}
          />
        );
      case "integer":
        return (
          <Input
            type="number"
            step="1"
            value={(currentValue as number)?.toString() ?? ""}
            onChange={(e) => {
              const val = e.target.value;
              if (!val && !isRequired) {
                handleFieldChange(path, undefined);
              } else {
                const num = Number(val);
                // Only update if it's a valid integer
                if (!isNaN(num) && Number.isInteger(num)) {
                  handleFieldChange(path, num);
                }
              }
            }}
            placeholder={propSchema.description}
            required={isRequired}
          />
        );
      case "boolean":
        return (
          <Input
            type="checkbox"
            checked={(currentValue as boolean) ?? false}
            onChange={(e) => handleFieldChange(path, e.target.checked)}
            className="w-4 h-4"
            required={isRequired}
          />
        );
      case "object":
        if (!propSchema.properties) {
          return (
            <JsonEditor
              value={JSON.stringify(currentValue ?? {}, null, 2)}
              onChange={(newValue) => {
                try {
                  const parsed = JSON.parse(newValue);
                  handleFieldChange(path, parsed);
                  setJsonError(undefined);
                } catch (err) {
                  setJsonError(
                    err instanceof Error ? err.message : "Invalid JSON",
                  );
                }
              }}
              error={jsonError}
            />
          );
        }

        return (
          <div className="space-y-2 border rounded p-3">
            {Object.entries(propSchema.properties).map(([key, subSchema]) => (
              <div key={key}>
                <label className="block text-sm font-medium mb-1">
                  {key}
                  {propSchema.required?.includes(key) && (
                    <span className="text-red-500 ml-1">*</span>
                  )}
                </label>
                {renderFormFields(
                  subSchema as JsonSchemaType,
                  (currentValue as Record<string, JsonValue>)?.[key],
                  [...path, key],
                  depth + 1,
                  propSchema,
                  key,
                )}
              </div>
            ))}
          </div>
        );
      case "array": {
        const arrayValue = Array.isArray(currentValue) ? currentValue : [];
        if (!propSchema.items) return null;

        // If the array items are simple, render as form fields, otherwise use JSON editor
        if (isSimpleObject(propSchema.items)) {
          return (
            <div className="space-y-4">
              {propSchema.description && (
                <p className="text-sm text-gray-600">
                  {propSchema.description}
                </p>
              )}

              {propSchema.items?.description && (
                <p className="text-sm text-gray-500">
                  Items: {propSchema.items.description}
                </p>
              )}

              <div className="space-y-2">
                {arrayValue.map((item, index) => (
                  <div key={index} className="flex items-center gap-2">
                    {renderFormFields(
                      propSchema.items as JsonSchemaType,
                      item,
                      [...path, index.toString()],
                      depth + 1,
                    )}
                    <Button
                      variant="outline"
                      size="sm"
                      onClick={() => {
                        const newArray = [...arrayValue];
                        newArray.splice(index, 1);
                        handleFieldChange(path, newArray);
                      }}
                    >
                      Remove
                    </Button>
                  </div>
                ))}
                <Button
                  variant="outline"
                  size="sm"
                  onClick={() => {
                    const defaultValue = getArrayItemDefault(
                      propSchema.items as JsonSchemaType,
                    );
                    handleFieldChange(path, [...arrayValue, defaultValue]);
                  }}
                  title={
                    propSchema.items?.description
                      ? `Add new ${propSchema.items.description}`
                      : "Add new item"
                  }
                >
                  Add Item
                </Button>
              </div>
            </div>
          );
        }

        // For complex arrays, fall back to JSON editor
        return (
          <JsonEditor
            value={JSON.stringify(currentValue ?? [], null, 2)}
            onChange={(newValue) => {
              try {
                const parsed = JSON.parse(newValue);
                handleFieldChange(path, parsed);
                setJsonError(undefined);
              } catch (err) {
                setJsonError(
                  err instanceof Error ? err.message : "Invalid JSON",
                );
              }
            }}
            error={jsonError}
          />
        );
      }
      default:
        return null;
    }
  };

  const handleFieldChange = (path: string[], fieldValue: JsonValue) => {
    if (path.length === 0) {
      onChange(fieldValue);
      return;
    }

    try {
      const newValue = updateValueAtPath(value, path, fieldValue);
      onChange(newValue);
    } catch (error) {
      console.error("Failed to update form value:", error);
      onChange(value);
    }
  };

  const shouldUseJsonMode =
    schema.type === "object" &&
    (!schema.properties || Object.keys(schema.properties).length === 0);

  useEffect(() => {
    if (shouldUseJsonMode && !isJsonMode) {
      setIsJsonMode(true);
    }
  }, [shouldUseJsonMode, isJsonMode]);

  const handleCopyJson = useCallback(() => {
    const copyToClipboard = async () => {
      try {
        await navigator.clipboard.writeText(
          JSON.stringify(value, null, 2) ?? "[]",
        );
        setCopiedJson(true);

        toast({
          title: "JSON copied",
          description:
            "The JSON data has been successfully copied to your clipboard.",
        });

        setTimeout(() => {
          setCopiedJson(false);
        }, 2000);
      } catch (error) {
        toast({
          title: "Error",
          description: `Failed to copy JSON: ${error instanceof Error ? error.message : String(error)}`,
          variant: "destructive",
        });
      }
    };

    copyToClipboard();
  }, [toast, value]);

  return (
    <div className="space-y-4">
      <div className="flex justify-end space-x-2">
        {isJsonMode && (
          <>
            <Button
              type="button"
              variant="outline"
              size="sm"
              onClick={handleCopyJson}
            >
              {copiedJson ? (
                <CheckCheck className="h-4 w-4 mr-2" />
              ) : (
                <Copy className="h-4 w-4 mr-2" />
              )}
              Copy JSON
            </Button>
            <Button
              type="button"
              variant="outline"
              size="sm"
              onClick={formatJson}
            >
              Format JSON
            </Button>
          </>
        )}

        {!isOnlyJSON && (
          <Button variant="outline" size="sm" onClick={handleSwitchToFormMode}>
            {isJsonMode ? "Switch to Form" : "Switch to JSON"}
          </Button>
        )}
      </div>

      {isJsonMode ? (
        <JsonEditor
          value={rawJsonValue}
          onChange={(newValue) => {
            // Always update local state
            setRawJsonValue(newValue);

            // Use the debounced function to attempt parsing and updating parent
            debouncedUpdateParent(newValue);
          }}
          error={jsonError}
        />
      ) : // If schema type is object but value is not an object or is empty, and we have actual JSON data,
      // render a simple representation of the JSON data
      schema.type === "object" &&
        (typeof value !== "object" ||
          value === null ||
          Object.keys(value).length === 0) &&
        rawJsonValue &&
        rawJsonValue !== "{}" ? (
        <div className="space-y-4 border rounded-md p-4">
          <p className="text-sm text-gray-500">
            Form view not available for this JSON structure. Using simplified
            view:
          </p>
          <pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto">
            {rawJsonValue}
          </pre>
          <p className="text-sm text-gray-500">
            Use JSON mode for full editing capabilities.
          </p>
        </div>
      ) : (
        renderFormFields(schema, value)
      )}
    </div>
  );
};

export default DynamicJsonForm;

```

## /client/src/components/HistoryAndNotifications.tsx

```tsx path="/client/src/components/HistoryAndNotifications.tsx" 
import { ServerNotification } from "@modelcontextprotocol/sdk/types.js";
import { useState } from "react";
import JsonView from "./JsonView";

const HistoryAndNotifications = ({
  requestHistory,
  serverNotifications,
}: {
  requestHistory: Array<{ request: string; response?: string }>;
  serverNotifications: ServerNotification[];
}) => {
  const [expandedRequests, setExpandedRequests] = useState<{
    [key: number]: boolean;
  }>({});
  const [expandedNotifications, setExpandedNotifications] = useState<{
    [key: number]: boolean;
  }>({});

  const toggleRequestExpansion = (index: number) => {
    setExpandedRequests((prev) => ({ ...prev, [index]: !prev[index] }));
  };

  const toggleNotificationExpansion = (index: number) => {
    setExpandedNotifications((prev) => ({ ...prev, [index]: !prev[index] }));
  };

  return (
    <div className="bg-card overflow-hidden flex h-full">
      <div className="flex-1 overflow-y-auto p-4 border-r">
        <h2 className="text-lg font-semibold mb-4">History</h2>
        {requestHistory.length === 0 ? (
          <p className="text-sm text-gray-500 dark:text-gray-400 italic">
            No history yet
          </p>
        ) : (
          <ul className="space-y-3">
            {requestHistory
              .slice()
              .reverse()
              .map((request, index) => (
                <li
                  key={index}
                  className="text-sm text-foreground bg-secondary py-2 px-3 rounded"
                >
                  <div
                    className="flex justify-between items-center cursor-pointer"
                    onClick={() =>
                      toggleRequestExpansion(requestHistory.length - 1 - index)
                    }
                  >
                    <span className="font-mono">
                      {requestHistory.length - index}.{" "}
                      {JSON.parse(request.request).method}
                    </span>
                    <span>
                      {expandedRequests[requestHistory.length - 1 - index]
                        ? "▼"
                        : "▶"}
                    </span>
                  </div>
                  {expandedRequests[requestHistory.length - 1 - index] && (
                    <>
                      <div className="mt-2">
                        <div className="flex justify-between items-center mb-1">
                          <span className="font-semibold text-blue-600">
                            Request:
                          </span>
                        </div>

                        <JsonView
                          data={request.request}
                          className="bg-background"
                        />
                      </div>
                      {request.response && (
                        <div className="mt-2">
                          <div className="flex justify-between items-center mb-1">
                            <span className="font-semibold text-green-600">
                              Response:
                            </span>
                          </div>
                          <JsonView
                            data={request.response}
                            className="bg-background"
                          />
                        </div>
                      )}
                    </>
                  )}
                </li>
              ))}
          </ul>
        )}
      </div>
      <div className="flex-1 overflow-y-auto p-4">
        <h2 className="text-lg font-semibold mb-4">Server Notifications</h2>
        {serverNotifications.length === 0 ? (
          <p className="text-sm text-gray-500 dark:text-gray-400 italic">
            No notifications yet
          </p>
        ) : (
          <ul className="space-y-3">
            {serverNotifications
              .slice()
              .reverse()
              .map((notification, index) => (
                <li
                  key={index}
                  className="text-sm text-foreground bg-secondary py-2 px-3 rounded"
                >
                  <div
                    className="flex justify-between items-center cursor-pointer"
                    onClick={() =>
                      toggleNotificationExpansion(
                        serverNotifications.length - 1 - index,
                      )
                    }
                  >
                    <span className="font-mono">
                      {serverNotifications.length - index}.{" "}
                      {notification.method}
                    </span>
                    <span>
                      {expandedNotifications[
                        serverNotifications.length - 1 - index
                      ]
                        ? "▼"
                        : "▶"}
                    </span>
                  </div>
                  {expandedNotifications[
                    serverNotifications.length - 1 - index
                  ] && (
                    <div className="mt-2">
                      <div className="flex justify-between items-center mb-1">
                        <span className="font-semibold text-purple-600">
                          Details:
                        </span>
                      </div>
                      <JsonView
                        data={JSON.stringify(notification, null, 2)}
                        className="bg-background"
                      />
                    </div>
                  )}
                </li>
              ))}
          </ul>
        )}
      </div>
    </div>
  );
};

export default HistoryAndNotifications;

```

## /client/src/components/JsonEditor.tsx

```tsx path="/client/src/components/JsonEditor.tsx" 
import { useState, useEffect } from "react";
import Editor from "react-simple-code-editor";
import Prism from "prismjs";
import "prismjs/components/prism-json";
import "prismjs/themes/prism.css";

interface JsonEditorProps {
  value: string;
  onChange: (value: string) => void;
  error?: string;
}

const JsonEditor = ({
  value,
  onChange,
  error: externalError,
}: JsonEditorProps) => {
  const [editorContent, setEditorContent] = useState(value || "");
  const [internalError, setInternalError] = useState<string | undefined>(
    undefined,
  );

  useEffect(() => {
    setEditorContent(value || "");
  }, [value]);

  const handleEditorChange = (newContent: string) => {
    setEditorContent(newContent);
    setInternalError(undefined);
    onChange(newContent);
  };

  const displayError = internalError || externalError;

  return (
    <div className="relative">
      <div
        className={`border rounded-md ${
          displayError
            ? "border-red-500"
            : "border-gray-200 dark:border-gray-800"
        }`}
      >
        <Editor
          value={editorContent}
          onValueChange={handleEditorChange}
          highlight={(code) =>
            Prism.highlight(code, Prism.languages.json, "json")
          }
          padding={10}
          style={{
            fontFamily: '"Fira code", "Fira Mono", monospace',
            fontSize: 14,
            backgroundColor: "transparent",
            minHeight: "100px",
          }}
          className="w-full"
        />
      </div>
      {displayError && (
        <p className="text-sm text-red-500 mt-1">{displayError}</p>
      )}
    </div>
  );
};

export default JsonEditor;

```

## /client/src/components/JsonView.tsx

```tsx path="/client/src/components/JsonView.tsx" 
import { useState, memo, useMemo, useCallback, useEffect } from "react";
import type { JsonValue } from "@/utils/jsonUtils";
import clsx from "clsx";
import { Copy, CheckCheck } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useToast } from "@/lib/hooks/useToast";
import { getDataType, tryParseJson } from "@/utils/jsonUtils";

interface JsonViewProps {
  data: unknown;
  name?: string;
  initialExpandDepth?: number;
  className?: string;
  withCopyButton?: boolean;
  isError?: boolean;
}

const JsonView = memo(
  ({
    data,
    name,
    initialExpandDepth = 3,
    className,
    withCopyButton = true,
    isError = false,
  }: JsonViewProps) => {
    const { toast } = useToast();
    const [copied, setCopied] = useState(false);

    useEffect(() => {
      let timeoutId: NodeJS.Timeout;
      if (copied) {
        timeoutId = setTimeout(() => {
          setCopied(false);
        }, 500);
      }
      return () => {
        if (timeoutId) {
          clearTimeout(timeoutId);
        }
      };
    }, [copied]);

    const normalizedData = useMemo(() => {
      return typeof data === "string"
        ? tryParseJson(data).success
          ? tryParseJson(data).data
          : data
        : data;
    }, [data]);

    const handleCopy = useCallback(() => {
      try {
        navigator.clipboard.writeText(
          typeof normalizedData === "string"
            ? normalizedData
            : JSON.stringify(normalizedData, null, 2),
        );
        setCopied(true);
      } catch (error) {
        toast({
          title: "Error",
          description: `There was an error coping result into the clipboard: ${error instanceof Error ? error.message : String(error)}`,
          variant: "destructive",
        });
      }
    }, [toast, normalizedData]);

    return (
      <div className={clsx("p-4 border rounded relative", className)}>
        {withCopyButton && (
          <Button
            size="icon"
            variant="ghost"
            className="absolute top-2 right-2"
            onClick={handleCopy}
          >
            {copied ? (
              <CheckCheck className="size-4 dark:text-green-700 text-green-600" />
            ) : (
              <Copy className="size-4 text-foreground" />
            )}
          </Button>
        )}
        <div className="font-mono text-sm transition-all duration-300">
          <JsonNode
            data={normalizedData as JsonValue}
            name={name}
            depth={0}
            initialExpandDepth={initialExpandDepth}
            isError={isError}
          />
        </div>
      </div>
    );
  },
);

JsonView.displayName = "JsonView";

interface JsonNodeProps {
  data: JsonValue;
  name?: string;
  depth: number;
  initialExpandDepth: number;
  isError?: boolean;
}

const JsonNode = memo(
  ({
    data,
    name,
    depth = 0,
    initialExpandDepth,
    isError = false,
  }: JsonNodeProps) => {
    const [isExpanded, setIsExpanded] = useState(depth < initialExpandDepth);
    const [typeStyleMap] = useState<Record<string, string>>({
      number: "text-blue-600",
      boolean: "text-amber-600",
      null: "text-purple-600",
      undefined: "text-gray-600",
      string: "text-green-600 group-hover:text-green-500",
      error: "text-red-600 group-hover:text-red-500",
      default: "text-gray-700",
    });
    const dataType = getDataType(data);

    const renderCollapsible = (isArray: boolean) => {
      const items = isArray
        ? (data as JsonValue[])
        : Object.entries(data as Record<string, JsonValue>);
      const itemCount = items.length;
      const isEmpty = itemCount === 0;

      const symbolMap = {
        open: isArray ? "[" : "{",
        close: isArray ? "]" : "}",
        collapsed: isArray ? "[ ... ]" : "{ ... }",
        empty: isArray ? "[]" : "{}",
      };

      if (isEmpty) {
        return (
          <div className="flex items-center">
            {name && (
              <span className="mr-1 text-gray-600 dark:text-gray-400">
                {name}:
              </span>
            )}
            <span className="text-gray-500">{symbolMap.empty}</span>
          </div>
        );
      }

      return (
        <div className="flex flex-col">
          <div
            className="flex items-center mr-1 rounded cursor-pointer group hover:bg-gray-800/10 dark:hover:bg-gray-800/20"
            onClick={() => setIsExpanded(!isExpanded)}
          >
            {name && (
              <span className="mr-1 text-gray-600 dark:text-gray-400 dark:group-hover:text-gray-100 group-hover:text-gray-400">
                {name}:
              </span>
            )}
            {isExpanded ? (
              <span className="text-gray-600 dark:text-gray-400 dark:group-hover:text-gray-100 group-hover:text-gray-400">
                {symbolMap.open}
              </span>
            ) : (
              <>
                <span className="text-gray-600 dark:group-hover:text-gray-100 group-hover:text-gray-400">
                  {symbolMap.collapsed}
                </span>
                <span className="ml-1 text-gray-700 dark:group-hover:text-gray-100 group-hover:text-gray-400">
                  {itemCount} {itemCount === 1 ? "item" : "items"}
                </span>
              </>
            )}
          </div>
          {isExpanded && (
            <>
              <div className="pl-2 ml-4 border-l border-gray-200 dark:border-gray-800">
                {isArray
                  ? (items as JsonValue[]).map((item, index) => (
                      <div key={index} className="my-1">
                        <JsonNode
                          data={item}
                          name={`${index}`}
                          depth={depth + 1}
                          initialExpandDepth={initialExpandDepth}
                        />
                      </div>
                    ))
                  : (items as [string, JsonValue][]).map(([key, value]) => (
                      <div key={key} className="my-1">
                        <JsonNode
                          data={value}
                          name={key}
                          depth={depth + 1}
                          initialExpandDepth={initialExpandDepth}
                        />
                      </div>
                    ))}
              </div>
              <div className="text-gray-600 dark:text-gray-400">
                {symbolMap.close}
              </div>
            </>
          )}
        </div>
      );
    };

    const renderString = (value: string) => {
      const maxLength = 100;
      const isTooLong = value.length > maxLength;

      if (!isTooLong) {
        return (
          <div className="flex mr-1 rounded hover:bg-gray-800/20">
            {name && (
              <span className="mr-1 text-gray-600 dark:text-gray-400">
                {name}:
              </span>
            )}
            <pre
              className={clsx(
                isError ? typeStyleMap.error : typeStyleMap.string,
                "break-all whitespace-pre-wrap",
              )}
            >
              "{value}"
            </pre>
          </div>
        );
      }

      return (
        <div className="flex mr-1 rounded group hover:bg-gray-800/20">
          {name && (
            <span className="mr-1 text-gray-600 dark:text-gray-400 dark:group-hover:text-gray-100 group-hover:text-gray-400">
              {name}:
            </span>
          )}
          <pre
            className={clsx(
              isError ? typeStyleMap.error : typeStyleMap.string,
              "cursor-pointer break-all whitespace-pre-wrap",
            )}
            onClick={() => setIsExpanded(!isExpanded)}
            title={isExpanded ? "Click to collapse" : "Click to expand"}
          >
            {isExpanded ? `"${value}"` : `"${value.slice(0, maxLength)}..."`}
          </pre>
        </div>
      );
    };

    switch (dataType) {
      case "object":
      case "array":
        return renderCollapsible(dataType === "array");
      case "string":
        return renderString(data as string);
      default:
        return (
          <div className="flex items-center mr-1 rounded hover:bg-gray-800/20">
            {name && (
              <span className="mr-1 text-gray-600 dark:text-gray-400">
                {name}:
              </span>
            )}
            <span className={typeStyleMap[dataType] || typeStyleMap.default}>
              {data === null ? "null" : String(data)}
            </span>
          </div>
        );
    }
  },
);

JsonNode.displayName = "JsonNode";

export default JsonView;

```

## /client/src/components/ListPane.tsx

```tsx path="/client/src/components/ListPane.tsx" 
import { Button } from "./ui/button";

type ListPaneProps<T> = {
  items: T[];
  listItems: () => void;
  clearItems: () => void;
  setSelectedItem: (item: T) => void;
  renderItem: (item: T) => React.ReactNode;
  title: string;
  buttonText: string;
  isButtonDisabled?: boolean;
};

const ListPane = <T extends object>({
  items,
  listItems,
  clearItems,
  setSelectedItem,
  renderItem,
  title,
  buttonText,
  isButtonDisabled,
}: ListPaneProps<T>) => (
  <div className="bg-card border border-border rounded-lg shadow">
    <div className="p-4 border-b border-gray-200 dark:border-border">
      <h3 className="font-semibold dark:text-white">{title}</h3>
    </div>
    <div className="p-4">
      <Button
        variant="outline"
        className="w-full mb-4"
        onClick={listItems}
        disabled={isButtonDisabled}
      >
        {buttonText}
      </Button>
      <Button
        variant="outline"
        className="w-full mb-4"
        onClick={clearItems}
        disabled={items.length === 0}
      >
        Clear
      </Button>
      <div className="space-y-2 overflow-y-auto max-h-96">
        {items.map((item, index) => (
          <div
            key={index}
            className="flex items-center py-2 px-4 rounded hover:bg-gray-50 dark:hover:bg-secondary cursor-pointer"
            onClick={() => setSelectedItem(item)}
          >
            {renderItem(item)}
          </div>
        ))}
      </div>
    </div>
  </div>
);

export default ListPane;

```

## /client/src/components/OAuthCallback.tsx

```tsx path="/client/src/components/OAuthCallback.tsx" 
import { useEffect, useRef } from "react";
import { InspectorOAuthClientProvider } from "../lib/auth";
import { SESSION_KEYS } from "../lib/constants";
import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
import { useToast } from "@/lib/hooks/useToast";
import {
  generateOAuthErrorDescription,
  parseOAuthCallbackParams,
} from "@/utils/oauthUtils.ts";

interface OAuthCallbackProps {
  onConnect: (serverUrl: string) => void;
}

const OAuthCallback = ({ onConnect }: OAuthCallbackProps) => {
  const { toast } = useToast();
  const hasProcessedRef = useRef(false);

  useEffect(() => {
    const handleCallback = async () => {
      // Skip if we've already processed this callback
      if (hasProcessedRef.current) {
        return;
      }
      hasProcessedRef.current = true;

      const notifyError = (description: string) =>
        void toast({
          title: "OAuth Authorization Error",
          description,
          variant: "destructive",
        });

      const params = parseOAuthCallbackParams(window.location.search);
      if (!params.successful) {
        return notifyError(generateOAuthErrorDescription(params));
      }

      const serverUrl = sessionStorage.getItem(SESSION_KEYS.SERVER_URL);
      if (!serverUrl) {
        return notifyError("Missing Server URL");
      }

      let result;
      try {
        // Create an auth provider with the current server URL
        const serverAuthProvider = new InspectorOAuthClientProvider(serverUrl);

        result = await auth(serverAuthProvider, {
          serverUrl,
          authorizationCode: params.code,
        });
      } catch (error) {
        console.error("OAuth callback error:", error);
        return notifyError(`Unexpected error occurred: ${error}`);
      }

      if (result !== "AUTHORIZED") {
        return notifyError(
          `Expected to be authorized after providing auth code, got: ${result}`,
        );
      }

      // Finally, trigger auto-connect
      toast({
        title: "Success",
        description: "Successfully authenticated with OAuth",
        variant: "default",
      });
      onConnect(serverUrl);
    };

    handleCallback().finally(() => {
      window.history.replaceState({}, document.title, "/");
    });
  }, [toast, onConnect]);

  return (
    <div className="flex items-center justify-center h-screen">
      <p className="text-lg text-gray-500">Processing OAuth callback...</p>
    </div>
  );
};

export default OAuthCallback;

```

## /client/src/components/OAuthDebugCallback.tsx

```tsx path="/client/src/components/OAuthDebugCallback.tsx" 
import { useEffect } from "react";
import { SESSION_KEYS } from "../lib/constants";
import {
  generateOAuthErrorDescription,
  parseOAuthCallbackParams,
} from "@/utils/oauthUtils.ts";
import { AuthDebuggerState } from "@/lib/auth-types";

interface OAuthCallbackProps {
  onConnect: ({
    authorizationCode,
    errorMsg,
    restoredState,
  }: {
    authorizationCode?: string;
    errorMsg?: string;
    restoredState?: AuthDebuggerState;
  }) => void;
}

const OAuthDebugCallback = ({ onConnect }: OAuthCallbackProps) => {
  useEffect(() => {
    let isProcessed = false;

    const handleCallback = async () => {
      // Skip if we've already processed this callback
      if (isProcessed) {
        return;
      }
      isProcessed = true;

      const params = parseOAuthCallbackParams(window.location.search);
      if (!params.successful) {
        const errorMsg = generateOAuthErrorDescription(params);
        onConnect({ errorMsg });
        return;
      }

      const serverUrl = sessionStorage.getItem(SESSION_KEYS.SERVER_URL);

      // Try to restore the auth state
      const storedState = sessionStorage.getItem(
        SESSION_KEYS.AUTH_DEBUGGER_STATE,
      );
      let restoredState = null;
      if (storedState) {
        try {
          restoredState = JSON.parse(storedState);
          // Clean up the stored state
          sessionStorage.removeItem(SESSION_KEYS.AUTH_DEBUGGER_STATE);
        } catch (e) {
          console.error("Failed to parse stored auth state:", e);
        }
      }

      // ServerURL isn't set, this can happen if we've opened the
      // authentication request in a new tab, so we don't have the same
      // session storage
      if (!serverUrl) {
        // If there's no server URL, we're likely in a new tab
        // Just display the code for manual copying
        return;
      }

      if (!params.code) {
        onConnect({ errorMsg: "Missing authorization code" });
        return;
      }

      // Instead of storing in sessionStorage, pass the code directly
      // to the auth state manager through onConnect, along with restored state
      onConnect({ authorizationCode: params.code, restoredState });
    };

    handleCallback().finally(() => {
      // Only redirect if we have the URL set, otherwise assume this was
      // in a new tab
      if (sessionStorage.getItem(SESSION_KEYS.SERVER_URL)) {
        window.history.replaceState({}, document.title, "/");
      }
    });

    return () => {
      isProcessed = true;
    };
  }, [onConnect]);

  const callbackParams = parseOAuthCallbackParams(window.location.search);

  return (
    <div className="flex items-center justify-center h-screen">
      <div className="mt-4 p-4 bg-secondary rounded-md max-w-md">
        <p className="mb-2 text-sm">
          Please copy this authorization code and return to the Auth Debugger:
        </p>
        <code className="block p-2 bg-muted rounded-sm overflow-x-auto text-xs">
          {callbackParams.successful && "code" in callbackParams
            ? callbackParams.code
            : `No code found: ${callbackParams.error}, ${callbackParams.error_description}`}
        </code>
        <p className="mt-4 text-xs text-muted-foreground">
          Close this tab and paste the code in the OAuth flow to complete
          authentication.
        </p>
      </div>
    </div>
  );
};

export default OAuthDebugCallback;

```

## /client/src/components/OAuthFlowProgress.tsx

```tsx path="/client/src/components/OAuthFlowProgress.tsx" 
import { AuthDebuggerState, OAuthStep } from "@/lib/auth-types";
import { CheckCircle2, Circle, ExternalLink } from "lucide-react";
import { Button } from "./ui/button";
import { DebugInspectorOAuthClientProvider } from "@/lib/auth";
import { useEffect, useMemo, useState } from "react";
import { OAuthClientInformation } from "@modelcontextprotocol/sdk/shared/auth.js";

interface OAuthStepProps {
  label: string;
  isComplete: boolean;
  isCurrent: boolean;
  error?: Error | null;
  children?: React.ReactNode;
}

const OAuthStepDetails = ({
  label,
  isComplete,
  isCurrent,
  error,
  children,
}: OAuthStepProps) => {
  return (
    <div>
      <div
        className={`flex items-center p-2 rounded-md ${isCurrent ? "bg-accent" : ""}`}
      >
        {isComplete ? (
          <CheckCircle2 className="h-5 w-5 text-green-500 mr-2" />
        ) : (
          <Circle className="h-5 w-5 text-muted-foreground mr-2" />
        )}
        <span className={`${isCurrent ? "font-medium" : ""}`}>{label}</span>
      </div>

      {/* Show children if current step or complete and children exist */}
      {(isCurrent || isComplete) && children && (
        <div className="ml-7 mt-1">{children}</div>
      )}

      {/* Display error if current step and an error exists */}
      {isCurrent && error && (
        <div className="ml-7 mt-2 p-3 border border-red-300 bg-red-50 rounded-md">
          <p className="text-sm font-medium text-red-700">Error:</p>
          <p className="text-xs text-red-600 mt-1">{error.message}</p>
        </div>
      )}
    </div>
  );
};

interface OAuthFlowProgressProps {
  serverUrl: string;
  authState: AuthDebuggerState;
  updateAuthState: (updates: Partial<AuthDebuggerState>) => void;
  proceedToNextStep: () => Promise<void>;
}

const steps: Array<OAuthStep> = [
  "metadata_discovery",
  "client_registration",
  "authorization_redirect",
  "authorization_code",
  "token_request",
  "complete",
];

export const OAuthFlowProgress = ({
  serverUrl,
  authState,
  updateAuthState,
  proceedToNextStep,
}: OAuthFlowProgressProps) => {
  const provider = useMemo(
    () => new DebugInspectorOAuthClientProvider(serverUrl),
    [serverUrl],
  );
  const [clientInfo, setClientInfo] = useState<OAuthClientInformation | null>(
    null,
  );

  const currentStepIdx = steps.findIndex((s) => s === authState.oauthStep);

  useEffect(() => {
    const fetchClientInfo = async () => {
      if (authState.oauthClientInfo) {
        setClientInfo(authState.oauthClientInfo);
      } else {
        try {
          const info = await provider.clientInformation();
          if (info) {
            setClientInfo(info);
          }
        } catch (error) {
          console.error("Failed to fetch client information:", error);
        }
      }
    };

    if (currentStepIdx > steps.indexOf("client_registration")) {
      fetchClientInfo();
    }
  }, [
    provider,
    authState.oauthStep,
    authState.oauthClientInfo,
    currentStepIdx,
  ]);

  // Helper to get step props
  const getStepProps = (stepName: OAuthStep) => ({
    isComplete:
      currentStepIdx > steps.indexOf(stepName) ||
      currentStepIdx === steps.length - 1, // last step is "complete"
    isCurrent: authState.oauthStep === stepName,
    error: authState.oauthStep === stepName ? authState.latestError : null,
  });

  return (
    <div className="rounded-md border p-6 space-y-4 mt-4">
      <h3 className="text-lg font-medium">OAuth Flow Progress</h3>
      <p className="text-sm text-muted-foreground">
        Follow these steps to complete OAuth authentication with the server.
      </p>

      <div className="space-y-3">
        <OAuthStepDetails
          label="Metadata Discovery"
          {...getStepProps("metadata_discovery")}
        >
          {authState.oauthMetadata && (
            <details className="text-xs mt-2">
              <summary className="cursor-pointer text-muted-foreground font-medium">
                OAuth Metadata Sources
                {!authState.resourceMetadata && " ℹ️"}
              </summary>

              {authState.resourceMetadata && (
                <div className="mt-2">
                  <p className="font-medium">Resource Metadata:</p>
                  <p className="text-xs text-muted-foreground">
                    From{" "}
                    {
                      new URL(
                        "/.well-known/oauth-protected-resource",
                        serverUrl,
                      ).href
                    }
                  </p>
                  <pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
                    {JSON.stringify(authState.resourceMetadata, null, 2)}
                  </pre>
                </div>
              )}

              {authState.resourceMetadataError && (
                <div className="mt-2 p-3 border border-blue-300 bg-blue-50 rounded-md">
                  <p className="text-sm font-medium text-blue-700">
                    ℹ️ Problem with resource metadata from{" "}
                    <a
                      href={
                        new URL(
                          "/.well-known/oauth-protected-resource",
                          serverUrl,
                        ).href
                      }
                      target="_blank"
                      rel="noopener noreferrer"
                      className="text-blue-500 hover:text-blue-700"
                    >
                      {
                        new URL(
                          "/.well-known/oauth-protected-resource",
                          serverUrl,
                        ).href
                      }
                    </a>
                  </p>
                  <p className="text-xs text-blue-600 mt-1">
                    Resource metadata was added in the{" "}
                    <a href="https://modelcontextprotocol.io/specification/draft/basic/authorization#2-3-1-authorization-server-location">
                      2025-DRAFT-v2 specification update
                    </a>
                    <br />
                    {authState.resourceMetadataError.message}
                    {authState.resourceMetadataError instanceof TypeError &&
                      " (This could indicate the endpoint doesn't exist or does not have CORS configured)"}
                  </p>
                </div>
              )}

              {authState.oauthMetadata && (
                <div className="mt-2">
                  <p className="font-medium">Authorization Server Metadata:</p>
                  {authState.authServerUrl && (
                    <p className="text-xs text-muted-foreground">
                      From{" "}
                      {
                        new URL(
                          "/.well-known/oauth-authorization-server",
                          authState.authServerUrl,
                        ).href
                      }
                    </p>
                  )}
                  <pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
                    {JSON.stringify(authState.oauthMetadata, null, 2)}
                  </pre>
                </div>
              )}
            </details>
          )}
        </OAuthStepDetails>

        <OAuthStepDetails
          label="Client Registration"
          {...getStepProps("client_registration")}
        >
          {clientInfo && (
            <details className="text-xs mt-2">
              <summary className="cursor-pointer text-muted-foreground font-medium">
                Registered Client Information
              </summary>
              <pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
                {JSON.stringify(clientInfo, null, 2)}
              </pre>
            </details>
          )}
        </OAuthStepDetails>

        <OAuthStepDetails
          label="Preparing Authorization"
          {...getStepProps("authorization_redirect")}
        >
          {authState.authorizationUrl && (
            <div className="mt-2 p-3 border rounded-md bg-muted">
              <p className="font-medium mb-2 text-sm">Authorization URL:</p>
              <div className="flex items-center gap-2">
                <p className="text-xs break-all">
                  {authState.authorizationUrl}
                </p>
                <a
                  href={authState.authorizationUrl}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="flex items-center text-blue-500 hover:text-blue-700"
                  aria-label="Open authorization URL in new tab"
                  title="Open authorization URL"
                >
                  <ExternalLink className="h-4 w-4" />
                </a>
              </div>
              <p className="text-xs text-muted-foreground mt-2">
                Click the link to authorize in your browser. After
                authorization, you'll be redirected back to continue the flow.
              </p>
            </div>
          )}
        </OAuthStepDetails>

        <OAuthStepDetails
          label="Request Authorization and acquire authorization code"
          {...getStepProps("authorization_code")}
        >
          <div className="mt-3">
            <label
              htmlFor="authCode"
              className="block text-sm font-medium mb-1"
            >
              Authorization Code
            </label>
            <div className="flex gap-2">
              <input
                id="authCode"
                value={authState.authorizationCode}
                onChange={(e) => {
                  updateAuthState({
                    authorizationCode: e.target.value,
                    validationError: null,
                  });
                }}
                placeholder="Enter the code from the authorization server"
                className={`flex h-9 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${
                  authState.validationError ? "border-red-500" : "border-input"
                }`}
              />
            </div>
            {authState.validationError && (
              <p className="text-xs text-red-600 mt-1">
                {authState.validationError}
              </p>
            )}
            <p className="text-xs text-muted-foreground mt-1">
              Once you've completed authorization in the link, paste the code
              here.
            </p>
          </div>
        </OAuthStepDetails>

        <OAuthStepDetails
          label="Token Request"
          {...getStepProps("token_request")}
        >
          {authState.oauthMetadata && (
            <details className="text-xs mt-2">
              <summary className="cursor-pointer text-muted-foreground font-medium">
                Token Request Details
              </summary>
              <div className="mt-2 p-2 bg-muted rounded-md">
                <p className="font-medium">Token Endpoint:</p>
                <code className="block mt-1 text-xs overflow-x-auto">
                  {authState.oauthMetadata.token_endpoint}
                </code>
              </div>
            </details>
          )}
        </OAuthStepDetails>

        <OAuthStepDetails
          label="Authentication Complete"
          {...getStepProps("complete")}
        >
          {authState.oauthTokens && (
            <details className="text-xs mt-2">
              <summary className="cursor-pointer text-muted-foreground font-medium">
                Access Tokens
              </summary>
              <p className="mt-1 text-sm">
                Authentication successful! You can now use the authenticated
                connection. These tokens will be used automatically for server
                requests.
              </p>
              <pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
                {JSON.stringify(authState.oauthTokens, null, 2)}
              </pre>
            </details>
          )}
        </OAuthStepDetails>
      </div>

      <div className="flex gap-3 mt-4">
        {authState.oauthStep !== "complete" && (
          <>
            <Button
              onClick={proceedToNextStep}
              disabled={authState.isInitiatingAuth}
            >
              {authState.isInitiatingAuth ? "Processing..." : "Continue"}
            </Button>
          </>
        )}

        {authState.oauthStep === "authorization_redirect" &&
          authState.authorizationUrl && (
            <Button
              variant="outline"
              onClick={() => window.open(authState.authorizationUrl!, "_blank")}
            >
              Open in New Tab
            </Button>
          )}
      </div>
    </div>
  );
};

```

## /client/src/components/PingTab.tsx

```tsx path="/client/src/components/PingTab.tsx" 
import { TabsContent } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";

const PingTab = ({ onPingClick }: { onPingClick: () => void }) => {
  return (
    <TabsContent value="ping">
      <div className="grid grid-cols-2 gap-4">
        <div className="col-span-2 flex justify-center items-center">
          <Button
            onClick={onPingClick}
            className="font-bold py-6 px-12 rounded-full"
          >
            Ping Server
          </Button>
        </div>
      </div>
    </TabsContent>
  );
};

export default PingTab;

```

## /client/src/components/PromptsTab.tsx

```tsx path="/client/src/components/PromptsTab.tsx" 
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Combobox } from "@/components/ui/combobox";
import { Label } from "@/components/ui/label";
import { TabsContent } from "@/components/ui/tabs";

import {
  ListPromptsResult,
  PromptReference,
  ResourceReference,
} from "@modelcontextprotocol/sdk/types.js";
import { AlertCircle } from "lucide-react";
import { useEffect, useState } from "react";
import ListPane from "./ListPane";
import { useCompletionState } from "@/lib/hooks/useCompletionState";
import JsonView from "./JsonView";

export type Prompt = {
  name: string;
  description?: string;
  arguments?: {
    name: string;
    description?: string;
    required?: boolean;
  }[];
};

const PromptsTab = ({
  prompts,
  listPrompts,
  clearPrompts,
  getPrompt,
  selectedPrompt,
  setSelectedPrompt,
  handleCompletion,
  completionsSupported,
  promptContent,
  nextCursor,
  error,
}: {
  prompts: Prompt[];
  listPrompts: () => void;
  clearPrompts: () => void;
  getPrompt: (name: string, args: Record<string, string>) => void;
  selectedPrompt: Prompt | null;
  setSelectedPrompt: (prompt: Prompt | null) => void;
  handleCompletion: (
    ref: PromptReference | ResourceReference,
    argName: string,
    value: string,
  ) => Promise<string[]>;
  completionsSupported: boolean;
  promptContent: string;
  nextCursor: ListPromptsResult["nextCursor"];
  error: string | null;
}) => {
  const [promptArgs, setPromptArgs] = useState<Record<string, string>>({});
  const { completions, clearCompletions, requestCompletions } =
    useCompletionState(handleCompletion, completionsSupported);

  useEffect(() => {
    clearCompletions();
  }, [clearCompletions, selectedPrompt]);

  const handleInputChange = async (argName: string, value: string) => {
    setPromptArgs((prev) => ({ ...prev, [argName]: value }));

    if (selectedPrompt) {
      requestCompletions(
        {
          type: "ref/prompt",
          name: selectedPrompt.name,
        },
        argName,
        value,
      );
    }
  };

  const handleGetPrompt = () => {
    if (selectedPrompt) {
      getPrompt(selectedPrompt.name, promptArgs);
    }
  };

  return (
    <TabsContent value="prompts">
      <div className="grid grid-cols-2 gap-4">
        <ListPane
          items={prompts}
          listItems={listPrompts}
          clearItems={() => {
            clearPrompts();
            setSelectedPrompt(null);
          }}
          setSelectedItem={(prompt) => {
            setSelectedPrompt(prompt);
            setPromptArgs({});
          }}
          renderItem={(prompt) => (
            <div className="flex flex-col items-start">
              <span className="flex-1">{prompt.name}</span>
              <span className="text-sm text-gray-500 text-left">
                {prompt.description}
              </span>
            </div>
          )}
          title="Prompts"
          buttonText={nextCursor ? "List More Prompts" : "List Prompts"}
          isButtonDisabled={!nextCursor && prompts.length > 0}
        />

        <div className="bg-card border border-border rounded-lg shadow">
          <div className="p-4 border-b border-gray-200 dark:border-border">
            <h3 className="font-semibold">
              {selectedPrompt ? selectedPrompt.name : "Select a prompt"}
            </h3>
          </div>
          <div className="p-4">
            {error ? (
              <Alert variant="destructive">
                <AlertCircle className="h-4 w-4" />
                <AlertTitle>Error</AlertTitle>
                <AlertDescription>{error}</AlertDescription>
              </Alert>
            ) : selectedPrompt ? (
              <div className="space-y-4">
                {selectedPrompt.description && (
                  <p className="text-sm text-gray-600 dark:text-gray-400">
                    {selectedPrompt.description}
                  </p>
                )}
                {selectedPrompt.arguments?.map((arg) => (
                  <div key={arg.name}>
                    <Label htmlFor={arg.name}>{arg.name}</Label>
                    <Combobox
                      id={arg.name}
                      placeholder={`Enter ${arg.name}`}
                      value={promptArgs[arg.name] || ""}
                      onChange={(value) => handleInputChange(arg.name, value)}
                      onInputChange={(value) =>
                        handleInputChange(arg.name, value)
                      }
                      options={completions[arg.name] || []}
                    />

                    {arg.description && (
                      <p className="text-xs text-gray-500 mt-1">
                        {arg.description}
                        {arg.required && (
                          <span className="text-xs mt-1 ml-1">(Required)</span>
                        )}
                      </p>
                    )}
                  </div>
                ))}
                <Button onClick={handleGetPrompt} className="w-full">
                  Get Prompt
                </Button>
                {promptContent && (
                  <JsonView data={promptContent} withCopyButton={false} />
                )}
              </div>
            ) : (
              <Alert>
                <AlertDescription>
                  Select a prompt from the list to view and use it
                </AlertDescription>
              </Alert>
            )}
          </div>
        </div>
      </div>
    </TabsContent>
  );
};

export default PromptsTab;

```

## /client/src/components/ResourcesTab.tsx

```tsx path="/client/src/components/ResourcesTab.tsx" 
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Combobox } from "@/components/ui/combobox";
import { TabsContent } from "@/components/ui/tabs";
import {
  ListResourcesResult,
  Resource,
  ResourceTemplate,
  ListResourceTemplatesResult,
  ResourceReference,
  PromptReference,
} from "@modelcontextprotocol/sdk/types.js";
import { AlertCircle, ChevronRight, FileText, RefreshCw } from "lucide-react";
import ListPane from "./ListPane";
import { useEffect, useState } from "react";
import { useCompletionState } from "@/lib/hooks/useCompletionState";
import JsonView from "./JsonView";
import { UriTemplate } from "@modelcontextprotocol/sdk/shared/uriTemplate.js";

const ResourcesTab = ({
  resources,
  resourceTemplates,
  listResources,
  clearResources,
  listResourceTemplates,
  clearResourceTemplates,
  readResource,
  selectedResource,
  setSelectedResource,
  resourceSubscriptionsSupported,
  resourceSubscriptions,
  subscribeToResource,
  unsubscribeFromResource,
  handleCompletion,
  completionsSupported,
  resourceContent,
  nextCursor,
  nextTemplateCursor,
  error,
}: {
  resources: Resource[];
  resourceTemplates: ResourceTemplate[];
  listResources: () => void;
  clearResources: () => void;
  listResourceTemplates: () => void;
  clearResourceTemplates: () => void;
  readResource: (uri: string) => void;
  selectedResource: Resource | null;
  setSelectedResource: (resource: Resource | null) => void;
  handleCompletion: (
    ref: ResourceReference | PromptReference,
    argName: string,
    value: string,
  ) => Promise<string[]>;
  completionsSupported: boolean;
  resourceContent: string;
  nextCursor: ListResourcesResult["nextCursor"];
  nextTemplateCursor: ListResourceTemplatesResult["nextCursor"];
  error: string | null;
  resourceSubscriptionsSupported: boolean;
  resourceSubscriptions: Set<string>;
  subscribeToResource: (uri: string) => void;
  unsubscribeFromResource: (uri: string) => void;
}) => {
  const [selectedTemplate, setSelectedTemplate] =
    useState<ResourceTemplate | null>(null);
  const [templateValues, setTemplateValues] = useState<Record<string, string>>(
    {},
  );

  const { completions, clearCompletions, requestCompletions } =
    useCompletionState(handleCompletion, completionsSupported);

  useEffect(() => {
    clearCompletions();
  }, [clearCompletions]);

  const fillTemplate = (
    template: string,
    values: Record<string, string>,
  ): string => {
    return new UriTemplate(template).expand(values);
  };

  const handleTemplateValueChange = async (key: string, value: string) => {
    setTemplateValues((prev) => ({ ...prev, [key]: value }));

    if (selectedTemplate?.uriTemplate) {
      requestCompletions(
        {
          type: "ref/resource",
          uri: selectedTemplate.uriTemplate,
        },
        key,
        value,
      );
    }
  };

  const handleReadTemplateResource = () => {
    if (selectedTemplate) {
      const uri = fillTemplate(selectedTemplate.uriTemplate, templateValues);
      readResource(uri);
      // We don't have the full Resource object here, so we create a partial one
      setSelectedResource({ uri, name: uri } as Resource);
    }
  };

  return (
    <TabsContent value="resources">
      <div className="grid grid-cols-3 gap-4">
        <ListPane
          items={resources}
          listItems={listResources}
          clearItems={() => {
            clearResources();
            // Condition to check if selected resource is not resource template's resource
            if (!selectedTemplate) {
              setSelectedResource(null);
            }
          }}
          setSelectedItem={(resource) => {
            setSelectedResource(resource);
            readResource(resource.uri);
            setSelectedTemplate(null);
          }}
          renderItem={(resource) => (
            <div className="flex items-center w-full">
              <FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
              <span className="flex-1 truncate" title={resource.uri.toString()}>
                {resource.name}
              </span>
              <ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
            </div>
          )}
          title="Resources"
          buttonText={nextCursor ? "List More Resources" : "List Resources"}
          isButtonDisabled={!nextCursor && resources.length > 0}
        />

        <ListPane
          items={resourceTemplates}
          listItems={listResourceTemplates}
          clearItems={() => {
            clearResourceTemplates();
            // Condition to check if selected resource is resource template's resource
            if (selectedTemplate) {
              setSelectedResource(null);
            }
            setSelectedTemplate(null);
          }}
          setSelectedItem={(template) => {
            setSelectedTemplate(template);
            setSelectedResource(null);
            setTemplateValues({});
          }}
          renderItem={(template) => (
            <div className="flex items-center w-full">
              <FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
              <span className="flex-1 truncate" title={template.uriTemplate}>
                {template.name}
              </span>
              <ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
            </div>
          )}
          title="Resource Templates"
          buttonText={
            nextTemplateCursor ? "List More Templates" : "List Templates"
          }
          isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0}
        />

        <div className="bg-card border border-border rounded-lg shadow">
          <div className="p-4 border-b border-gray-200 dark:border-border flex justify-between items-center">
            <h3
              className="font-semibold truncate"
              title={selectedResource?.name || selectedTemplate?.name}
            >
              {selectedResource
                ? selectedResource.name
                : selectedTemplate
                  ? selectedTemplate.name
                  : "Select a resource or template"}
            </h3>
            {selectedResource && (
              <div className="flex row-auto gap-1 justify-end w-2/5">
                {resourceSubscriptionsSupported &&
                  !resourceSubscriptions.has(selectedResource.uri) && (
                    <Button
                      variant="outline"
                      size="sm"
                      onClick={() => subscribeToResource(selectedResource.uri)}
                    >
                      Subscribe
                    </Button>
                  )}
                {resourceSubscriptionsSupported &&
                  resourceSubscriptions.has(selectedResource.uri) && (
                    <Button
                      variant="outline"
                      size="sm"
                      onClick={() =>
                        unsubscribeFromResource(selectedResource.uri)
                      }
                    >
                      Unsubscribe
                    </Button>
                  )}
                <Button
                  variant="outline"
                  size="sm"
                  onClick={() => readResource(selectedResource.uri)}
                >
                  <RefreshCw className="w-4 h-4 mr-2" />
                  Refresh
                </Button>
              </div>
            )}
          </div>
          <div className="p-4">
            {error ? (
              <Alert variant="destructive">
                <AlertCircle className="h-4 w-4" />
                <AlertTitle>Error</AlertTitle>
                <AlertDescription>{error}</AlertDescription>
              </Alert>
            ) : selectedResource ? (
              <JsonView
                data={resourceContent}
                className="bg-gray-50 dark:bg-gray-800 p-4 rounded text-sm overflow-auto max-h-96 text-gray-900 dark:text-gray-100"
              />
            ) : selectedTemplate ? (
              <div className="space-y-4">
                <p className="text-sm text-gray-600 dark:text-gray-400">
                  {selectedTemplate.description}
                </p>
                {new UriTemplate(
                  selectedTemplate.uriTemplate,
                ).variableNames?.map((key) => {
                  return (
                    <div key={key}>
                      <Label htmlFor={key}>{key}</Label>
                      <Combobox
                        id={key}
                        placeholder={`Enter ${key}`}
                        value={templateValues[key] || ""}
                        onChange={(value) =>
                          handleTemplateValueChange(key, value)
                        }
                        onInputChange={(value) =>
                          handleTemplateValueChange(key, value)
                        }
                        options={completions[key] || []}
                      />
                    </div>
                  );
                })}
                <Button
                  onClick={handleReadTemplateResource}
                  disabled={Object.keys(templateValues).length === 0}
                >
                  Read Resource
                </Button>
              </div>
            ) : (
              <Alert>
                <AlertDescription>
                  Select a resource or template from the list to view its
                  contents
                </AlertDescription>
              </Alert>
            )}
          </div>
        </div>
      </div>
    </TabsContent>
  );
};

export default ResourcesTab;

```

## /client/src/components/RootsTab.tsx

```tsx path="/client/src/components/RootsTab.tsx" 
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { TabsContent } from "@/components/ui/tabs";
import { Root } from "@modelcontextprotocol/sdk/types.js";
import { Plus, Minus, Save } from "lucide-react";

const RootsTab = ({
  roots,
  setRoots,
  onRootsChange,
}: {
  roots: Root[];
  setRoots: React.Dispatch<React.SetStateAction<Root[]>>;
  onRootsChange: () => void;
}) => {
  const addRoot = () => {
    setRoots((currentRoots) => [...currentRoots, { uri: "file://", name: "" }]);
  };

  const removeRoot = (index: number) => {
    setRoots((currentRoots) => currentRoots.filter((_, i) => i !== index));
  };

  const updateRoot = (index: number, field: keyof Root, value: string) => {
    setRoots((currentRoots) =>
      currentRoots.map((root, i) =>
        i === index ? { ...root, [field]: value } : root,
      ),
    );
  };

  const handleSave = () => {
    onRootsChange();
  };

  return (
    <TabsContent value="roots">
      <div className="space-y-4">
        <Alert>
          <AlertDescription>
            Configure the root directories that the server can access
          </AlertDescription>
        </Alert>

        {roots.map((root, index) => (
          <div key={index} className="flex gap-2 items-center">
            <Input
              placeholder="file:// URI"
              value={root.uri}
              onChange={(e) => updateRoot(index, "uri", e.target.value)}
              className="flex-1"
            />
            <Button
              variant="destructive"
              size="sm"
              onClick={() => removeRoot(index)}
            >
              <Minus className="h-4 w-4" />
            </Button>
          </div>
        ))}

        <div className="flex gap-2">
          <Button variant="outline" onClick={addRoot}>
            <Plus className="h-4 w-4 mr-2" />
            Add Root
          </Button>
          <Button onClick={handleSave}>
            <Save className="h-4 w-4 mr-2" />
            Save Changes
          </Button>
        </div>
      </div>
    </TabsContent>
  );
};

export default RootsTab;

```

## /client/src/components/SamplingRequest.tsx

```tsx path="/client/src/components/SamplingRequest.tsx" 
import { Button } from "@/components/ui/button";
import JsonView from "./JsonView";
import { useMemo, useState } from "react";
import {
  CreateMessageResult,
  CreateMessageResultSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { PendingRequest } from "./SamplingTab";
import DynamicJsonForm from "./DynamicJsonForm";
import { useToast } from "@/lib/hooks/useToast";
import { JsonSchemaType, JsonValue } from "@/utils/jsonUtils";

export type SamplingRequestProps = {
  request: PendingRequest;
  onApprove: (id: number, result: CreateMessageResult) => void;
  onReject: (id: number) => void;
};

const SamplingRequest = ({
  onApprove,
  request,
  onReject,
}: SamplingRequestProps) => {
  const { toast } = useToast();

  const [messageResult, setMessageResult] = useState<JsonValue>({
    model: "stub-model",
    stopReason: "endTurn",
    role: "assistant",
    content: {
      type: "text",
      text: "",
    },
  });

  const contentType = (
    (messageResult as { [key: string]: JsonValue })?.content as {
      [key: string]: JsonValue;
    }
  )?.type;

  const schema = useMemo(() => {
    const s: JsonSchemaType = {
      type: "object",
      description: "Message result",
      properties: {
        model: {
          type: "string",
          default: "stub-model",
          description: "model name",
        },
        stopReason: {
          type: "string",
          default: "endTurn",
          description: "Stop reason",
        },
        role: {
          type: "string",
          default: "endTurn",
          description: "Role of the model",
        },
        content: {
          type: "object",
          properties: {
            type: {
              type: "string",
              default: "text",
              description: "Type of content",
            },
          },
        },
      },
    };

    if (contentType === "text" && s.properties) {
      s.properties.content.properties = {
        ...s.properties.content.properties,
        text: {
          type: "string",
          default: "",
          description: "text content",
        },
      };
      setMessageResult((prev) => ({
        ...(prev as { [key: string]: JsonValue }),
        content: {
          type: contentType,
          text: "",
        },
      }));
    } else if (contentType === "image" && s.properties) {
      s.properties.content.properties = {
        ...s.properties.content.properties,
        data: {
          type: "string",
          default: "",
          description: "Base64 encoded image data",
        },
        mimeType: {
          type: "string",
          default: "",
          description: "Mime type of the image",
        },
      };
      setMessageResult((prev) => ({
        ...(prev as { [key: string]: JsonValue }),
        content: {
          type: contentType,
          data: "",
          mimeType: "",
        },
      }));
    }

    return s;
  }, [contentType]);

  const handleApprove = (id: number) => {
    const validationResult = CreateMessageResultSchema.safeParse(messageResult);
    if (!validationResult.success) {
      toast({
        title: "Error",
        description: `There was an error validating the message result: ${validationResult.error.message}`,
        variant: "destructive",
      });
      return;
    }

    onApprove(id, validationResult.data);
  };

  return (
    <div
      data-testid="sampling-request"
      className="flex gap-4 p-4 border rounded-lg space-y-4"
    >
      <div className="flex-1 bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-2 rounded">
        <JsonView data={JSON.stringify(request.request)} />
      </div>
      <form className="flex-1 space-y-4">
        <div className="space-y-2">
          <DynamicJsonForm
            schema={schema}
            value={messageResult}
            onChange={(newValue: JsonValue) => {
              setMessageResult(newValue);
            }}
          />
        </div>
        <div className="flex space-x-2 mt-1">
          <Button type="button" onClick={() => handleApprove(request.id)}>
            Approve
          </Button>
          <Button
            type="button"
            variant="outline"
            onClick={() => onReject(request.id)}
          >
            Reject
          </Button>
        </div>
      </form>
    </div>
  );
};

export default SamplingRequest;

```

## /client/src/components/SamplingTab.tsx

```tsx path="/client/src/components/SamplingTab.tsx" 
import { Alert, AlertDescription } from "@/components/ui/alert";
import { TabsContent } from "@/components/ui/tabs";
import {
  CreateMessageRequest,
  CreateMessageResult,
} from "@modelcontextprotocol/sdk/types.js";
import SamplingRequest from "./SamplingRequest";

export type PendingRequest = {
  id: number;
  request: CreateMessageRequest;
};

export type Props = {
  pendingRequests: PendingRequest[];
  onApprove: (id: number, result: CreateMessageResult) => void;
  onReject: (id: number) => void;
};

const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => {
  return (
    <TabsContent value="sampling">
      <div className="h-96">
        <Alert>
          <AlertDescription>
            When the server requests LLM sampling, requests will appear here for
            approval.
          </AlertDescription>
        </Alert>
        <div className="mt-4 space-y-4">
          <h3 className="text-lg font-semibold">Recent Requests</h3>
          {pendingRequests.map((request) => (
            <SamplingRequest
              key={request.id}
              request={request}
              onApprove={onApprove}
              onReject={onReject}
            />
          ))}
          {pendingRequests.length === 0 && (
            <p className="text-gray-500">No pending requests</p>
          )}
        </div>
      </div>
    </TabsContent>
  );
};

export default SamplingTab;

```

## /client/src/components/ToolResults.tsx

```tsx path="/client/src/components/ToolResults.tsx" 
import JsonView from "./JsonView";
import {
  CallToolResultSchema,
  CompatibilityCallToolResult,
  Tool,
} from "@modelcontextprotocol/sdk/types.js";
import { validateToolOutput, hasOutputSchema } from "@/utils/schemaUtils";

interface ToolResultsProps {
  toolResult: CompatibilityCallToolResult | null;
  selectedTool: Tool | null;
}

const checkContentCompatibility = (
  structuredContent: unknown,
  unstructuredContent: Array<{
    type: string;
    text?: string;
    [key: string]: unknown;
  }>,
): { isCompatible: boolean; message: string } => {
  if (
    unstructuredContent.length !== 1 ||
    unstructuredContent[0].type !== "text"
  ) {
    return {
      isCompatible: false,
      message: "Unstructured content is not a single text block",
    };
  }

  const textContent = unstructuredContent[0].text;
  if (!textContent) {
    return {
      isCompatible: false,
      message: "Text content is empty",
    };
  }

  try {
    const parsedContent = JSON.parse(textContent);
    const isEqual =
      JSON.stringify(parsedContent) === JSON.stringify(structuredContent);

    if (isEqual) {
      return {
        isCompatible: true,
        message: "Unstructured content matches structured content",
      };
    } else {
      return {
        isCompatible: false,
        message: "Parsed JSON does not match structured content",
      };
    }
  } catch {
    return {
      isCompatible: false,
      message: "Unstructured content is not valid JSON",
    };
  }
};

const ToolResults = ({ toolResult, selectedTool }: ToolResultsProps) => {
  if (!toolResult) return null;

  if ("content" in toolResult) {
    const parsedResult = CallToolResultSchema.safeParse(toolResult);
    if (!parsedResult.success) {
      return (
        <>
          <h4 className="font-semibold mb-2">Invalid Tool Result:</h4>
          <JsonView data={toolResult} />
          <h4 className="font-semibold mb-2">Errors:</h4>
          {parsedResult.error.errors.map((error, idx) => (
            <JsonView data={error} key={idx} />
          ))}
        </>
      );
    }
    const structuredResult = parsedResult.data;
    const isError = structuredResult.isError ?? false;

    let validationResult = null;
    const toolHasOutputSchema =
      selectedTool && hasOutputSchema(selectedTool.name);

    if (toolHasOutputSchema) {
      if (!structuredResult.structuredContent && !isError) {
        validationResult = {
          isValid: false,
          error:
            "Tool has an output schema but did not return structured content",
        };
      } else if (structuredResult.structuredContent) {
        validationResult = validateToolOutput(
          selectedTool.name,
          structuredResult.structuredContent,
        );
      }
    }

    let compatibilityResult = null;
    if (
      structuredResult.structuredContent &&
      structuredResult.content.length > 0 &&
      selectedTool &&
      hasOutputSchema(selectedTool.name)
    ) {
      compatibilityResult = checkContentCompatibility(
        structuredResult.structuredContent,
        structuredResult.content,
      );
    }

    return (
      <>
        <h4 className="font-semibold mb-2">
          Tool Result:{" "}
          {isError ? (
            <span className="text-red-600 font-semibold">Error</span>
          ) : (
            <span className="text-green-600 font-semibold">Success</span>
          )}
        </h4>
        {structuredResult.structuredContent && (
          <div className="mb-4">
            <h5 className="font-semibold mb-2 text-sm">Structured Content:</h5>
            <div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
              <JsonView data={structuredResult.structuredContent} />
              {validationResult && (
                <div
                  className={`mt-2 p-2 rounded text-sm ${
                    validationResult.isValid
                      ? "bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200"
                      : "bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"
                  }`}
                >
                  {validationResult.isValid ? (
                    "✓ Valid according to output schema"
                  ) : (
                    <>✗ Validation Error: {validationResult.error}</>
                  )}
                </div>
              )}
            </div>
          </div>
        )}
        {!structuredResult.structuredContent &&
          validationResult &&
          !validationResult.isValid && (
            <div className="mb-4">
              <div className="bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 p-2 rounded text-sm">
                ✗ Validation Error: {validationResult.error}
              </div>
            </div>
          )}
        {structuredResult.content.length > 0 && (
          <div className="mb-4">
            {structuredResult.structuredContent && (
              <>
                <h5 className="font-semibold mb-2 text-sm">
                  Unstructured Content:
                </h5>
                {compatibilityResult && (
                  <div
                    className={`mb-2 p-2 rounded text-sm ${
                      compatibilityResult.isCompatible
                        ? "bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200"
                        : "bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200"
                    }`}
                  >
                    {compatibilityResult.isCompatible ? "✓" : "⚠"}{" "}
                    {compatibilityResult.message}
                  </div>
                )}
              </>
            )}
            {structuredResult.content.map((item, index) => (
              <div key={index} className="mb-2">
                {item.type === "text" && (
                  <JsonView data={item.text} isError={isError} />
                )}
                {item.type === "image" && (
                  <img
                    src={`data:${item.mimeType};base64,${item.data}`}
                    alt="Tool result image"
                    className="max-w-full h-auto"
                  />
                )}
                {item.type === "resource" &&
                  (item.resource?.mimeType?.startsWith("audio/") ? (
                    <audio
                      controls
                      src={`data:${item.resource.mimeType};base64,${item.resource.blob}`}
                      className="w-full"
                    >
                      <p>Your browser does not support audio playback</p>
                    </audio>
                  ) : (
                    <JsonView data={item.resource} />
                  ))}
              </div>
            ))}
          </div>
        )}
      </>
    );
  } else if ("toolResult" in toolResult) {
    return (
      <>
        <h4 className="font-semibold mb-2">Tool Result (Legacy):</h4>
        <JsonView data={toolResult.toolResult} />
      </>
    );
  }

  return null;
};

export default ToolResults;

```

## /client/src/components/ToolsTab.tsx

```tsx path="/client/src/components/ToolsTab.tsx" 
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { TabsContent } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import DynamicJsonForm from "./DynamicJsonForm";
import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils";
import { generateDefaultValue, isPropertyRequired } from "@/utils/schemaUtils";
import {
  CompatibilityCallToolResult,
  ListToolsResult,
  Tool,
} from "@modelcontextprotocol/sdk/types.js";
import { Loader2, Send, ChevronDown, ChevronUp } from "lucide-react";
import { useEffect, useState } from "react";
import ListPane from "./ListPane";
import JsonView from "./JsonView";
import ToolResults from "./ToolResults";

const ToolsTab = ({
  tools,
  listTools,
  clearTools,
  callTool,
  selectedTool,
  setSelectedTool,
  toolResult,
  nextCursor,
}: {
  tools: Tool[];
  listTools: () => void;
  clearTools: () => void;
  callTool: (name: string, params: Record<string, unknown>) => Promise<void>;
  selectedTool: Tool | null;
  setSelectedTool: (tool: Tool | null) => void;
  toolResult: CompatibilityCallToolResult | null;
  nextCursor: ListToolsResult["nextCursor"];
  error: string | null;
}) => {
  const [params, setParams] = useState<Record<string, unknown>>({});
  const [isToolRunning, setIsToolRunning] = useState(false);
  const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false);

  useEffect(() => {
    const params = Object.entries(
      selectedTool?.inputSchema.properties ?? [],
    ).map(([key, value]) => [
      key,
      generateDefaultValue(
        value as JsonSchemaType,
        key,
        selectedTool?.inputSchema as JsonSchemaType,
      ),
    ]);
    setParams(Object.fromEntries(params));
  }, [selectedTool]);

  return (
    <TabsContent value="tools">
      <div className="grid grid-cols-2 gap-4">
        <ListPane
          items={tools}
          listItems={listTools}
          clearItems={() => {
            clearTools();
            setSelectedTool(null);
          }}
          setSelectedItem={setSelectedTool}
          renderItem={(tool) => (
            <div className="flex flex-col items-start">
              <span className="flex-1">{tool.name}</span>
              <span className="text-sm text-gray-500 text-left">
                {tool.description}
              </span>
            </div>
          )}
          title="Tools"
          buttonText={nextCursor ? "List More Tools" : "List Tools"}
          isButtonDisabled={!nextCursor && tools.length > 0}
        />

        <div className="bg-card border border-border rounded-lg shadow">
          <div className="p-4 border-b border-gray-200 dark:border-border">
            <h3 className="font-semibold">
              {selectedTool ? selectedTool.name : "Select a tool"}
            </h3>
          </div>
          <div className="p-4">
            {selectedTool ? (
              <div className="space-y-4">
                <p className="text-sm text-gray-600 dark:text-gray-400">
                  {selectedTool.description}
                </p>
                {Object.entries(selectedTool.inputSchema.properties ?? []).map(
                  ([key, value]) => {
                    const prop = value as JsonSchemaType;
                    const inputSchema =
                      selectedTool.inputSchema as JsonSchemaType;
                    const required = isPropertyRequired(key, inputSchema);
                    return (
                      <div key={key}>
                        <Label
                          htmlFor={key}
                          className="block text-sm font-medium text-gray-700 dark:text-gray-300"
                        >
                          {key}
                          {required && (
                            <span className="text-red-500 ml-1">*</span>
                          )}
                        </Label>
                        {prop.type === "boolean" ? (
                          <div className="flex items-center space-x-2 mt-2">
                            <Checkbox
                              id={key}
                              name={key}
                              checked={!!params[key]}
                              onCheckedChange={(checked: boolean) =>
                                setParams({
                                  ...params,
                                  [key]: checked,
                                })
                              }
                            />
                            <label
                              htmlFor={key}
                              className="text-sm font-medium text-gray-700 dark:text-gray-300"
                            >
                              {prop.description || "Toggle this option"}
                            </label>
                          </div>
                        ) : prop.type === "string" ? (
                          <Textarea
                            id={key}
                            name={key}
                            placeholder={prop.description}
                            value={(params[key] as string) ?? ""}
                            onChange={(e) =>
                              setParams({
                                ...params,
                                [key]: e.target.value,
                              })
                            }
                            className="mt-1"
                          />
                        ) : prop.type === "object" || prop.type === "array" ? (
                          <div className="mt-1">
                            <DynamicJsonForm
                              schema={{
                                type: prop.type,
                                properties: prop.properties,
                                description: prop.description,
                                items: prop.items,
                              }}
                              value={
                                (params[key] as JsonValue) ??
                                generateDefaultValue(prop)
                              }
                              onChange={(newValue: JsonValue) => {
                                setParams({
                                  ...params,
                                  [key]: newValue,
                                });
                              }}
                            />
                          </div>
                        ) : prop.type === "number" ||
                          prop.type === "integer" ? (
                          <Input
                            type="number"
                            id={key}
                            name={key}
                            placeholder={prop.description}
                            value={(params[key] as string) ?? ""}
                            onChange={(e) => {
                              const value = e.target.value;
                              setParams({
                                ...params,
                                [key]: value === "" ? "" : Number(value),
                              });
                            }}
                            className="mt-1"
                          />
                        ) : (
                          <div className="mt-1">
                            <DynamicJsonForm
                              schema={{
                                type: prop.type,
                                properties: prop.properties,
                                description: prop.description,
                                items: prop.items,
                              }}
                              value={params[key] as JsonValue}
                              onChange={(newValue: JsonValue) => {
                                setParams({
                                  ...params,
                                  [key]: newValue,
                                });
                              }}
                            />
                          </div>
                        )}
                      </div>
                    );
                  },
                )}
                {selectedTool.outputSchema && (
                  <div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
                    <div className="flex items-center justify-between mb-2">
                      <h4 className="text-sm font-semibold">Output Schema:</h4>
                      <Button
                        size="sm"
                        variant="ghost"
                        onClick={() =>
                          setIsOutputSchemaExpanded(!isOutputSchemaExpanded)
                        }
                        className="h-6 px-2"
                      >
                        {isOutputSchemaExpanded ? (
                          <>
                            <ChevronUp className="h-3 w-3 mr-1" />
                            Collapse
                          </>
                        ) : (
                          <>
                            <ChevronDown className="h-3 w-3 mr-1" />
                            Expand
                          </>
                        )}
                      </Button>
                    </div>
                    <div
                      className={`transition-all ${
                        isOutputSchemaExpanded
                          ? ""
                          : "max-h-[8rem] overflow-y-auto"
                      }`}
                    >
                      <JsonView data={selectedTool.outputSchema} />
                    </div>
                  </div>
                )}
                <Button
                  onClick={async () => {
                    try {
                      setIsToolRunning(true);
                      await callTool(selectedTool.name, params);
                    } finally {
                      setIsToolRunning(false);
                    }
                  }}
                  disabled={isToolRunning}
                >
                  {isToolRunning ? (
                    <>
                      <Loader2 className="w-4 h-4 mr-2 animate-spin" />
                      Running...
                    </>
                  ) : (
                    <>
                      <Send className="w-4 h-4 mr-2" />
                      Run Tool
                    </>
                  )}
                </Button>
                <ToolResults
                  toolResult={toolResult}
                  selectedTool={selectedTool}
                />
              </div>
            ) : (
              <Alert>
                <AlertDescription>
                  Select a tool from the list to view its details and run it
                </AlertDescription>
              </Alert>
            )}
          </div>
        </div>
      </div>
    </TabsContent>
  );
};

export default ToolsTab;

```

## /client/src/components/__tests__/samplingRequest.test.tsx

```tsx path="/client/src/components/__tests__/samplingRequest.test.tsx" 
import { render, screen, fireEvent } from "@testing-library/react";
import SamplingRequest from "../SamplingRequest";
import { PendingRequest } from "../SamplingTab";

const mockRequest: PendingRequest = {
  id: 1,
  request: {
    method: "sampling/createMessage",
    params: {
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: "What files are in the current directory?",
          },
        },
      ],
      systemPrompt: "You are a helpful file system assistant.",
      includeContext: "thisServer",
      maxTokens: 100,
    },
  },
};

describe("Form to handle sampling response", () => {
  const mockOnApprove = jest.fn();
  const mockOnReject = jest.fn();

  afterEach(() => {
    jest.clearAllMocks();
  });

  it("should call onApprove with correct text content when Approve button is clicked", () => {
    render(
      <SamplingRequest
        request={mockRequest}
        onApprove={mockOnApprove}
        onReject={mockOnReject}
      />,
    );

    // Click the Approve button
    fireEvent.click(screen.getByRole("button", { name: /approve/i }));

    // Assert that onApprove is called with the correct arguments
    expect(mockOnApprove).toHaveBeenCalledWith(mockRequest.id, {
      model: "stub-model",
      stopReason: "endTurn",
      role: "assistant",
      content: {
        type: "text",
        text: "",
      },
    });
  });

  it("should call onReject with correct request id when Reject button is clicked", () => {
    render(
      <SamplingRequest
        request={mockRequest}
        onApprove={mockOnApprove}
        onReject={mockOnReject}
      />,
    );

    // Click the Approve button
    fireEvent.click(screen.getByRole("button", { name: /Reject/i }));

    // Assert that onApprove is called with the correct arguments
    expect(mockOnReject).toHaveBeenCalledWith(mockRequest.id);
  });
});

```

## /client/src/components/__tests__/samplingTab.test.tsx

```tsx path="/client/src/components/__tests__/samplingTab.test.tsx" 
import { render, screen } from "@testing-library/react";
import { Tabs } from "@/components/ui/tabs";
import SamplingTab, { PendingRequest } from "../SamplingTab";

describe("Sampling tab", () => {
  const mockOnApprove = jest.fn();
  const mockOnReject = jest.fn();

  const renderSamplingTab = (pendingRequests: PendingRequest[]) =>
    render(
      <Tabs defaultValue="sampling">
        <SamplingTab
          pendingRequests={pendingRequests}
          onApprove={mockOnApprove}
          onReject={mockOnReject}
        />
      </Tabs>,
    );

  it("should render 'No pending requests' when there are no pending requests", () => {
    renderSamplingTab([]);
    expect(
      screen.getByText(
        "When the server requests LLM sampling, requests will appear here for approval.",
      ),
    ).toBeTruthy();
    expect(screen.findByText("No pending requests")).toBeTruthy();
  });

  it("should render the correct number of requests", () => {
    renderSamplingTab(
      Array.from({ length: 5 }, (_, i) => ({
        id: i,
        request: {
          method: "sampling/createMessage",
          params: {
            messages: [
              {
                role: "user",
                content: {
                  type: "text",
                  text: "What files are in the current directory?",
                },
              },
            ],
            systemPrompt: "You are a helpful file system assistant.",
            includeContext: "thisServer",
            maxTokens: 100,
          },
        },
      })),
    );
    expect(screen.getAllByTestId("sampling-request").length).toBe(5);
  });
});

```

## /client/src/lib/utils.ts

```ts path="/client/src/lib/utils.ts" 
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

```

## /client/tsconfig.jest.json

```json path="/client/tsconfig.jest.json" 
{
  "extends": "./tsconfig.app.json",
  "compilerOptions": {
    "jsx": "react-jsx",
    "esModuleInterop": true,
    "module": "ESNext",
    "moduleResolution": "node"
  },
  "include": ["src"]
}

```


The content has been capped at 50000 tokens. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.