``` ├── .gitattributes ├── .github/ ├── workflows/ ├── main.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── CLAUDE.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── cli/ ├── package.json ├── scripts/ ├── cli-tests.js ├── make-executable.js ├── src/ ├── cli.ts ├── client/ ├── connection.ts ├── index.ts ├── prompts.ts ├── resources.ts ├── tools.ts ├── types.ts ├── error-handler.ts ├── index.ts ├── transport.ts ├── tsconfig.json ├── client/ ├── .gitignore ├── README.md ├── bin/ ├── client.js ├── start.js ├── components.json ├── eslint.config.js ├── index.html ├── jest.config.cjs ├── package.json ├── postcss.config.js ├── public/ ├── mcp.svg ├── src/ ├── App.css ├── App.tsx ├── __mocks__/ ├── styleMock.js ├── components/ ├── ConsoleTab.tsx ├── DynamicJsonForm.tsx ├── History.tsx ├── JsonEditor.tsx ├── JsonView.tsx ├── ListPane.tsx ├── OAuthCallback.tsx ├── PingTab.tsx ├── PromptsTab.tsx ├── ResourcesTab.tsx ├── RootsTab.tsx ├── SamplingRequest.tsx ├── SamplingTab.tsx ├── Sidebar.tsx ├── ToolsTab.tsx ├── __tests__/ ├── DynamicJsonForm.test.tsx ├── Sidebar.test.tsx ├── ToolsTab.test.tsx ├── samplingRequest.test.tsx ├── samplingTab.test.tsx ├── ui/ ├── alert.tsx ├── button.tsx ├── checkbox.tsx ├── combobox.tsx ├── command.tsx ├── dialog.tsx ├── input.tsx ├── label.tsx ├── popover.tsx ├── select.tsx ├── tabs.tsx ├── textarea.tsx ├── toast.tsx ``` ## /.gitattributes ```gitattributes path="/.gitattributes" package-lock.json linguist-generated=true ``` ## /.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: 18 cache: npm # Working around https://github.com/npm/cli/issues/4828 # - run: npm ci - run: npm install --no-package-lock - 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: 18 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 - run: npm run build # TODO: Add --provenance once the repo is public - run: npm run publish-all env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} ``` ## /.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 ``` ## /.npmrc ```npmrc path="/.npmrc" registry="https://registry.npmjs.org/" @modelcontextprotocol:registry="https://registry.npmjs.org/" ``` ## /.prettierignore ```prettierignore path="/.prettierignore" packages server/build CODE_OF_CONDUCT.md SECURITY.md ``` ## /.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 - `bin/`: CLI scripts ## /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://127.0.0.1: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` 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. ## /LICENSE ``` path="/LICENSE" MIT License Copyright (c) 2024 Anthropic, PBC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` ## /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) ## Running the Inspector ### Requirements - Node.js: ^22.7.5 ### 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). ### 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. ### 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 | "" | 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" } } } } ``` ### 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 npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com # 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.10.2", "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.10.0", "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}\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 = fs.mkdirSync(path.join(os.tmpdir(), "mcp-inspector-tests"), { recursive: true, }); 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}`, ); } }); // 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"], }); // 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) => { 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"], }); // 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) => { 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", ); // 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; cli: boolean; }; type CliOptions = { e?: Record; config?: string; server?: string; cli?: boolean; }; type ServerConfig = { command: string; args?: string[]; env?: Record; }; 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 { return new Promise((resolve) => setTimeout(resolve, ms, true)); } async function runWebClient(args: Args): Promise { 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; 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 { 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 = {}, ): Record { 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 ", "environment variables in KEY=VALUE format", parseKeyValuePair, {}, ) .option("--config ", "config file path") .option("--server ", "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 { 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 { 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 { 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 { 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 { 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, ): Promise { 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 { 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 { 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 { 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; items?: JsonSchemaType; }; export async function listTools(client: Client): Promise { 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, ): Record { const result: Record = {}; 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, ): Promise { try { const toolsResponse = await listTools(client); const tools = toolsResponse.tools as Tool[]; const tool = tools.find((t) => t.name === name); let convertedArgs: Record = 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; ``` ## /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; uri?: string; logLevel?: LogLevel; toolName?: string; toolArg?: Record; }; function createTransportOptions(target: string[]): 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."); } return { transportType: isUrl ? "sse" : "stdio", command: isUrl ? undefined : command, args: isUrl ? undefined : commandArgs, url: isUrl ? command : undefined, }; } async function callMethod(args: Args): Promise { const transportOptions = createTransportOptions(args.target); 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 = {}, ): Record { 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("", "Command and arguments or URL of the MCP server") // // Method selection // .option("--method ", "Method to invoke") // // Tool-related options // .option("--tool-name ", "Tool name (for tools/call method)") .option( "--tool-arg ", "Tool argument as key=value pair", parseKeyValuePair, {}, ) // // Resource-related options // .option("--uri ", "URI of the resource (for resources/read method)") // // Prompt-related options // .option( "--prompt-name ", "Name of the prompt (for prompts/get method)", ) .option( "--prompt-args ", "Prompt arguments as key=value pairs", parseKeyValuePair, {}, ) // // Logging options // .option( "--log-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; }, ); // Parse only the arguments before -- program.parse(preArgs); const options = program.opts() as Omit; 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 { 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 type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { findActualExecutable } from "spawn-rx"; export type TransportOptions = { transportType: "sse" | "stdio"; command?: string; args?: string[]; url?: string; }; function createSSETransport(options: TransportOptions): Transport { const baseUrl = new URL(options.url ?? ""); const sseUrl = new URL("/sse", baseUrl); return new SSEClientTransport(sseUrl); } function createStdioTransport(options: TransportOptions): Transport { let args: string[] = []; if (options.args !== undefined) { args = options.args; } const processEnv: Record = {}; for (const [key, value] of Object.entries(process.env)) { if (value !== undefined) { processEnv[key] = value; } } const defaultEnv = getDefaultEnvironment(); const env: Record = { ...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); } 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 { 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) => { return handler(request, response, { public: distPath, rewrites: [{ source: "/**", destination: "/index.html" }], }); }); const port = process.env.PORT || 6274; server.on("listening", () => { console.log( `🔍 MCP Inspector is up and running at http://127.0.0.1:${port} 🚀`, ); }); server.on("error", (err) => { if (err.message.includes(`EADDRINUSE`)) { console.error( `❌ MCP Inspector PORT IS IN USE at http://127.0.0.1:${port} ❌ `, ); } else { throw err; } }); server.listen(port); ``` ## /client/bin/start.js ```js path="/client/bin/start.js" #!/usr/bin/env node import { resolve, dirname } from "path"; import { spawnPromise } from "spawn-rx"; import { fileURLToPath } from "url"; const __dirname = dirname(fileURLToPath(import.meta.url)); function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms, true)); } async function main() { // Parse command line arguments const args = process.argv.slice(2); const envVars = {}; const mcpServerArgs = []; let command = null; let parsingFlags = true; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (parsingFlags && arg === "--") { parsingFlags = false; 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) { command = arg; } else { mcpServerArgs.push(arg); } } 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 = process.env.CLIENT_PORT ?? "6274"; const SERVER_PORT = process.env.SERVER_PORT ?? "6277"; console.log("Starting MCP inspector..."); const abort = new AbortController(); let cancelled = false; process.on("SIGINT", () => { cancelled = true; abort.abort(); }); let server, serverOk; try { server = spawnPromise( "node", [ inspectorServerPath, ...(command ? [`--env`, command] : []), ...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []), ], { env: { ...process.env, PORT: SERVER_PORT, MCP_ENV_VARS: JSON.stringify(envVars), }, 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; } } 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/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" MCP Inspector
``` ## /client/jest.config.cjs ```cjs path="/client/jest.config.cjs" module.exports = { preset: "ts-jest", testEnvironment: "jest-fixed-jsdom", moduleNameMapper: { "^@/(.*)$": "/src/$1", "\\.css$": "/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/", "\\.config\\.(js|ts|cjs|mjs)$", ], // Exclude the same patterns from coverage reports coveragePathIgnorePatterns: [ "/node_modules/", "/dist/", "/bin/", "\\.config\\.(js|ts|cjs|mjs)$", ], }; ``` ## /client/package.json ```json path="/client/package.json" { "name": "@modelcontextprotocol/inspector-client", "version": "0.10.2", "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" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.10.0", "@radix-ui/react-checkbox": "^1.1.4", "@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.447.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/postcss.config.js ```js path="/client/postcss.config.js" export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ``` ## /client/public/mcp.svg ```svg path="/client/public/mcp.svg" ``` ## /client/src/App.css ```css path="/client/src/App.css" #root { margin: 0 auto; } .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 React, { Suspense, useCallback, useEffect, useRef, useState, } from "react"; import { useConnection } from "./lib/hooks/useConnection"; import { useDraggablePane } from "./lib/hooks/useDraggablePane"; import { StdErrNotification } from "./lib/notificationTypes"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Bell, Files, FolderTree, Hammer, Hash, MessageSquare, } from "lucide-react"; import { z } from "zod"; import "./App.css"; import ConsoleTab from "./components/ConsoleTab"; import HistoryAndNotifications from "./components/History"; 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 { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants"; import { InspectorConfig } from "./lib/configurationTypes"; import { getMCPProxyAddress } from "./utils/configUtils"; const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1"; const App = () => { const [resources, setResources] = useState([]); const [resourceTemplates, setResourceTemplates] = useState< ResourceTemplate[] >([]); const [resourceContent, setResourceContent] = useState(""); const [prompts, setPrompts] = useState([]); const [promptContent, setPromptContent] = useState(""); const [tools, setTools] = useState([]); const [toolResult, setToolResult] = useState(null); const [errors, setErrors] = useState>({ resources: null, prompts: null, tools: null, }); const [command, setCommand] = useState(() => { return localStorage.getItem("lastCommand") || "mcp-server-everything"; }); const [args, setArgs] = useState(() => { return localStorage.getItem("lastArgs") || ""; }); const [sseUrl, setSseUrl] = useState(() => { return localStorage.getItem("lastSseUrl") || "http://localhost:3001/sse"; }); const [transportType, setTransportType] = useState< "stdio" | "sse" | "streamable-http" >(() => { return ( (localStorage.getItem("lastTransportType") as | "stdio" | "sse" | "streamable-http") || "stdio" ); }); const [logLevel, setLogLevel] = useState("debug"); const [notifications, setNotifications] = useState([]); const [stdErrNotifications, setStdErrNotifications] = useState< StdErrNotification[] >([]); const [roots, setRoots] = useState([]); const [env, setEnv] = useState>({}); const [config, setConfig] = useState(() => { const savedConfig = localStorage.getItem(CONFIG_LOCAL_STORAGE_KEY); if (savedConfig) { // merge default config with saved config const mergedConfig = { ...DEFAULT_INSPECTOR_CONFIG, ...JSON.parse(savedConfig), } as InspectorConfig; // update description of keys to match the new description (in case of any updates to the default config description) Object.entries(mergedConfig).forEach(([key, value]) => { mergedConfig[key as keyof InspectorConfig] = { ...value, label: DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].label, }; }); return mergedConfig; } return DEFAULT_INSPECTOR_CONFIG; }); const [bearerToken, setBearerToken] = useState(() => { return localStorage.getItem("lastBearerToken") || ""; }); const [headerName, setHeaderName] = useState(() => { return localStorage.getItem("lastHeaderName") || ""; }); const [pendingSampleRequests, setPendingSampleRequests] = useState< Array< PendingRequest & { resolve: (result: CreateMessageResult) => void; reject: (error: Error) => void; } > >([]); const nextRequestId = useRef(0); const rootsRef = useRef([]); const [selectedResource, setSelectedResource] = useState( null, ); const [resourceSubscriptions, setResourceSubscriptions] = useState< Set >(new Set()); const [selectedPrompt, setSelectedPrompt] = useState(null); const [selectedTool, setSelectedTool] = useState(null); const [nextResourceCursor, setNextResourceCursor] = useState< string | undefined >(); const [nextResourceTemplateCursor, setNextResourceTemplateCursor] = useState< string | undefined >(); const [nextPromptCursor, setNextPromptCursor] = useState< string | undefined >(); const [nextToolCursor, setNextToolCursor] = useState(); const progressTokenRef = useRef(0); const { height: historyPaneHeight, handleDragStart } = useDraggablePane(300); 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(() => { localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config)); }, [config]); // Auto-connect to previously saved serverURL after OAuth callback const onOAuthConnect = useCallback( (serverUrl: string) => { setSseUrl(serverUrl); setTransportType("sse"); void connectMcpServer(); }, [connectMcpServer], ); useEffect(() => { fetch(`${getMCPProxyAddress(config)}/config`) .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), ); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); 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 ( 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 = {}) => { 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); }; const callTool = async (name: string, params: Record) => { 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([]); }; if (window.location.pathname === "/oauth/callback") { const OAuthCallback = React.lazy( () => import("./components/OAuthCallback"), ); return ( Loading...}> ); } return (
{mcpClient ? ( (window.location.hash = value)} > Resources Prompts Tools Ping Sampling {pendingSampleRequests.length > 0 && ( {pendingSampleRequests.length} )} Roots
{!serverCapabilities?.resources && !serverCapabilities?.prompts && !serverCapabilities?.tools ? (

The connected server does not support any MCP capabilities

) : ( <> { 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} /> { 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} /> { clearError("tools"); listTools(); }} clearTools={() => { setTools([]); setNextToolCursor(undefined); }} 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} /> { void sendMCPRequest( { method: "ping" as const, }, EmptyResultSchema, ); }} /> )}
) : (

Connect to an MCP server to start inspecting

)}
); }; export default App; ``` ## /client/src/__mocks__/styleMock.js ```js path="/client/src/__mocks__/styleMock.js" module.exports = {}; ``` ## /client/src/components/ConsoleTab.tsx ```tsx path="/client/src/components/ConsoleTab.tsx" import { TabsContent } from "@/components/ui/tabs"; const ConsoleTab = () => (
Welcome to MCP Client Console
{/* Console output would go here */}
); 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"; 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 false; return Object.values(schema.properties ?? {}).every((prop) => supportedTypes.includes(prop.type), ); }; const DynamicJsonForm = ({ schema, value, onChange, maxDepth = 3, }: DynamicJsonFormProps) => { const isOnlyJSON = !isSimpleObject(schema); const [isJsonMode, setIsJsonMode] = useState(isOnlyJSON); const [jsonError, setJsonError] = useState(); // Store the raw JSON string to allow immediate feedback during typing // while deferring parsing until the user stops typing const [rawJsonValue, setRawJsonValue] = useState( 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>(); // 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, ) => { if ( depth >= maxDepth && (propSchema.type === "object" || propSchema.type === "array") ) { // Render as JSON editor when max depth is reached return ( { try { const parsed = JSON.parse(newValue); handleFieldChange(path, parsed); setJsonError(undefined); } catch (err) { setJsonError(err instanceof Error ? err.message : "Invalid JSON"); } }} error={jsonError} /> ); } switch (propSchema.type) { case "string": return ( { const val = e.target.value; // Allow clearing non-required fields by setting undefined // This preserves the distinction between empty string and unset if (!val && !propSchema.required) { handleFieldChange(path, undefined); } else { handleFieldChange(path, val); } }} placeholder={propSchema.description} required={propSchema.required} /> ); case "number": return ( { const val = e.target.value; // Allow clearing non-required number fields // This preserves the distinction between 0 and unset if (!val && !propSchema.required) { handleFieldChange(path, undefined); } else { const num = Number(val); if (!isNaN(num)) { handleFieldChange(path, num); } } }} placeholder={propSchema.description} required={propSchema.required} /> ); case "integer": return ( { const val = e.target.value; // Allow clearing non-required integer fields // This preserves the distinction between 0 and unset if (!val && !propSchema.required) { 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={propSchema.required} /> ); case "boolean": return ( handleFieldChange(path, e.target.checked)} className="w-4 h-4" required={propSchema.required} /> ); 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]); return (
{isJsonMode && ( )} {!isOnlyJSON && ( )}
{isJsonMode ? ( { // 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 !== "{}" ? (

Form view not available for this JSON structure. Using simplified view:

            {rawJsonValue}
          

Use JSON mode for full editing capabilities.

) : ( renderFormFields(schema, value) )}
); }; export default DynamicJsonForm; ``` ## /client/src/components/History.tsx ```tsx path="/client/src/components/History.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 (

History

{requestHistory.length === 0 ? (

No history yet

) : (
    {requestHistory .slice() .reverse() .map((request, index) => (
  • toggleRequestExpansion(requestHistory.length - 1 - index) } > {requestHistory.length - index}.{" "} {JSON.parse(request.request).method} {expandedRequests[requestHistory.length - 1 - index] ? "▼" : "▶"}
    {expandedRequests[requestHistory.length - 1 - index] && ( <>
    Request:
    {request.response && (
    Response:
    )} )}
  • ))}
)}

Server Notifications

{serverNotifications.length === 0 ? (

No notifications yet

) : (
    {serverNotifications .slice() .reverse() .map((notification, index) => (
  • toggleNotificationExpansion(index)} > {serverNotifications.length - index}.{" "} {notification.method} {expandedNotifications[index] ? "▼" : "▶"}
    {expandedNotifications[index] && (
    Details:
    )}
  • ))}
)}
); }; 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( undefined, ); useEffect(() => { setEditorContent(value || ""); }, [value]); const handleEditorChange = (newContent: string) => { setEditorContent(newContent); setInternalError(undefined); onChange(newContent); }; const displayError = internalError || externalError; return (
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" />
{displayError && (

{displayError}

)}
); }; 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 "@/hooks/use-toast"; 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 (
{withCopyButton && ( )}
); }, ); 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>({ 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); const itemCount = items.length; const isEmpty = itemCount === 0; const symbolMap = { open: isArray ? "[" : "{", close: isArray ? "]" : "}", collapsed: isArray ? "[ ... ]" : "{ ... }", empty: isArray ? "[]" : "{}", }; if (isEmpty) { return (
{name && ( {name}: )} {symbolMap.empty}
); } return (
setIsExpanded(!isExpanded)} > {name && ( {name}: )} {isExpanded ? ( {symbolMap.open} ) : ( <> {symbolMap.collapsed} {itemCount} {itemCount === 1 ? "item" : "items"} )}
{isExpanded && ( <>
{isArray ? (items as JsonValue[]).map((item, index) => (
)) : (items as [string, JsonValue][]).map(([key, value]) => (
))}
{symbolMap.close}
)}
); }; const renderString = (value: string) => { const maxLength = 100; const isTooLong = value.length > maxLength; if (!isTooLong) { return (
{name && ( {name}: )}
              "{value}"
            
); } return (
{name && ( {name}: )}
 setIsExpanded(!isExpanded)}
            title={isExpanded ? "Click to collapse" : "Click to expand"}
          >
            {isExpanded ? `"${value}"` : `"${value.slice(0, maxLength)}..."`}
          
); }; switch (dataType) { case "object": case "array": return renderCollapsible(dataType === "array"); case "string": return renderString(data as string); default: return (
{name && ( {name}: )} {data === null ? "null" : String(data)}
); } }, ); 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 = { items: T[]; listItems: () => void; clearItems: () => void; setSelectedItem: (item: T) => void; renderItem: (item: T) => React.ReactNode; title: string; buttonText: string; isButtonDisabled?: boolean; }; const ListPane = ({ items, listItems, clearItems, setSelectedItem, renderItem, title, buttonText, isButtonDisabled, }: ListPaneProps) => (

{title}

{items.map((item, index) => (
setSelectedItem(item)} > {renderItem(item)}
))}
); 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 "@/hooks/use-toast.ts"; 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 (

Processing OAuth callback...

); }; export default OAuthCallback; ``` ## /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 (
); }; 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) => void; selectedPrompt: Prompt | null; setSelectedPrompt: (prompt: Prompt | null) => void; handleCompletion: ( ref: PromptReference | ResourceReference, argName: string, value: string, ) => Promise; completionsSupported: boolean; promptContent: string; nextCursor: ListPromptsResult["nextCursor"]; error: string | null; }) => { const [promptArgs, setPromptArgs] = useState>({}); 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 (
{ clearPrompts(); setSelectedPrompt(null); }} setSelectedItem={(prompt) => { setSelectedPrompt(prompt); setPromptArgs({}); }} renderItem={(prompt) => ( <> {prompt.name} {prompt.description} )} title="Prompts" buttonText={nextCursor ? "List More Prompts" : "List Prompts"} isButtonDisabled={!nextCursor && prompts.length > 0} />

{selectedPrompt ? selectedPrompt.name : "Select a prompt"}

{error ? ( Error {error} ) : selectedPrompt ? (
{selectedPrompt.description && (

{selectedPrompt.description}

)} {selectedPrompt.arguments?.map((arg) => (
handleInputChange(arg.name, value)} onInputChange={(value) => handleInputChange(arg.name, value) } options={completions[arg.name] || []} /> {arg.description && (

{arg.description} {arg.required && ( (Required) )}

)}
))} {promptContent && ( )}
) : ( Select a prompt from the list to view and use it )}
); }; 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"; 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; completionsSupported: boolean; resourceContent: string; nextCursor: ListResourcesResult["nextCursor"]; nextTemplateCursor: ListResourceTemplatesResult["nextCursor"]; error: string | null; resourceSubscriptionsSupported: boolean; resourceSubscriptions: Set; subscribeToResource: (uri: string) => void; unsubscribeFromResource: (uri: string) => void; }) => { const [selectedTemplate, setSelectedTemplate] = useState(null); const [templateValues, setTemplateValues] = useState>( {}, ); const { completions, clearCompletions, requestCompletions } = useCompletionState(handleCompletion, completionsSupported); useEffect(() => { clearCompletions(); }, [clearCompletions]); const fillTemplate = ( template: string, values: Record, ): string => { return template.replace( /{([^}]+)}/g, (_, key) => values[key] || `{${key}}`, ); }; 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 (
{ 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) => (
{resource.name}
)} title="Resources" buttonText={nextCursor ? "List More Resources" : "List Resources"} isButtonDisabled={!nextCursor && resources.length > 0} /> { 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) => (
{template.name}
)} title="Resource Templates" buttonText={ nextTemplateCursor ? "List More Templates" : "List Templates" } isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0} />

{selectedResource ? selectedResource.name : selectedTemplate ? selectedTemplate.name : "Select a resource or template"}

{selectedResource && (
{resourceSubscriptionsSupported && !resourceSubscriptions.has(selectedResource.uri) && ( )} {resourceSubscriptionsSupported && resourceSubscriptions.has(selectedResource.uri) && ( )}
)}
{error ? ( Error {error} ) : selectedResource ? ( ) : selectedTemplate ? (

{selectedTemplate.description}

{selectedTemplate.uriTemplate .match(/{([^}]+)}/g) ?.map((param) => { const key = param.slice(1, -1); return (
handleTemplateValueChange(key, value) } onInputChange={(value) => handleTemplateValueChange(key, value) } options={completions[key] || []} />
); })}
) : ( Select a resource or template from the list to view its contents )}
); }; 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>; 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 (
Configure the root directories that the server can access {roots.map((root, index) => (
updateRoot(index, "uri", e.target.value)} className="flex-1" />
))}
); }; 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 "@/hooks/use-toast"; 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({ 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 (
{ setMessageResult(newValue); }} />
); }; 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 (
When the server requests LLM sampling, requests will appear here for approval.

Recent Requests

{pendingRequests.map((request) => ( ))} {pendingRequests.length === 0 && (

No pending requests

)}
); }; export default SamplingTab; ``` ## /client/src/components/Sidebar.tsx ```tsx path="/client/src/components/Sidebar.tsx" import { useState } from "react"; import { Play, ChevronDown, ChevronRight, CircleHelp, Bug, Github, Eye, EyeOff, RotateCcw, Settings, HelpCircle, RefreshCwOff, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { StdErrNotification } from "@/lib/notificationTypes"; import { LoggingLevel, LoggingLevelSchema, } from "@modelcontextprotocol/sdk/types.js"; import { InspectorConfig } from "@/lib/configurationTypes"; import { ConnectionStatus } from "@/lib/constants"; import useTheme from "../lib/useTheme"; import { version } from "../../../package.json"; import { Tooltip, TooltipTrigger, TooltipContent, } from "@/components/ui/tooltip"; interface SidebarProps { connectionStatus: ConnectionStatus; transportType: "stdio" | "sse" | "streamable-http"; setTransportType: (type: "stdio" | "sse" | "streamable-http") => void; command: string; setCommand: (command: string) => void; args: string; setArgs: (args: string) => void; sseUrl: string; setSseUrl: (url: string) => void; env: Record; setEnv: (env: Record) => void; bearerToken: string; setBearerToken: (token: string) => void; headerName?: string; setHeaderName?: (name: string) => void; onConnect: () => void; onDisconnect: () => void; stdErrNotifications: StdErrNotification[]; clearStdErrNotifications: () => void; logLevel: LoggingLevel; sendLogLevelRequest: (level: LoggingLevel) => void; loggingSupported: boolean; config: InspectorConfig; setConfig: (config: InspectorConfig) => void; } const Sidebar = ({ connectionStatus, transportType, setTransportType, command, setCommand, args, setArgs, sseUrl, setSseUrl, env, setEnv, bearerToken, setBearerToken, headerName, setHeaderName, onConnect, onDisconnect, stdErrNotifications, clearStdErrNotifications, logLevel, sendLogLevelRequest, loggingSupported, config, setConfig, }: SidebarProps) => { const [theme, setTheme] = useTheme(); const [showEnvVars, setShowEnvVars] = useState(false); const [showBearerToken, setShowBearerToken] = useState(false); const [showConfig, setShowConfig] = useState(false); const [shownEnvVars, setShownEnvVars] = useState>(new Set()); return (

MCP Inspector v{version}

{transportType === "stdio" ? ( <>
setCommand(e.target.value)} className="font-mono" />
setArgs(e.target.value)} className="font-mono" />
) : ( <>
setSseUrl(e.target.value)} className="font-mono" />
{showBearerToken && (
setHeaderName && setHeaderName(e.target.value) } data-testid="header-input" className="font-mono" value={headerName} /> setBearerToken(e.target.value)} data-testid="bearer-token-input" className="font-mono" type="password" />
)}
)} {transportType === "stdio" && (
{showEnvVars && (
{Object.entries(env).map(([key, value], idx) => (
{ const newKey = e.target.value; const newEnv = Object.entries(env).reduce( (acc, [k, v]) => { if (k === key) { acc[newKey] = value; } else { acc[k] = v; } return acc; }, {} as Record, ); setEnv(newEnv); setShownEnvVars((prev) => { const next = new Set(prev); if (next.has(key)) { next.delete(key); next.add(newKey); } return next; }); }} className="font-mono" />
{ const newEnv = { ...env }; newEnv[key] = e.target.value; setEnv(newEnv); }} className="font-mono" />
))}
)}
)} {/* Configuration */}
{showConfig && (
{Object.entries(config).map(([key, configItem]) => { const configKey = key as keyof InspectorConfig; return (
{configItem.description}
{typeof configItem.value === "number" ? ( { const newConfig = { ...config }; newConfig[configKey] = { ...configItem, value: Number(e.target.value), }; setConfig(newConfig); }} className="font-mono" /> ) : typeof configItem.value === "boolean" ? ( ) : ( { const newConfig = { ...config }; newConfig[configKey] = { ...configItem, value: e.target.value, }; setConfig(newConfig); }} className="font-mono" /> )}
); })}
)}
{connectionStatus === "connected" && (
)} {connectionStatus !== "connected" && ( )}
{ switch (connectionStatus) { case "connected": return "bg-green-500"; case "error": return "bg-red-500"; case "error-connecting-to-proxy": return "bg-red-500"; default: return "bg-gray-500"; } })()}`} /> {(() => { switch (connectionStatus) { case "connected": return "Connected"; case "error": return "Connection Error, is your MCP server running?"; case "error-connecting-to-proxy": return "Error Connecting to MCP Inspector Proxy - Check Console logs"; default: return "Disconnected"; } })()}
{loggingSupported && connectionStatus === "connected" && (
)} {stdErrNotifications.length > 0 && ( <>

Error output from MCP server

{stdErrNotifications.map((notification, index) => (
{notification.params.content}
))}
)}
); }; export default Sidebar; ``` ## /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 } from "@/utils/schemaUtils"; import { CallToolResultSchema, CompatibilityCallToolResult, ListToolsResult, Tool, } from "@modelcontextprotocol/sdk/types.js"; import { Loader2, Send } from "lucide-react"; import { useEffect, useState } from "react"; import ListPane from "./ListPane"; import JsonView from "./JsonView"; const ToolsTab = ({ tools, listTools, clearTools, callTool, selectedTool, setSelectedTool, toolResult, nextCursor, }: { tools: Tool[]; listTools: () => void; clearTools: () => void; callTool: (name: string, params: Record) => Promise; selectedTool: Tool | null; setSelectedTool: (tool: Tool | null) => void; toolResult: CompatibilityCallToolResult | null; nextCursor: ListToolsResult["nextCursor"]; error: string | null; }) => { const [params, setParams] = useState>({}); const [isToolRunning, setIsToolRunning] = useState(false); useEffect(() => { const params = Object.entries( selectedTool?.inputSchema.properties ?? [], ).map(([key, value]) => [ key, generateDefaultValue(value as JsonSchemaType), ]); setParams(Object.fromEntries(params)); }, [selectedTool]); const renderToolResult = () => { if (!toolResult) return null; if ("content" in toolResult) { const parsedResult = CallToolResultSchema.safeParse(toolResult); if (!parsedResult.success) { return ( <>

Invalid Tool Result:

Errors:

{parsedResult.error.errors.map((error, idx) => ( ))} ); } const structuredResult = parsedResult.data; const isError = structuredResult.isError ?? false; return ( <>

Tool Result:{" "} {isError ? ( Error ) : ( Success )}

{structuredResult.content.map((item, index) => (
{item.type === "text" && ( )} {item.type === "image" && ( Tool result image )} {item.type === "resource" && (item.resource?.mimeType?.startsWith("audio/") ? ( ) : ( ))}
))} ); } else if ("toolResult" in toolResult) { return ( <>

Tool Result (Legacy):

); } }; return (
{ clearTools(); setSelectedTool(null); }} setSelectedItem={setSelectedTool} renderItem={(tool) => ( <> {tool.name} {tool.description} )} title="Tools" buttonText={nextCursor ? "List More Tools" : "List Tools"} isButtonDisabled={!nextCursor && tools.length > 0} />

{selectedTool ? selectedTool.name : "Select a tool"}

{selectedTool ? (

{selectedTool.description}

{Object.entries(selectedTool.inputSchema.properties ?? []).map( ([key, value]) => { const prop = value as JsonSchemaType; return (
{prop.type === "boolean" ? (
setParams({ ...params, [key]: checked, }) } />
) : prop.type === "string" ? (