diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index a3e960e3..00000000 --- a/.dockerignore +++ /dev/null @@ -1,35 +0,0 @@ -# Version control -.git -.gitignore - -# Node.js -node_modules -npm-debug.log - -# Build artifacts -client/dist -client/build -server/dist -server/build - -# Environment variables -.env -.env.local -.env.development -.env.test -.env.production - -# Editor files -.vscode -.idea - -# Logs -logs -*.log - -# Testing -coverage - -# Docker -Dockerfile -.dockerignore \ No newline at end of file diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs deleted file mode 100644 index e69de29b..00000000 diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index cf968721..00000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -package-lock.json linguist-generated=true diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 31eb7a22..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: "" -labels: "" -assignees: "" ---- - -**Inspector Version** - -- [e.g. 0.16.5) - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: - -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Environment (please complete the following information):** - -- OS: [e.g. iOS] -- Browser [e.g. chrome, safari] - -**Additional context** -Add any other context about the problem here. - -**Version Consideration** - -Inspector V2 is under development to address architectural and UX improvements. See [CONTRIBUTING.md](CONTRIBUTING.md) for information about V2 development. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 50189d36..00000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,57 +0,0 @@ -## Summary - - - -> **Note:** Inspector V2 is under development to address architectural and UX improvements. See [CONTRIBUTING.md](CONTRIBUTING.md) for information about V2 development. - -## Type of Change - - - -- [ ] Bug fix (non-breaking change that fixes an issue) -- [ ] New feature (non-breaking change that adds functionality) -- [ ] Documentation update -- [ ] Refactoring (no functional changes) -- [ ] Test updates -- [ ] Build/CI improvements - -## Changes Made - - - -## Related Issues - - - -## Testing - - - -- [ ] Tested in UI mode -- [ ] Tested in CLI mode -- [ ] Tested with STDIO transport -- [ ] Tested with SSE transport -- [ ] Tested with Streamable HTTP transport -- [ ] Added/updated automated tests -- [ ] Manual testing performed - -### Test Results and/or Instructions - - - -Screenshots are encouraged to share your testing results for this change. - -## Checklist - -- [ ] Code follows the style guidelines (ran `npm run prettier-fix`) -- [ ] Self-review completed -- [ ] Code is commented where necessary -- [ ] Documentation updated (README, comments, etc.) - -## Breaking Changes - - - -## Additional Context - - diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml deleted file mode 100644 index 2910e4f3..00000000 --- a/.github/workflows/claude.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Claude Code - -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - issues: - types: [opened, assigned] - pull_request_review: - types: [submitted] - -jobs: - claude: - if: | - ( - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) - ) - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - actions: read - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude Code - id: claude - uses: anthropics/claude-code-action@v1 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - - # Allow Claude to read CI results on PRs - additional_permissions: | - actions: read - - # Trigger when assigned to an issue - assignee_trigger: "claude" - - claude_args: | - --allowedTools Bash - --system-prompt "If posting a comment to GitHub, give a concise summary of the comment at the top and put all the details in a
block." diff --git a/.github/workflows/cli_tests.yml b/.github/workflows/cli_tests.yml deleted file mode 100644 index 8bd3bb8e..00000000 --- a/.github/workflows/cli_tests.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: CLI Tests - -on: - push: - paths: - - "cli/**" - pull_request: - paths: - - "cli/**" - -jobs: - test: - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./cli - steps: - - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version-file: package.json - cache: npm - - - name: Install dependencies - run: | - cd .. - npm ci --ignore-scripts - - - name: Build CLI - run: npm run build - - - name: Explicitly pre-install test dependencies - run: npx -y @modelcontextprotocol/server-everything --help || true - - - name: Run tests - run: npm test - env: - NPM_CONFIG_YES: true - CI: true diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml deleted file mode 100644 index 378905b4..00000000 --- a/.github/workflows/e2e_tests.yml +++ /dev/null @@ -1,78 +0,0 @@ -name: Playwright Tests - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - test: - # Installing Playwright dependencies can take quite awhile, and also depends on GitHub CI load. - timeout-minutes: 15 - runs-on: ubuntu-latest - - steps: - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y libwoff1 - - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - id: setup_node - with: - node-version-file: package.json - cache: npm - - # Cache Playwright browsers - - name: Cache Playwright browsers - id: cache-playwright - uses: actions/cache@v4 - with: - path: ~/.cache/ms-playwright # The default Playwright cache path - key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }} # Cache key based on OS and package-lock.json - restore-keys: | - ${{ runner.os }}-playwright- - - - name: Install dependencies - run: npm ci - - - name: Install Playwright dependencies - run: npx playwright install-deps - - - name: Install Playwright and browsers unless cached - run: npx playwright install --with-deps - if: steps.cache-playwright.outputs.cache-hit != 'true' - - - name: Run Playwright tests - id: playwright-tests - run: npm run test:e2e - - - name: Upload Playwright Report and Screenshots - uses: actions/upload-artifact@v4 - if: steps.playwright-tests.conclusion != 'skipped' - with: - name: playwright-report - path: | - client/playwright-report/ - client/test-results/ - client/results.json - retention-days: 2 - - - name: Publish Playwright Test Summary - uses: daun/playwright-report-summary@v3 - if: steps.playwright-tests.conclusion != 'skipped' - with: - create-comment: ${{ github.event.pull_request.head.repo.full_name == github.repository }} - report-file: client/results.json - comment-title: "🎭 Playwright E2E Test Results" - job-summary: true - icon-style: "emojis" - custom-info: | - **Test Environment:** Ubuntu Latest, Node.js ${{ steps.setup_node.outputs.node-version }} - **Browsers:** Chromium, Firefox - - πŸ“Š [View Detailed HTML Report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) (download artifacts) - test-command: "npm run test:e2e" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index d01f7175..00000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,116 +0,0 @@ -on: - push: - branches: - - main - - pull_request: - release: - types: [published] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Check formatting - run: npx prettier --check . - - - uses: actions/setup-node@v4 - with: - node-version-file: package.json - cache: npm - - # Working around https://github.com/npm/cli/issues/4828 - # - run: npm ci - - run: npm install --no-package-lock - - - name: Check version consistency - run: npm run check-version - - - name: Check linting - working-directory: ./client - run: npm run lint - - - name: Run client tests - working-directory: ./client - run: npm test - - - run: npm run build - - publish: - runs-on: ubuntu-latest - if: github.event_name == 'release' - environment: release - needs: build - - permissions: - contents: read - id-token: write - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version-file: package.json - cache: npm - registry-url: "https://registry.npmjs.org" - - # Working around https://github.com/npm/cli/issues/4828 - # - run: npm ci - - run: npm install --no-package-lock - - # TODO: Add --provenance once the repo is public - - run: npm run publish-all - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - publish-github-container-registry: - runs-on: ubuntu-latest - if: github.event_name == 'release' - environment: release - needs: build - permissions: - contents: read - packages: write - attestations: write - id-token: write - steps: - - uses: actions/checkout@v4 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: ghcr.io/${{ github.repository }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and push Docker image - id: push - uses: docker/build-push-action@v6 - with: - context: . - push: true - platforms: linux/amd64,linux/arm64 - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - - name: Generate artifact attestation - uses: actions/attest-build-provenance@v2 - with: - subject-name: ghcr.io/${{ github.repository }} - subject-digest: ${{ steps.push.outputs.digest }} - push-to-registry: true diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 230d72d4..00000000 --- a/.gitignore +++ /dev/null @@ -1,21 +0,0 @@ -.DS_Store -.vscode -.idea -node_modules/ -*-workspace/ -server/build -client/dist -client/tsconfig.app.tsbuildinfo -client/tsconfig.node.tsbuildinfo -cli/build -test-output -tool-test-output -metadata-test-output -# symlinked by `npm run link:sdk`: -sdk -client/playwright-report/ -client/results.json -client/test-results/ -client/e2e/test-results/ -mcp.json -.claude/settings.local.json diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100644 index 0a3afdcb..00000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,2 +0,0 @@ -npx lint-staged -git update-index --again diff --git a/.node-version b/.node-version deleted file mode 100644 index 980f29c2..00000000 --- a/.node-version +++ /dev/null @@ -1 +0,0 @@ -22.x.x diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 1a3d6209..00000000 --- a/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -registry="https://registry.npmjs.org/" -@modelcontextprotocol:registry="https://registry.npmjs.org/" diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 167a7cb4..00000000 --- a/.prettierignore +++ /dev/null @@ -1,6 +0,0 @@ -packages -server/build -CODE_OF_CONDUCT.md -SECURITY.md -mcp.json -.claude/settings.local.json \ No newline at end of file diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index e69de29b..00000000 diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index b0e46712..00000000 --- a/AGENTS.md +++ /dev/null @@ -1,35 +0,0 @@ -# MCP Inspector Development Guide - -> **Note:** Inspector V2 is under development to address architectural and UX improvements. These guidelines apply to the current version (V1). See [CONTRIBUTING.md](CONTRIBUTING.md) for information about V2 development. - -## Build Commands - -- Build all: `npm run build` -- Build client: `npm run build-client` -- Build server: `npm run build-server` -- Development mode: `npm run dev` (use `npm run dev:windows` on Windows) -- Format code: `npm run prettier-fix` -- Client lint: `cd client && npm run lint` - -## Code Style Guidelines - -- Use TypeScript with proper type annotations -- Follow React functional component patterns with hooks -- Use ES modules (import/export) not CommonJS -- Use Prettier for formatting (auto-formatted on commit) -- Follow existing naming conventions: - - camelCase for variables and functions - - PascalCase for component names and types - - kebab-case for file names -- Use async/await for asynchronous operations -- Implement proper error handling with try/catch blocks -- Use Tailwind CSS for styling in the client -- Keep components small and focused on a single responsibility - -## Project Organization - -The project is organized as a monorepo with workspaces: - -- `client/`: React frontend with Vite, TypeScript and Tailwind -- `server/`: Express backend with TypeScript -- `cli/`: Command-line interface for testing and invoking MCP server methods directly diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 285e0f5b..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -@./AGENTS.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 05c32c60..00000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,128 +0,0 @@ -# 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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 9f98adcf..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,48 +0,0 @@ -# Contributing to Model Context Protocol Inspector - -Thanks for your interest in contributing! This guide explains how to get involved. - -## Getting Started - -1. Fork the repository and clone it locally -2. Install dependencies with `npm install` -3. Run `npm run dev` to start both client and server in development mode -4. Use the web UI at http://localhost:6274 to interact with the inspector - -## Inspector V2 Development - -We're actively developing **Inspector V2** to address architectural and UX improvements. We invite you to follow progress and participate in the Inspector V2 Working Group in [Discord](https://modelcontextprotocol.io/community/communication), [weekly meetings](https://meet.modelcontextprotocol.io/tag/inspector-v2-wg), and [GitHub Discussions](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/categories/meeting-notes-other) (where notes are posted after meetings). - -**Current version (V1) contribution scope:** - -- Critical issues are actively maintained -- Small UX improvements are welcome -- Documentation updates are always appreciated -- Major changes will be directed to V2 development - -## Development Process & Pull Requests - -1. Create a new branch for your changes -2. Make your changes following existing code style and conventions. You can run `npm run prettier-check` and `npm run prettier-fix` as applicable. -3. Test changes locally by running `npm test` and `npm run test:e2e` -4. Update documentation as needed -5. Use clear commit messages explaining your changes -6. Verify all changes work as expected -7. Submit a pull request -8. PRs will be reviewed by maintainers - -## Code of Conduct - -This project follows our [Code of Conduct](CODE_OF_CONDUCT.md). Please read it before contributing. - -## Security - -If you find a security vulnerability, please refer to our [Security Policy](SECURITY.md) for reporting instructions. - -## Questions? - -Feel free to [open an issue](https://github.com/modelcontextprotocol/mcp-inspector/issues) for questions or join the MCP Contributor [Discord server](https://modelcontextprotocol.io/community/communication). Also, please see notes above on Inspector V2 Development. - -## License - -By contributing, you agree that your contributions will be licensed under the MIT license. diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index d66091d1..00000000 --- a/Dockerfile +++ /dev/null @@ -1,52 +0,0 @@ -# Build stage -FROM node:current-alpine3.22 AS builder - -# Set working directory -WORKDIR /app - -# Copy package files for installation -COPY package*.json ./ -COPY .npmrc ./ -COPY client/package*.json ./client/ -COPY server/package*.json ./server/ -COPY cli/package*.json ./cli/ - -# Install dependencies -RUN npm ci --ignore-scripts - -# Copy source files -COPY . . - -# Build the application -RUN npm run build - -# Production stage -FROM node:24-slim - -WORKDIR /app - -# Copy package files for production -COPY package*.json ./ -COPY .npmrc ./ -COPY client/package*.json ./client/ -COPY server/package*.json ./server/ -COPY cli/package*.json ./cli/ - -# Install only production dependencies -RUN npm ci --omit=dev --ignore-scripts - -# Copy built files from builder stage -COPY --from=builder /app/client/dist ./client/dist -COPY --from=builder /app/client/bin ./client/bin -COPY --from=builder /app/server/build ./server/build -COPY --from=builder /app/cli/build ./cli/build - -# Set default port values as environment variables -ENV CLIENT_PORT=6274 -ENV SERVER_PORT=6277 - -# Document which ports the application uses internally -EXPOSE ${CLIENT_PORT} ${SERVER_PORT} - -# Use ENTRYPOINT with CMD for arguments -ENTRYPOINT ["npm", "start"] diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 3d484354..00000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -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. diff --git a/README.md b/README.md deleted file mode 100644 index 4f07df86..00000000 --- a/README.md +++ /dev/null @@ -1,474 +0,0 @@ -# MCP Inspector - -The MCP inspector is a developer tool for testing and debugging MCP servers. - -![MCP Inspector Screenshot](https://raw.githubusercontent.com/modelcontextprotocol/inspector/main/mcp-inspector.png) - -## Architecture Overview - -The MCP Inspector consists of two main components that work together: - -- **MCP Inspector Client (MCPI)**: A React-based web UI that provides an interactive interface for testing and debugging MCP servers -- **MCP Proxy (MCPP)**: A Node.js server that acts as a protocol bridge, connecting the web UI to MCP servers via various transport methods (stdio, SSE, streamable-http) - -Note that the proxy is not a network proxy for intercepting traffic. Instead, it functions as both an MCP client (connecting to your MCP server) and an HTTP server (serving the web UI), enabling browser-based interaction with MCP servers that use different transport protocols. - -## Running the Inspector - -### Requirements - -- Node.js: ^22.7.5 - -### Quick Start (UI mode) - -To get up and running right away with the UI, just execute the following: - -```bash -npx @modelcontextprotocol/inspector -``` - -The server will start up and the UI will be accessible at `http://localhost:6274`. - -### Docker Container - -You can also start it in a Docker container with the following command: - -```bash -docker run --rm --network host -p 6274:6274 -p 6277:6277 ghcr.io/modelcontextprotocol/inspector:latest -``` - -### From an MCP server repository - -To inspect an MCP server implementation, there's no need to clone this repo. Instead, use `npx`. For example, if your server is built at `build/index.js`: - -```bash -npx @modelcontextprotocol/inspector node build/index.js -``` - -You can pass both arguments and environment variables to your MCP server. Arguments are passed directly to your server, while environment variables can be set using the `-e` flag: - -```bash -# Pass arguments only -npx @modelcontextprotocol/inspector node build/index.js arg1 arg2 - -# Pass environment variables only -npx @modelcontextprotocol/inspector -e key=value -e key2=$VALUE2 node build/index.js - -# Pass both environment variables and arguments -npx @modelcontextprotocol/inspector -e key=value -e key2=$VALUE2 node build/index.js arg1 arg2 - -# Use -- to separate inspector flags from server arguments -npx @modelcontextprotocol/inspector -e key=$VALUE -- node build/index.js -e server-flag -``` - -The inspector runs both an MCP Inspector (MCPI) client UI (default port 6274) and an MCP Proxy (MCPP) server (default port 6277). Open the MCPI client UI in your browser to use the inspector. (These ports are derived from the T9 dialpad mapping of MCPI and MCPP respectively, as a mnemonic). You can customize the ports if needed: - -```bash -CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector node build/index.js -``` - -For more details on ways to use the inspector, see the [Inspector section of the MCP docs site](https://modelcontextprotocol.io/docs/tools/inspector). For help with debugging, see the [Debugging guide](https://modelcontextprotocol.io/docs/tools/debugging). - -### Servers File Export - -The MCP Inspector provides convenient buttons to export server launch configurations for use in clients such as Cursor, Claude Code, or the Inspector's CLI. The file is usually called `mcp.json`. - -- **Server Entry** - Copies a single server configuration entry to your clipboard. This can be added to your `mcp.json` file inside the `mcpServers` object with your preferred server name. - - **STDIO transport example:** - - ```json - { - "command": "node", - "args": ["build/index.js", "--debug"], - "env": { - "API_KEY": "your-api-key", - "DEBUG": "true" - } - } - ``` - - **SSE transport example:** - - ```json - { - "type": "sse", - "url": "http://localhost:3000/events", - "note": "For SSE connections, add this URL directly in Client" - } - ``` - - **Streamable HTTP transport example:** - - ```json - { - "type": "streamable-http", - "url": "http://localhost:3000/mcp", - "note": "For Streamable HTTP connections, add this URL directly in your MCP Client" - } - ``` - -- **Servers File** - Copies a complete MCP configuration file structure to your clipboard, with your current server configuration added as `default-server`. This can be saved directly as `mcp.json`. - - **STDIO transport example:** - - ```json - { - "mcpServers": { - "default-server": { - "command": "node", - "args": ["build/index.js", "--debug"], - "env": { - "API_KEY": "your-api-key", - "DEBUG": "true" - } - } - } - } - ``` - - **SSE transport example:** - - ```json - { - "mcpServers": { - "default-server": { - "type": "sse", - "url": "http://localhost:3000/events", - "note": "For SSE connections, add this URL directly in Client" - } - } - } - ``` - - **Streamable HTTP transport example:** - - ```json - { - "mcpServers": { - "default-server": { - "type": "streamable-http", - "url": "http://localhost:3000/mcp", - "note": "For Streamable HTTP connections, add this URL directly in your MCP Client" - } - } - } - ``` - -These buttons appear in the Inspector UI after you've configured your server settings, making it easy to save and reuse your configurations. - -For SSE and Streamable HTTP transport connections, the Inspector provides similar functionality for both buttons. The "Server Entry" button copies the configuration that can be added to your existing configuration file, while the "Servers File" button creates a complete configuration file containing the URL for direct use in clients. - -You can paste the Server Entry into your existing `mcp.json` file under your chosen server name, or use the complete Servers File payload to create a new configuration file. - -### Authentication - -The inspector supports bearer token authentication for SSE connections. Enter your token in the UI when connecting to an MCP server, and it will be sent in the Authorization header. You can override the header name using the input field in the sidebar. - -### Security Considerations - -The MCP Inspector includes a proxy server that can run and communicate with local MCP processes. The proxy server should not be exposed to untrusted networks as it has permissions to spawn local processes and can connect to any specified MCP server. - -#### Authentication - -The MCP Inspector proxy server requires authentication by default. When starting the server, a random session token is generated and printed to the console: - -``` -πŸ”‘ Session token: 3a1c267fad21f7150b7d624c160b7f09b0b8c4f623c7107bbf13378f051538d4 - -πŸ”— Open inspector with token pre-filled: - http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=3a1c267fad21f7150b7d624c160b7f09b0b8c4f623c7107bbf13378f051538d4 -``` - -This token must be included as a Bearer token in the Authorization header for all requests to the server. The inspector will automatically open your browser with the token pre-filled in the URL. - -**Automatic browser opening** - The inspector now automatically opens your browser with the token pre-filled in the URL when authentication is enabled. - -**Alternative: Manual configuration** - If you already have the inspector open: - -1. Click the "Configuration" button in the sidebar -2. Find "Proxy Session Token" and enter the token displayed in the proxy console -3. Click "Save" to apply the configuration - -The token will be saved in your browser's local storage for future use. - -If you need to disable authentication (NOT RECOMMENDED), you can set the `DANGEROUSLY_OMIT_AUTH` environment variable: - -```bash -DANGEROUSLY_OMIT_AUTH=true npm start -``` - ---- - -**🚨 WARNING 🚨** - -Disabling authentication with `DANGEROUSLY_OMIT_AUTH` is incredibly dangerous! Disabling auth leaves your machine open to attack not just when exposed to the public internet, but also **via your web browser**. Meaning, visiting a malicious website OR viewing a malicious advertizement could allow an attacker to remotely compromise your computer. Do not disable this feature unless you truly understand the risks. - -Read more about the risks of this vulnerability on Oligo's blog: [Critical RCE Vulnerability in Anthropic MCP Inspector - CVE-2025-49596](https://www.oligo.security/blog/critical-rce-vulnerability-in-anthropic-mcp-inspector-cve-2025-49596) - ---- - -You can also set the token via the `MCP_PROXY_AUTH_TOKEN` environment variable when starting the server: - -```bash -MCP_PROXY_AUTH_TOKEN=$(openssl rand -hex 32) npm start -``` - -#### Local-only Binding - -By default, both the MCP Inspector proxy server and client bind only to `localhost` to prevent network access. This ensures they are not accessible from other devices on the network. If you need to bind to all interfaces for development purposes, you can override this with the `HOST` environment variable: - -```bash -HOST=0.0.0.0 npm start -``` - -**Warning:** Only bind to all interfaces in trusted network environments, as this exposes the proxy server's ability to execute local processes and both services to network access. - -#### DNS Rebinding Protection - -To prevent DNS rebinding attacks, the MCP Inspector validates the `Origin` header on incoming requests. By default, only requests from the client origin are allowed (respects `CLIENT_PORT` if set, defaulting to port 6274). You can configure additional allowed origins by setting the `ALLOWED_ORIGINS` environment variable (comma-separated list): - -```bash -ALLOWED_ORIGINS=http://localhost:6274,http://localhost:8000 npm start -``` - -### Configuration - -The MCP Inspector supports the following configuration settings. To change them, click on the `Configuration` button in the MCP Inspector UI: - -| Setting | Description | Default | -| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -| `MCP_SERVER_REQUEST_TIMEOUT` | Client-side timeout (ms) - Inspector will cancel the request if no response is received within this time. Note: servers may have their own timeouts | 300000 | -| `MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS` | Reset timeout on progress notifications | true | -| `MCP_REQUEST_MAX_TOTAL_TIMEOUT` | Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications) | 60000 | -| `MCP_PROXY_FULL_ADDRESS` | Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577 | "" | -| `MCP_AUTO_OPEN_ENABLED` | Enable automatic browser opening when inspector starts (works with authentication enabled). Only as environment var, not configurable in browser. | true | - -**Note on Timeouts:** The timeout settings above control when the Inspector (as an MCP client) will cancel requests. These are independent of any server-side timeouts. For example, if a server tool has a 10-minute timeout but the Inspector's timeout is set to 30 seconds, the Inspector will cancel the request after 30 seconds. Conversely, if the Inspector's timeout is 10 minutes but the server times out after 30 seconds, you'll receive the server's timeout error. For tools that require user interaction (like elicitation) or long-running operations, ensure the Inspector's timeout is set appropriately. - -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" - } - } - } -} -``` - -#### Transport Types in Config Files - -The inspector automatically detects the transport type from your config file. You can specify different transport types: - -**STDIO (default):** - -```json -{ - "mcpServers": { - "my-stdio-server": { - "type": "stdio", - "command": "npx", - "args": ["@modelcontextprotocol/server-everything"] - } - } -} -``` - -**SSE (Server-Sent Events):** - -```json -{ - "mcpServers": { - "my-sse-server": { - "type": "sse", - "url": "http://localhost:3000/sse" - } - } -} -``` - -**Streamable HTTP:** - -```json -{ - "mcpServers": { - "my-http-server": { - "type": "streamable-http", - "url": "http://localhost:3000/mcp" - } - } -} -``` - -#### Default Server Selection - -You can launch the inspector without specifying a server name if your config has: - -1. **A single server** - automatically selected: - -```bash -# Automatically uses "my-server" if it's the only one -npx @modelcontextprotocol/inspector --config mcp.json -``` - -2. **A server named "default-server"** - automatically selected: - -```json -{ - "mcpServers": { - "default-server": { - "command": "npx", - "args": ["@modelcontextprotocol/server-everything"] - }, - "other-server": { - "command": "node", - "args": ["other.js"] - } - } -} -``` - -> **Tip:** You can easily generate this configuration format using the **Server Entry** and **Servers File** buttons in the Inspector UI, as described in the Servers File Export section above. - -You can also set the initial `transport` type, `serverUrl`, `serverCommand`, and `serverArgs` via query params, for example: - -``` -http://localhost:6274/?transport=sse&serverUrl=http://localhost:8787/sse -http://localhost:6274/?transport=streamable-http&serverUrl=http://localhost:8787/mcp -http://localhost:6274/?transport=stdio&serverCommand=npx&serverArgs=arg1%20arg2 -``` - -You can also set initial config settings via query params, for example: - -``` -http://localhost:6274/?MCP_SERVER_REQUEST_TIMEOUT=60000&MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS=false&MCP_PROXY_FULL_ADDRESS=http://10.1.1.22:5577 -``` - -Note that if both the query param and the corresponding localStorage item are set, the query param will take precedence. - -### From this repository - -If you're working on the inspector itself: - -Development mode: - -```bash -npm run dev - -# To co-develop with the typescript-sdk package (assuming it's cloned in ../typescript-sdk; set MCP_SDK otherwise): -npm run dev:sdk "cd sdk && npm run examples:simple-server:w" -# then open http://localhost:3000/mcp as SHTTP in the inspector. -# To go back to the deployed SDK version: -# npm run unlink:sdk && npm i -``` - -> **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 - -# Call a tool with JSON arguments -npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/call --tool-name mytool --tool-arg 'options={"format": "json", "max_tokens": 100}' - -# List available resources -npx @modelcontextprotocol/inspector --cli node build/index.js --method resources/list - -# List available prompts -npx @modelcontextprotocol/inspector --cli node build/index.js --method prompts/list - -# Connect to a remote MCP server (default is SSE transport) -npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com - -# Connect to a remote MCP server (with Streamable HTTP transport) -npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --transport http --method tools/list - -# Connect to a remote MCP server (with custom headers) -npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --transport http --method tools/list --header "X-API-Key: your-api-key" - -# 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 | - -## Tool Input Validation Guidelines - -When implementing or modifying tool input parameter handling in the Inspector: - -- **Omit optional fields with empty values** - When processing form inputs, omit empty strings or null values for optional parameters, UNLESS the field has an explicit default value in the schema that matches the current value -- **Preserve explicit default values** - If a field schema contains an explicit default (e.g., `default: null`), and the current value matches that default, include it in the request. This is a meaningful value the tool expects -- **Always include required fields** - Preserve required field values even when empty, allowing the MCP server to validate and return appropriate error messages -- **Defer deep validation to the server** - Implement basic field presence checking in the Inspector client, but rely on the MCP server for parameter validation according to its schema - -These guidelines maintain clean parameter passing and proper separation of concerns between the Inspector client and MCP servers. - -## License - -This project is licensed under the MIT Licenseβ€”see the [LICENSE](LICENSE) file for details. diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 21f8e598..00000000 --- a/SECURITY.md +++ /dev/null @@ -1,14 +0,0 @@ -# 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). diff --git a/cli/package.json b/cli/package.json deleted file mode 100644 index 77cc2613..00000000 --- a/cli/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@modelcontextprotocol/inspector-cli", - "version": "0.17.4", - "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 && node scripts/cli-tool-tests.js && node scripts/cli-header-tests.js", - "test:cli": "node scripts/cli-tests.js", - "test:cli-tools": "node scripts/cli-tool-tests.js", - "test:cli-headers": "node scripts/cli-header-tests.js" - }, - "devDependencies": {}, - "dependencies": { - "@modelcontextprotocol/sdk": "^1.20.1", - "commander": "^13.1.0", - "spawn-rx": "^5.1.2" - } -} diff --git a/cli/scripts/cli-header-tests.js b/cli/scripts/cli-header-tests.js deleted file mode 100644 index 0f1d22a9..00000000 --- a/cli/scripts/cli-header-tests.js +++ /dev/null @@ -1,252 +0,0 @@ -#!/usr/bin/env node - -/** - * Integration tests for header functionality - * Tests the CLI header parsing end-to-end - */ - -import { spawn } from "node:child_process"; -import { resolve, dirname } from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const CLI_PATH = resolve(__dirname, "..", "build", "index.js"); - -// ANSI colors for output -const colors = { - GREEN: "\x1b[32m", - RED: "\x1b[31m", - YELLOW: "\x1b[33m", - BLUE: "\x1b[34m", - NC: "\x1b[0m", // No Color -}; - -let testsPassed = 0; -let testsFailed = 0; - -/** - * Run a CLI test with given arguments and check for expected behavior - */ -function runHeaderTest( - testName, - args, - expectSuccess = false, - expectedInOutput = null, -) { - return new Promise((resolve) => { - console.log(`\n${colors.BLUE}Testing: ${testName}${colors.NC}`); - console.log( - `${colors.BLUE}Command: node ${CLI_PATH} ${args.join(" ")}${colors.NC}`, - ); - - const child = spawn("node", [CLI_PATH, ...args], { - stdio: ["pipe", "pipe", "pipe"], - timeout: 10000, - }); - - let stdout = ""; - let stderr = ""; - - child.stdout.on("data", (data) => { - stdout += data.toString(); - }); - - child.stderr.on("data", (data) => { - stderr += data.toString(); - }); - - child.on("close", (code) => { - const output = stdout + stderr; - let passed = true; - let reason = ""; - - // Check exit code expectation - if (expectSuccess && code !== 0) { - passed = false; - reason = `Expected success (exit code 0) but got ${code}`; - } else if (!expectSuccess && code === 0) { - passed = false; - reason = `Expected failure (non-zero exit code) but got success`; - } - - // Check expected output - if (passed && expectedInOutput && !output.includes(expectedInOutput)) { - passed = false; - reason = `Expected output to contain "${expectedInOutput}"`; - } - - if (passed) { - console.log(`${colors.GREEN}PASS: ${testName}${colors.NC}`); - testsPassed++; - } else { - console.log(`${colors.RED}FAIL: ${testName}${colors.NC}`); - console.log(`${colors.RED}Reason: ${reason}${colors.NC}`); - console.log(`${colors.RED}Exit code: ${code}${colors.NC}`); - console.log(`${colors.RED}Output: ${output}${colors.NC}`); - testsFailed++; - } - - resolve(); - }); - - child.on("error", (error) => { - console.log( - `${colors.RED}ERROR: ${testName} - ${error.message}${colors.NC}`, - ); - testsFailed++; - resolve(); - }); - }); -} - -async function runHeaderIntegrationTests() { - console.log( - `${colors.YELLOW}=== MCP Inspector CLI Header Integration Tests ===${colors.NC}`, - ); - console.log( - `${colors.BLUE}Testing header parsing and validation${colors.NC}`, - ); - - // Test 1: Valid header format should parse successfully (connection will fail) - await runHeaderTest( - "Valid single header", - [ - "https://example.com", - "--method", - "tools/list", - "--transport", - "http", - "--header", - "Authorization: Bearer token123", - ], - false, - ); - - // Test 2: Multiple headers should parse successfully - await runHeaderTest( - "Multiple headers", - [ - "https://example.com", - "--method", - "tools/list", - "--transport", - "http", - "--header", - "Authorization: Bearer token123", - "--header", - "X-API-Key: secret123", - ], - false, - ); - - // Test 3: Invalid header format - no colon - await runHeaderTest( - "Invalid header format - no colon", - [ - "https://example.com", - "--method", - "tools/list", - "--transport", - "http", - "--header", - "InvalidHeader", - ], - false, - "Invalid header format", - ); - - // Test 4: Invalid header format - empty name - await runHeaderTest( - "Invalid header format - empty name", - [ - "https://example.com", - "--method", - "tools/list", - "--transport", - "http", - "--header", - ": value", - ], - false, - "Invalid header format", - ); - - // Test 5: Invalid header format - empty value - await runHeaderTest( - "Invalid header format - empty value", - [ - "https://example.com", - "--method", - "tools/list", - "--transport", - "http", - "--header", - "Header:", - ], - false, - "Invalid header format", - ); - - // Test 6: Header with colons in value - await runHeaderTest( - "Header with colons in value", - [ - "https://example.com", - "--method", - "tools/list", - "--transport", - "http", - "--header", - "X-Time: 2023:12:25:10:30:45", - ], - false, - ); - - // Test 7: Whitespace handling - await runHeaderTest( - "Whitespace handling in headers", - [ - "https://example.com", - "--method", - "tools/list", - "--transport", - "http", - "--header", - " X-Header : value with spaces ", - ], - false, - ); - - console.log(`\n${colors.YELLOW}=== Test Results ===${colors.NC}`); - console.log(`${colors.GREEN}Tests passed: ${testsPassed}${colors.NC}`); - console.log(`${colors.RED}Tests failed: ${testsFailed}${colors.NC}`); - - if (testsFailed === 0) { - console.log( - `${colors.GREEN}All header integration tests passed!${colors.NC}`, - ); - process.exit(0); - } else { - console.log( - `${colors.RED}Some header integration tests failed.${colors.NC}`, - ); - process.exit(1); - } -} - -// Handle graceful shutdown -process.on("SIGINT", () => { - console.log(`\n${colors.YELLOW}Test interrupted by user${colors.NC}`); - process.exit(1); -}); - -process.on("SIGTERM", () => { - console.log(`\n${colors.YELLOW}Test terminated${colors.NC}`); - process.exit(1); -}); - -// Run the tests -runHeaderIntegrationTests().catch((error) => { - console.error(`${colors.RED}Test runner error: ${error.message}${colors.NC}`); - process.exit(1); -}); diff --git a/cli/scripts/cli-metadata-tests.js b/cli/scripts/cli-metadata-tests.js deleted file mode 100755 index 0bc664d2..00000000 --- a/cli/scripts/cli-metadata-tests.js +++ /dev/null @@ -1,676 +0,0 @@ -#!/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 { 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 Metadata Tests ===${colors.NC}`, -); -console.log( - `${colors.BLUE}This script tests the MCP Inspector CLI's metadata functionality:${colors.NC}`, -); -console.log( - `${colors.BLUE}- General metadata with --metadata option${colors.NC}`, -); -console.log( - `${colors.BLUE}- Tool-specific metadata with --tool-metadata option${colors.NC}`, -); -console.log( - `${colors.BLUE}- Metadata parsing with various data types${colors.NC}`, -); -console.log( - `${colors.BLUE}- Metadata merging (tool-specific overrides general)${colors.NC}`, -); -console.log( - `${colors.BLUE}- Metadata evaluation in different MCP methods${colors.NC}`, -); -console.log(`\n`); - -// Get directory paths -const SCRIPTS_DIR = __dirname; -const PROJECT_ROOT = path.join(SCRIPTS_DIR, "../../"); -const BUILD_DIR = path.resolve(SCRIPTS_DIR, "../build"); - -// Define the test server command using npx -const TEST_CMD = "npx"; -const TEST_ARGS = ["@modelcontextprotocol/server-everything"]; - -// Create output directory for test results -const OUTPUT_DIR = path.join(SCRIPTS_DIR, "metadata-test-output"); -if (!fs.existsSync(OUTPUT_DIR)) { - fs.mkdirSync(OUTPUT_DIR, { recursive: true }); -} - -// Create a temporary directory for test files -const TEMP_DIR = path.join(os.tmpdir(), "mcp-inspector-metadata-tests"); -fs.mkdirSync(TEMP_DIR, { recursive: true }); - -// Track servers for cleanup -let runningServers = []; - -process.on("exit", () => { - try { - fs.rmSync(TEMP_DIR, { recursive: true, force: true }); - } catch (err) { - console.error( - `${colors.RED}Failed to remove temp directory: ${err.message}${colors.NC}`, - ); - } - - runningServers.forEach((server) => { - try { - process.kill(-server.pid); - } catch (e) {} - }); -}); - -process.on("SIGINT", () => { - runningServers.forEach((server) => { - try { - process.kill(-server.pid); - } catch (e) {} - }); - process.exit(1); -}); - -// Function to run a basic test -async function runBasicTest(testName, ...args) { - const outputFile = path.join( - OUTPUT_DIR, - `${testName.replace(/\//g, "_")}.log`, - ); - - console.log(`\n${colors.YELLOW}Testing: ${testName}${colors.NC}`); - TOTAL_TESTS++; - - // Run the command and capture output - console.log( - `${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`, - ); - - try { - // Create a write stream for the output file - const outputStream = fs.createWriteStream(outputFile); - - // Spawn the process - return new Promise((resolve) => { - const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], { - stdio: ["ignore", "pipe", "pipe"], - }); - - const timeout = setTimeout(() => { - console.log(`${colors.YELLOW}Test timed out: ${testName}${colors.NC}`); - child.kill(); - }, 15000); - - // Pipe stdout and stderr to the output file - child.stdout.pipe(outputStream); - child.stderr.pipe(outputStream); - - // Also capture output for display - let output = ""; - child.stdout.on("data", (data) => { - output += data.toString(); - }); - child.stderr.on("data", (data) => { - output += data.toString(); - }); - - child.on("close", (code) => { - clearTimeout(timeout); - outputStream.end(); - - // Check if we got valid JSON output (indicating success) even if process didn't exit cleanly - const hasValidJsonOutput = - output.includes('"tools"') || - output.includes('"resources"') || - output.includes('"prompts"') || - output.includes('"content"') || - output.includes('"messages"') || - output.includes('"contents"'); - - if (code === 0 || hasValidJsonOutput) { - console.log(`${colors.GREEN}βœ“ Test passed: ${testName}${colors.NC}`); - console.log(`${colors.BLUE}First few lines of output:${colors.NC}`); - const firstFewLines = output - .split("\n") - .slice(0, 5) - .map((line) => ` ${line}`) - .join("\n"); - console.log(firstFewLines); - PASSED_TESTS++; - resolve(true); - } else { - console.log(`${colors.RED}βœ— Test failed: ${testName}${colors.NC}`); - console.log(`${colors.RED}Error output:${colors.NC}`); - console.log( - output - .split("\n") - .map((line) => ` ${line}`) - .join("\n"), - ); - FAILED_TESTS++; - - // Stop after any error is encountered - console.log( - `${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`, - ); - process.exit(1); - } - }); - }); - } catch (error) { - console.error( - `${colors.RED}Error running test: ${error.message}${colors.NC}`, - ); - FAILED_TESTS++; - process.exit(1); - } -} - -// Function to run an error test (expected to fail) -async function runErrorTest(testName, ...args) { - const outputFile = path.join( - OUTPUT_DIR, - `${testName.replace(/\//g, "_")}.log`, - ); - - console.log(`\n${colors.YELLOW}Testing error case: ${testName}${colors.NC}`); - TOTAL_TESTS++; - - // Run the command and capture output - console.log( - `${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`, - ); - - try { - // Create a write stream for the output file - const outputStream = fs.createWriteStream(outputFile); - - // Spawn the process - return new Promise((resolve) => { - const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], { - stdio: ["ignore", "pipe", "pipe"], - }); - - const timeout = setTimeout(() => { - console.log( - `${colors.YELLOW}Error test timed out: ${testName}${colors.NC}`, - ); - child.kill(); - }, 15000); - - // Pipe stdout and stderr to the output file - child.stdout.pipe(outputStream); - child.stderr.pipe(outputStream); - - // Also capture output for display - let output = ""; - child.stdout.on("data", (data) => { - output += data.toString(); - }); - child.stderr.on("data", (data) => { - output += data.toString(); - }); - - child.on("close", (code) => { - clearTimeout(timeout); - outputStream.end(); - - // For error tests, we expect a non-zero exit code - if (code !== 0) { - console.log( - `${colors.GREEN}βœ“ Error test passed: ${testName}${colors.NC}`, - ); - console.log(`${colors.BLUE}Error output (expected):${colors.NC}`); - const firstFewLines = output - .split("\n") - .slice(0, 5) - .map((line) => ` ${line}`) - .join("\n"); - console.log(firstFewLines); - PASSED_TESTS++; - resolve(true); - } else { - console.log( - `${colors.RED}βœ— Error test failed: ${testName} (expected error but got success)${colors.NC}`, - ); - console.log(`${colors.RED}Output:${colors.NC}`); - console.log( - output - .split("\n") - .map((line) => ` ${line}`) - .join("\n"), - ); - FAILED_TESTS++; - - // Stop after any error is encountered - console.log( - `${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`, - ); - process.exit(1); - } - }); - }); - } catch (error) { - console.error( - `${colors.RED}Error running test: ${error.message}${colors.NC}`, - ); - FAILED_TESTS++; - process.exit(1); - } -} - -// Run all tests -async function runTests() { - console.log( - `\n${colors.YELLOW}=== Running General Metadata Tests ===${colors.NC}`, - ); - - // Test 1: General metadata with tools/list - await runBasicTest( - "metadata_tools_list", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/list", - "--metadata", - "client=test-client", - ); - - // Test 2: General metadata with resources/list - await runBasicTest( - "metadata_resources_list", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "resources/list", - "--metadata", - "client=test-client", - ); - - // Test 3: General metadata with prompts/list - await runBasicTest( - "metadata_prompts_list", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "prompts/list", - "--metadata", - "client=test-client", - ); - - // Test 4: General metadata with resources/read - await runBasicTest( - "metadata_resources_read", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "resources/read", - "--uri", - "test://static/resource/1", - "--metadata", - "client=test-client", - ); - - // Test 5: General metadata with prompts/get - await runBasicTest( - "metadata_prompts_get", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "prompts/get", - "--prompt-name", - "simple_prompt", - "--metadata", - "client=test-client", - ); - - console.log( - `\n${colors.YELLOW}=== Running Tool-Specific Metadata Tests ===${colors.NC}`, - ); - - // Test 6: Tool-specific metadata with tools/call - await runBasicTest( - "metadata_tools_call_tool_meta", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "echo", - "--tool-arg", - "message=hello world", - "--tool-metadata", - "client=test-client", - ); - - // Test 7: Tool-specific metadata with complex tool - await runBasicTest( - "metadata_tools_call_complex_tool_meta", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "add", - "--tool-arg", - "a=10", - "b=20", - "--tool-metadata", - "client=test-client", - ); - - console.log( - `\n${colors.YELLOW}=== Running Metadata Merging Tests ===${colors.NC}`, - ); - - // Test 8: General metadata + tool-specific metadata (tool-specific should override) - await runBasicTest( - "metadata_merging_general_and_tool", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "echo", - "--tool-arg", - "message=hello world", - "--metadata", - "client=general-client", - "--tool-metadata", - "client=test-client", - ); - - console.log( - `\n${colors.YELLOW}=== Running Metadata Parsing Tests ===${colors.NC}`, - ); - - // Test 10: Metadata with numeric values (should be converted to strings) - await runBasicTest( - "metadata_parsing_numbers", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/list", - "--metadata", - "integer_value=42", - "decimal_value=3.14159", - "negative_value=-10", - ); - - // Test 11: Metadata with JSON values (should be converted to strings) - await runBasicTest( - "metadata_parsing_json", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/list", - "--metadata", - 'json_object="{\\"key\\":\\"value\\"}"', - 'json_array="[1,2,3]"', - 'json_string="\\"quoted\\""', - ); - - // Test 12: Metadata with special characters - await runBasicTest( - "metadata_parsing_special_chars", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/list", - "--metadata", - "unicode=πŸš€πŸŽ‰βœ¨", - "special_chars=!@#$%^&*()", - "spaces=hello world with spaces", - ); - - console.log( - `\n${colors.YELLOW}=== Running Metadata Edge Cases ===${colors.NC}`, - ); - - // Test 13: Single metadata entry - await runBasicTest( - "metadata_single_entry", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/list", - "--metadata", - "single_key=single_value", - ); - - // Test 14: Many metadata entries - await runBasicTest( - "metadata_many_entries", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/list", - "--metadata", - "key1=value1", - "key2=value2", - "key3=value3", - "key4=value4", - "key5=value5", - ); - - console.log( - `\n${colors.YELLOW}=== Running Metadata Error Cases ===${colors.NC}`, - ); - - // Test 15: Invalid metadata format (missing equals) - await runErrorTest( - "metadata_error_invalid_format", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/list", - "--metadata", - "invalid_format_no_equals", - ); - - // Test 16: Invalid tool-meta format (missing equals) - await runErrorTest( - "metadata_error_invalid_tool_meta_format", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "echo", - "--tool-arg", - "message=test", - "--tool-metadata", - "invalid_format_no_equals", - ); - - console.log( - `\n${colors.YELLOW}=== Running Metadata Impact Tests ===${colors.NC}`, - ); - - // Test 17: Test tool-specific metadata vs general metadata precedence - await runBasicTest( - "metadata_precedence_tool_overrides_general", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "echo", - "--tool-arg", - "message=precedence test", - "--metadata", - "client=general-client", - "--tool-metadata", - "client=tool-specific-client", - ); - - // Test 18: Test metadata with resources methods - await runBasicTest( - "metadata_resources_methods", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "resources/list", - "--metadata", - "resource_client=test-resource-client", - ); - - // Test 19: Test metadata with prompts methods - await runBasicTest( - "metadata_prompts_methods", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "prompts/get", - "--prompt-name", - "simple_prompt", - "--metadata", - "prompt_client=test-prompt-client", - ); - - console.log( - `\n${colors.YELLOW}=== Running Metadata Validation Tests ===${colors.NC}`, - ); - - // Test 20: Test metadata with special characters in keys - await runBasicTest( - "metadata_special_key_characters", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "echo", - "--tool-arg", - "message=special keys test", - "--metadata", - "key-with-dashes=value1", - "key_with_underscores=value2", - "key.with.dots=value3", - ); - - console.log( - `\n${colors.YELLOW}=== Running Metadata Integration Tests ===${colors.NC}`, - ); - - // Test 21: Metadata with all MCP methods - await runBasicTest( - "metadata_integration_all_methods", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/list", - "--metadata", - "integration_test=true", - "test_phase=all_methods", - ); - - // Test 22: Complex metadata scenario - await runBasicTest( - "metadata_complex_scenario", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "echo", - "--tool-arg", - "message=complex test", - "--metadata", - "session_id=12345", - "user_id=67890", - "timestamp=2024-01-01T00:00:00Z", - "request_id=req-abc-123", - "--tool-metadata", - "tool_session=session-xyz-789", - "execution_context=test", - "priority=high", - ); - - // Test 23: Metadata parsing validation test - await runBasicTest( - "metadata_parsing_validation", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "echo", - "--tool-arg", - "message=parsing validation test", - "--metadata", - "valid_key=valid_value", - "numeric_key=123", - "boolean_key=true", - 'json_key=\'{"test":"value"}\'', - "special_key=!@#$%^&*()", - "unicode_key=πŸš€πŸŽ‰βœ¨", - ); - - // 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 metadata 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); -}); diff --git a/cli/scripts/cli-tests.js b/cli/scripts/cli-tests.js deleted file mode 100755 index 554a5262..00000000 --- a/cli/scripts/cli-tests.js +++ /dev/null @@ -1,935 +0,0 @@ -#!/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 { 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}- Resource-related options (--uri)${colors.NC}`); -console.log( - `${colors.BLUE}- Prompt-related options (--prompt-name, --prompt-args)${colors.NC}`, -); -console.log(`${colors.BLUE}- Logging options (--log-level)${colors.NC}`); -console.log( - `${colors.BLUE}- Transport types (--transport http/sse/stdio)${colors.NC}`, -); -console.log( - `${colors.BLUE}- Transport inference from URL suffixes (/mcp, /sse)${colors.NC}`, -); -console.log(`\n`); - -// Get directory paths -const SCRIPTS_DIR = __dirname; -const PROJECT_ROOT = path.join(SCRIPTS_DIR, "../../"); -const BUILD_DIR = path.resolve(SCRIPTS_DIR, "../build"); - -// Define the test server command using npx -const TEST_CMD = "npx"; -const TEST_ARGS = ["@modelcontextprotocol/server-everything"]; - -// Create output directory for test results -const OUTPUT_DIR = path.join(SCRIPTS_DIR, "test-output"); -if (!fs.existsSync(OUTPUT_DIR)) { - fs.mkdirSync(OUTPUT_DIR, { recursive: true }); -} - -// Create a temporary directory for test files -const TEMP_DIR = path.join(os.tmpdir(), "mcp-inspector-tests"); -fs.mkdirSync(TEMP_DIR, { recursive: true }); - -// Track servers for cleanup -let runningServers = []; - -process.on("exit", () => { - try { - fs.rmSync(TEMP_DIR, { recursive: true, force: true }); - } catch (err) { - console.error( - `${colors.RED}Failed to remove temp directory: ${err.message}${colors.NC}`, - ); - } - - runningServers.forEach((server) => { - try { - process.kill(-server.pid); - } catch (e) {} - }); -}); - -process.on("SIGINT", () => { - runningServers.forEach((server) => { - try { - process.kill(-server.pid); - } catch (e) {} - }); - process.exit(1); -}); - -// Use the existing sample config file -console.log( - `${colors.BLUE}Using existing sample config file: ${PROJECT_ROOT}/sample-config.json${colors.NC}`, -); -try { - const sampleConfig = fs.readFileSync( - path.join(PROJECT_ROOT, "sample-config.json"), - "utf8", - ); - console.log(sampleConfig); -} catch (error) { - console.error( - `${colors.RED}Error reading sample config: ${error.message}${colors.NC}`, - ); -} - -// Create an invalid config file for testing -const invalidConfigPath = path.join(TEMP_DIR, "invalid-config.json"); -fs.writeFileSync(invalidConfigPath, '{\n "mcpServers": {\n "invalid": {'); - -// Create config files with different transport types for testing -const sseConfigPath = path.join(TEMP_DIR, "sse-config.json"); -fs.writeFileSync( - sseConfigPath, - JSON.stringify( - { - mcpServers: { - "test-sse": { - type: "sse", - url: "http://localhost:3000/sse", - note: "Test SSE server", - }, - }, - }, - null, - 2, - ), -); - -const httpConfigPath = path.join(TEMP_DIR, "http-config.json"); -fs.writeFileSync( - httpConfigPath, - JSON.stringify( - { - mcpServers: { - "test-http": { - type: "streamable-http", - url: "http://localhost:3000/mcp", - note: "Test HTTP server", - }, - }, - }, - null, - 2, - ), -); - -const stdioConfigPath = path.join(TEMP_DIR, "stdio-config.json"); -fs.writeFileSync( - stdioConfigPath, - JSON.stringify( - { - mcpServers: { - "test-stdio": { - type: "stdio", - command: "npx", - args: ["@modelcontextprotocol/server-everything"], - env: { - TEST_ENV: "test-value", - }, - }, - }, - }, - null, - 2, - ), -); - -// Config without type field (backward compatibility) -const legacyConfigPath = path.join(TEMP_DIR, "legacy-config.json"); -fs.writeFileSync( - legacyConfigPath, - JSON.stringify( - { - mcpServers: { - "test-legacy": { - command: "npx", - args: ["@modelcontextprotocol/server-everything"], - env: { - LEGACY_ENV: "legacy-value", - }, - }, - }, - }, - null, - 2, - ), -); - -// Function to run a basic test -async function runBasicTest(testName, ...args) { - const outputFile = path.join( - OUTPUT_DIR, - `${testName.replace(/\//g, "_")}.log`, - ); - - console.log(`\n${colors.YELLOW}Testing: ${testName}${colors.NC}`); - TOTAL_TESTS++; - - // Run the command and capture output - console.log( - `${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`, - ); - - try { - // Create a write stream for the output file - const outputStream = fs.createWriteStream(outputFile); - - // Spawn the process - return new Promise((resolve) => { - const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], { - stdio: ["ignore", "pipe", "pipe"], - }); - - const timeout = setTimeout(() => { - console.log(`${colors.YELLOW}Test timed out: ${testName}${colors.NC}`); - child.kill(); - }, 10000); - - // Pipe stdout and stderr to the output file - child.stdout.pipe(outputStream); - child.stderr.pipe(outputStream); - - // Also capture output for display - let output = ""; - child.stdout.on("data", (data) => { - output += data.toString(); - }); - child.stderr.on("data", (data) => { - output += data.toString(); - }); - - child.on("close", (code) => { - clearTimeout(timeout); - outputStream.end(); - - if (code === 0) { - console.log(`${colors.GREEN}βœ“ Test passed: ${testName}${colors.NC}`); - console.log(`${colors.BLUE}First few lines of output:${colors.NC}`); - const firstFewLines = output - .split("\n") - .slice(0, 5) - .map((line) => ` ${line}`) - .join("\n"); - console.log(firstFewLines); - PASSED_TESTS++; - resolve(true); - } else { - console.log(`${colors.RED}βœ— Test failed: ${testName}${colors.NC}`); - console.log(`${colors.RED}Error output:${colors.NC}`); - console.log( - output - .split("\n") - .map((line) => ` ${line}`) - .join("\n"), - ); - FAILED_TESTS++; - - // Stop after any error is encountered - console.log( - `${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`, - ); - process.exit(1); - } - }); - }); - } catch (error) { - console.error( - `${colors.RED}Error running test: ${error.message}${colors.NC}`, - ); - FAILED_TESTS++; - process.exit(1); - } -} - -// Function to run an error test (expected to fail) -async function runErrorTest(testName, ...args) { - const outputFile = path.join( - OUTPUT_DIR, - `${testName.replace(/\//g, "_")}.log`, - ); - - console.log(`\n${colors.YELLOW}Testing error case: ${testName}${colors.NC}`); - TOTAL_TESTS++; - - // Run the command and capture output - console.log( - `${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`, - ); - - try { - // Create a write stream for the output file - const outputStream = fs.createWriteStream(outputFile); - - // Spawn the process - return new Promise((resolve) => { - const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], { - stdio: ["ignore", "pipe", "pipe"], - }); - - const timeout = setTimeout(() => { - console.log( - `${colors.YELLOW}Error test timed out: ${testName}${colors.NC}`, - ); - child.kill(); - }, 10000); - - // Pipe stdout and stderr to the output file - child.stdout.pipe(outputStream); - child.stderr.pipe(outputStream); - - // Also capture output for display - let output = ""; - child.stdout.on("data", (data) => { - output += data.toString(); - }); - child.stderr.on("data", (data) => { - output += data.toString(); - }); - - child.on("close", (code) => { - clearTimeout(timeout); - outputStream.end(); - - // For error tests, we expect a non-zero exit code - if (code !== 0) { - console.log( - `${colors.GREEN}βœ“ Error test passed: ${testName}${colors.NC}`, - ); - console.log(`${colors.BLUE}Error output (expected):${colors.NC}`); - const firstFewLines = output - .split("\n") - .slice(0, 5) - .map((line) => ` ${line}`) - .join("\n"); - console.log(firstFewLines); - PASSED_TESTS++; - resolve(true); - } else { - console.log( - `${colors.RED}βœ— Error test failed: ${testName} (expected error but got success)${colors.NC}`, - ); - console.log(`${colors.RED}Output:${colors.NC}`); - console.log( - output - .split("\n") - .map((line) => ` ${line}`) - .join("\n"), - ); - FAILED_TESTS++; - - // Stop after any error is encountered - console.log( - `${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`, - ); - process.exit(1); - } - }); - }); - } catch (error) { - console.error( - `${colors.RED}Error running test: ${error.message}${colors.NC}`, - ); - FAILED_TESTS++; - process.exit(1); - } -} - -// Run all tests -async function runTests() { - console.log( - `\n${colors.YELLOW}=== Running Basic CLI Mode Tests ===${colors.NC}`, - ); - - // Test 1: Basic CLI mode with method - await runBasicTest( - "basic_cli_mode", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/list", - ); - - // Test 2: CLI mode with non-existent method (should fail) - await runErrorTest( - "nonexistent_method", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "nonexistent/method", - ); - - // Test 3: CLI mode without method (should fail) - await runErrorTest("missing_method", TEST_CMD, ...TEST_ARGS, "--cli"); - - console.log( - `\n${colors.YELLOW}=== Running Environment Variable Tests ===${colors.NC}`, - ); - - // Test 4: CLI mode with environment variables - await runBasicTest( - "env_variables", - TEST_CMD, - ...TEST_ARGS, - "-e", - "KEY1=value1", - "-e", - "KEY2=value2", - "--cli", - "--method", - "tools/list", - ); - - // Test 5: CLI mode with invalid environment variable format (should fail) - await runErrorTest( - "invalid_env_format", - TEST_CMD, - ...TEST_ARGS, - "-e", - "INVALID_FORMAT", - "--cli", - "--method", - "tools/list", - ); - - // Test 5b: CLI mode with environment variable containing equals sign in value - await runBasicTest( - "env_variable_with_equals", - TEST_CMD, - ...TEST_ARGS, - "-e", - "API_KEY=abc123=xyz789==", - "--cli", - "--method", - "tools/list", - ); - - // Test 5c: CLI mode with environment variable containing base64-encoded value - await runBasicTest( - "env_variable_with_base64", - TEST_CMD, - ...TEST_ARGS, - "-e", - "JWT_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=", - "--cli", - "--method", - "tools/list", - ); - - console.log( - `\n${colors.YELLOW}=== Running Config File Tests ===${colors.NC}`, - ); - - // Test 6: Using config file with CLI mode - await runBasicTest( - "config_file", - "--config", - path.join(PROJECT_ROOT, "sample-config.json"), - "--server", - "everything", - "--cli", - "--method", - "tools/list", - ); - - // Test 7: Using config file without server name (should fail) - await runErrorTest( - "config_without_server", - "--config", - path.join(PROJECT_ROOT, "sample-config.json"), - "--cli", - "--method", - "tools/list", - ); - - // Test 8: Using server name without config file (should fail) - await runErrorTest( - "server_without_config", - "--server", - "everything", - "--cli", - "--method", - "tools/list", - ); - - // Test 9: Using non-existent config file (should fail) - await runErrorTest( - "nonexistent_config", - "--config", - "./nonexistent-config.json", - "--server", - "everything", - "--cli", - "--method", - "tools/list", - ); - - // Test 10: Using invalid config file format (should fail) - await runErrorTest( - "invalid_config", - "--config", - invalidConfigPath, - "--server", - "everything", - "--cli", - "--method", - "tools/list", - ); - - // Test 11: Using config file with non-existent server (should fail) - await runErrorTest( - "nonexistent_server", - "--config", - path.join(PROJECT_ROOT, "sample-config.json"), - "--server", - "nonexistent", - "--cli", - "--method", - "tools/list", - ); - - console.log( - `\n${colors.YELLOW}=== Running Resource-Related Tests ===${colors.NC}`, - ); - - // Test 16: CLI mode with resource read - await runBasicTest( - "resource_read", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "resources/read", - "--uri", - "test://static/resource/1", - ); - - // Test 17: CLI mode with resource read but missing URI (should fail) - await runErrorTest( - "missing_uri", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "resources/read", - ); - - console.log( - `\n${colors.YELLOW}=== Running Prompt-Related Tests ===${colors.NC}`, - ); - - // Test 18: CLI mode with prompt get - await runBasicTest( - "prompt_get", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "prompts/get", - "--prompt-name", - "simple_prompt", - ); - - // Test 19: CLI mode with prompt get and args - await runBasicTest( - "prompt_get_with_args", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "prompts/get", - "--prompt-name", - "complex_prompt", - "--prompt-args", - "temperature=0.7", - "style=concise", - ); - - // Test 20: CLI mode with prompt get but missing prompt name (should fail) - await runErrorTest( - "missing_prompt_name", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "prompts/get", - ); - - console.log(`\n${colors.YELLOW}=== Running Logging Tests ===${colors.NC}`); - - // Test 21: CLI mode with log level - await runBasicTest( - "log_level", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "logging/setLevel", - "--log-level", - "debug", - ); - - // Test 22: CLI mode with invalid log level (should fail) - await runErrorTest( - "invalid_log_level", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "logging/setLevel", - "--log-level", - "invalid", - ); - - console.log( - `\n${colors.YELLOW}=== Running Combined Option Tests ===${colors.NC}`, - ); - - // Note about the combined options issue - console.log( - `${colors.BLUE}Testing combined options with environment variables and config file.${colors.NC}`, - ); - - // Test 23: CLI mode with config file, environment variables, and tool call - await runBasicTest( - "combined_options", - "--config", - path.join(PROJECT_ROOT, "sample-config.json"), - "--server", - "everything", - "-e", - "CLI_ENV_VAR=cli_value", - "--cli", - "--method", - "tools/list", - ); - - // Test 24: CLI mode with all possible options (that make sense together) - await runBasicTest( - "all_options", - "--config", - path.join(PROJECT_ROOT, "sample-config.json"), - "--server", - "everything", - "-e", - "CLI_ENV_VAR=cli_value", - "--cli", - "--method", - "tools/call", - "--tool-name", - "echo", - "--tool-arg", - "message=Hello", - "--log-level", - "debug", - ); - - console.log( - `\n${colors.YELLOW}=== Running Config Transport Type Tests ===${colors.NC}`, - ); - - // Test 25: Config with stdio transport type - await runBasicTest( - "config_stdio_type", - "--config", - stdioConfigPath, - "--server", - "test-stdio", - "--cli", - "--method", - "tools/list", - ); - - // Test 26: Config with SSE transport type (CLI mode) - expects connection error - await runErrorTest( - "config_sse_type_cli", - "--config", - sseConfigPath, - "--server", - "test-sse", - "--cli", - "--method", - "tools/list", - ); - - // Test 27: Config with streamable-http transport type (CLI mode) - expects connection error - await runErrorTest( - "config_http_type_cli", - "--config", - httpConfigPath, - "--server", - "test-http", - "--cli", - "--method", - "tools/list", - ); - - // Test 28: Legacy config without type field (backward compatibility) - await runBasicTest( - "config_legacy_no_type", - "--config", - legacyConfigPath, - "--server", - "test-legacy", - "--cli", - "--method", - "tools/list", - ); - - console.log( - `\n${colors.YELLOW}=== Running Default Server Tests ===${colors.NC}`, - ); - - // Create config with single server for auto-selection - const singleServerConfigPath = path.join( - TEMP_DIR, - "single-server-config.json", - ); - fs.writeFileSync( - singleServerConfigPath, - JSON.stringify( - { - mcpServers: { - "only-server": { - command: "npx", - args: ["@modelcontextprotocol/server-everything"], - }, - }, - }, - null, - 2, - ), - ); - - // Create config with default-server - const defaultServerConfigPath = path.join( - TEMP_DIR, - "default-server-config.json", - ); - fs.writeFileSync( - defaultServerConfigPath, - JSON.stringify( - { - mcpServers: { - "default-server": { - command: "npx", - args: ["@modelcontextprotocol/server-everything"], - }, - "other-server": { - command: "node", - args: ["other.js"], - }, - }, - }, - null, - 2, - ), - ); - - // Create config with multiple servers (no default) - const multiServerConfigPath = path.join(TEMP_DIR, "multi-server-config.json"); - fs.writeFileSync( - multiServerConfigPath, - JSON.stringify( - { - mcpServers: { - server1: { - command: "npx", - args: ["@modelcontextprotocol/server-everything"], - }, - server2: { - command: "node", - args: ["other.js"], - }, - }, - }, - null, - 2, - ), - ); - - // Test 29: Config with single server auto-selection - await runBasicTest( - "single_server_auto_select", - "--config", - singleServerConfigPath, - "--cli", - "--method", - "tools/list", - ); - - // Test 30: Config with default-server should now require explicit selection (multiple servers) - await runErrorTest( - "default_server_requires_explicit_selection", - "--config", - defaultServerConfigPath, - "--cli", - "--method", - "tools/list", - ); - - // Test 31: Config with multiple servers and no default (should fail) - await runErrorTest( - "multi_server_no_default", - "--config", - multiServerConfigPath, - "--cli", - "--method", - "tools/list", - ); - - console.log( - `\n${colors.YELLOW}=== Running HTTP Transport Tests ===${colors.NC}`, - ); - - console.log( - `${colors.BLUE}Starting server-everything in streamableHttp mode.${colors.NC}`, - ); - const httpServer = spawn( - "npx", - ["@modelcontextprotocol/server-everything", "streamableHttp"], - { - detached: true, - stdio: "ignore", - }, - ); - runningServers.push(httpServer); - - await new Promise((resolve) => setTimeout(resolve, 3000)); - - // Test 32: HTTP transport inferred from URL ending with /mcp - await runBasicTest( - "http_transport_inferred", - "http://127.0.0.1:3001/mcp", - "--cli", - "--method", - "tools/list", - ); - - // Test 33: HTTP transport with explicit --transport http flag - await runBasicTest( - "http_transport_with_explicit_flag", - "http://127.0.0.1:3001/mcp", - "--transport", - "http", - "--cli", - "--method", - "tools/list", - ); - - // Test 34: HTTP transport with suffix and --transport http flag - await runBasicTest( - "http_transport_with_explicit_flag_and_suffix", - "http://127.0.0.1:3001/mcp", - "--transport", - "http", - "--cli", - "--method", - "tools/list", - ); - - // Test 35: SSE transport given to HTTP server (should fail) - await runErrorTest( - "sse_transport_given_to_http_server", - "http://127.0.0.1:3001", - "--transport", - "sse", - "--cli", - "--method", - "tools/list", - ); - - // Test 36: HTTP transport without URL (should fail) - await runErrorTest( - "http_transport_without_url", - "--transport", - "http", - "--cli", - "--method", - "tools/list", - ); - - // Test 37: SSE transport without URL (should fail) - await runErrorTest( - "sse_transport_without_url", - "--transport", - "sse", - "--cli", - "--method", - "tools/list", - ); - - // Kill HTTP server - try { - process.kill(-httpServer.pid); - console.log( - `${colors.BLUE}HTTP server killed, waiting for port to be released...${colors.NC}`, - ); - } catch (e) { - console.log( - `${colors.RED}Error killing HTTP server: ${e.message}${colors.NC}`, - ); - } - - // Print test summary - console.log(`\n${colors.YELLOW}=== Test Summary ===${colors.NC}`); - console.log(`${colors.GREEN}Passed: ${PASSED_TESTS}${colors.NC}`); - console.log(`${colors.RED}Failed: ${FAILED_TESTS}${colors.NC}`); - console.log(`${colors.ORANGE}Skipped: ${SKIPPED_TESTS}${colors.NC}`); - console.log(`Total: ${TOTAL_TESTS}`); - console.log( - `${colors.BLUE}Detailed logs saved to: ${OUTPUT_DIR}${colors.NC}`, - ); - - console.log(`\n${colors.GREEN}All tests completed!${colors.NC}`); -} - -// Run all tests -runTests().catch((error) => { - console.error( - `${colors.RED}Tests failed with error: ${error.message}${colors.NC}`, - ); - process.exit(1); -}); diff --git a/cli/scripts/cli-tool-tests.js b/cli/scripts/cli-tool-tests.js deleted file mode 100644 index b06aea94..00000000 --- a/cli/scripts/cli-tool-tests.js +++ /dev/null @@ -1,614 +0,0 @@ -#!/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 { 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 Tool Tests ===${colors.NC}`); -console.log( - `${colors.BLUE}This script tests the MCP Inspector CLI's tool-related functionality:${colors.NC}`, -); -console.log(`${colors.BLUE}- Tool discovery and listing${colors.NC}`); -console.log( - `${colors.BLUE}- JSON argument parsing (strings, numbers, booleans, objects, arrays)${colors.NC}`, -); -console.log(`${colors.BLUE}- Tool schema validation${colors.NC}`); -console.log( - `${colors.BLUE}- Tool execution with various argument types${colors.NC}`, -); -console.log( - `${colors.BLUE}- Error handling for invalid tools and arguments${colors.NC}`, -); -console.log(`\n`); - -// Get directory paths -const SCRIPTS_DIR = __dirname; -const PROJECT_ROOT = path.join(SCRIPTS_DIR, "../../"); -const BUILD_DIR = path.resolve(SCRIPTS_DIR, "../build"); - -// Define the test server command using npx -const TEST_CMD = "npx"; -const TEST_ARGS = ["@modelcontextprotocol/server-everything"]; - -// Create output directory for test results -const OUTPUT_DIR = path.join(SCRIPTS_DIR, "tool-test-output"); -if (!fs.existsSync(OUTPUT_DIR)) { - fs.mkdirSync(OUTPUT_DIR, { recursive: true }); -} - -// Create a temporary directory for test files -const TEMP_DIR = path.join(os.tmpdir(), "mcp-inspector-tool-tests"); -fs.mkdirSync(TEMP_DIR, { recursive: true }); - -// Track servers for cleanup -let runningServers = []; - -process.on("exit", () => { - try { - fs.rmSync(TEMP_DIR, { recursive: true, force: true }); - } catch (err) { - console.error( - `${colors.RED}Failed to remove temp directory: ${err.message}${colors.NC}`, - ); - } - - runningServers.forEach((server) => { - try { - process.kill(-server.pid); - } catch (e) {} - }); -}); - -process.on("SIGINT", () => { - runningServers.forEach((server) => { - try { - process.kill(-server.pid); - } catch (e) {} - }); - process.exit(1); -}); - -// Function to run a basic test -async function runBasicTest(testName, ...args) { - const outputFile = path.join( - OUTPUT_DIR, - `${testName.replace(/\//g, "_")}.log`, - ); - - console.log(`\n${colors.YELLOW}Testing: ${testName}${colors.NC}`); - TOTAL_TESTS++; - - // Run the command and capture output - console.log( - `${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`, - ); - - try { - // Create a write stream for the output file - const outputStream = fs.createWriteStream(outputFile); - - // Spawn the process - return new Promise((resolve) => { - const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], { - stdio: ["ignore", "pipe", "pipe"], - }); - - const timeout = setTimeout(() => { - console.log(`${colors.YELLOW}Test timed out: ${testName}${colors.NC}`); - child.kill(); - }, 10000); - - // Pipe stdout and stderr to the output file - child.stdout.pipe(outputStream); - child.stderr.pipe(outputStream); - - // Also capture output for display - let output = ""; - child.stdout.on("data", (data) => { - output += data.toString(); - }); - child.stderr.on("data", (data) => { - output += data.toString(); - }); - - child.on("close", (code) => { - clearTimeout(timeout); - outputStream.end(); - - if (code === 0) { - console.log(`${colors.GREEN}βœ“ Test passed: ${testName}${colors.NC}`); - console.log(`${colors.BLUE}First few lines of output:${colors.NC}`); - const firstFewLines = output - .split("\n") - .slice(0, 5) - .map((line) => ` ${line}`) - .join("\n"); - console.log(firstFewLines); - PASSED_TESTS++; - resolve(true); - } else { - console.log(`${colors.RED}βœ— Test failed: ${testName}${colors.NC}`); - console.log(`${colors.RED}Error output:${colors.NC}`); - console.log( - output - .split("\n") - .map((line) => ` ${line}`) - .join("\n"), - ); - FAILED_TESTS++; - - // Stop after any error is encountered - console.log( - `${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`, - ); - process.exit(1); - } - }); - }); - } catch (error) { - console.error( - `${colors.RED}Error running test: ${error.message}${colors.NC}`, - ); - FAILED_TESTS++; - process.exit(1); - } -} - -// Function to run an error test (expected to fail) -async function runErrorTest(testName, ...args) { - const outputFile = path.join( - OUTPUT_DIR, - `${testName.replace(/\//g, "_")}.log`, - ); - - console.log(`\n${colors.YELLOW}Testing error case: ${testName}${colors.NC}`); - TOTAL_TESTS++; - - // Run the command and capture output - console.log( - `${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`, - ); - - try { - // Create a write stream for the output file - const outputStream = fs.createWriteStream(outputFile); - - // Spawn the process - return new Promise((resolve) => { - const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], { - stdio: ["ignore", "pipe", "pipe"], - }); - - const timeout = setTimeout(() => { - console.log( - `${colors.YELLOW}Error test timed out: ${testName}${colors.NC}`, - ); - child.kill(); - }, 10000); - - // Pipe stdout and stderr to the output file - child.stdout.pipe(outputStream); - child.stderr.pipe(outputStream); - - // Also capture output for display - let output = ""; - child.stdout.on("data", (data) => { - output += data.toString(); - }); - child.stderr.on("data", (data) => { - output += data.toString(); - }); - - child.on("close", (code) => { - clearTimeout(timeout); - outputStream.end(); - - // For error tests, we expect a non-zero exit code - if (code !== 0) { - console.log( - `${colors.GREEN}βœ“ Error test passed: ${testName}${colors.NC}`, - ); - console.log(`${colors.BLUE}Error output (expected):${colors.NC}`); - const firstFewLines = output - .split("\n") - .slice(0, 5) - .map((line) => ` ${line}`) - .join("\n"); - console.log(firstFewLines); - PASSED_TESTS++; - resolve(true); - } else { - console.log( - `${colors.RED}βœ— Error test failed: ${testName} (expected error but got success)${colors.NC}`, - ); - console.log(`${colors.RED}Output:${colors.NC}`); - console.log( - output - .split("\n") - .map((line) => ` ${line}`) - .join("\n"), - ); - FAILED_TESTS++; - - // Stop after any error is encountered - console.log( - `${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`, - ); - process.exit(1); - } - }); - }); - } catch (error) { - console.error( - `${colors.RED}Error running test: ${error.message}${colors.NC}`, - ); - FAILED_TESTS++; - process.exit(1); - } -} - -// Run all tests -async function runTests() { - console.log( - `\n${colors.YELLOW}=== Running Tool Discovery Tests ===${colors.NC}`, - ); - - // Test 1: List available tools - await runBasicTest( - "tool_discovery_list", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/list", - ); - - console.log( - `\n${colors.YELLOW}=== Running JSON Argument Parsing Tests ===${colors.NC}`, - ); - - // Test 2: String arguments (backward compatibility) - await runBasicTest( - "json_args_string", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "echo", - "--tool-arg", - "message=hello world", - ); - - // Test 3: Number arguments - await runBasicTest( - "json_args_number_integer", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "add", - "--tool-arg", - "a=42", - "b=58", - ); - - // Test 4: Number arguments with decimals (using add tool with decimal numbers) - await runBasicTest( - "json_args_number_decimal", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "add", - "--tool-arg", - "a=19.99", - "b=20.01", - ); - - // Test 5: Boolean arguments - true - await runBasicTest( - "json_args_boolean_true", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "annotatedMessage", - "--tool-arg", - "messageType=success", - "includeImage=true", - ); - - // Test 6: Boolean arguments - false - await runBasicTest( - "json_args_boolean_false", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "annotatedMessage", - "--tool-arg", - "messageType=error", - "includeImage=false", - ); - - // Test 7: Null arguments (using echo with string "null") - await runBasicTest( - "json_args_null", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "echo", - "--tool-arg", - 'message="null"', - ); - - // Test 14: Multiple arguments with mixed types (using add tool) - await runBasicTest( - "json_args_multiple_mixed", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "add", - "--tool-arg", - "a=42.5", - "b=57.5", - ); - - console.log( - `\n${colors.YELLOW}=== Running JSON Parsing Edge Cases ===${colors.NC}`, - ); - - // Test 15: Invalid JSON should fall back to string - await runBasicTest( - "json_args_invalid_fallback", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "echo", - "--tool-arg", - "message={invalid json}", - ); - - // Test 16: Empty string value - await runBasicTest( - "json_args_empty_value", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "echo", - "--tool-arg", - 'message=""', - ); - - // Test 17: Special characters in strings - await runBasicTest( - "json_args_special_chars", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "echo", - "--tool-arg", - 'message="C:\\\\Users\\\\test"', - ); - - // Test 18: Unicode characters - await runBasicTest( - "json_args_unicode", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "echo", - "--tool-arg", - 'message="πŸš€πŸŽ‰βœ¨"', - ); - - // Test 19: Arguments with equals signs in values - await runBasicTest( - "json_args_equals_in_value", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "echo", - "--tool-arg", - "message=2+2=4", - ); - - // Test 20: Base64-like strings - await runBasicTest( - "json_args_base64_like", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "echo", - "--tool-arg", - "message=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=", - ); - - console.log( - `\n${colors.YELLOW}=== Running Tool Error Handling Tests ===${colors.NC}`, - ); - - // Test 21: Non-existent tool - await runErrorTest( - "tool_error_nonexistent", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "nonexistent_tool", - "--tool-arg", - "message=test", - ); - - // Test 22: Missing tool name - await runErrorTest( - "tool_error_missing_name", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-arg", - "message=test", - ); - - // Test 23: Invalid tool argument format - await runErrorTest( - "tool_error_invalid_arg_format", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "echo", - "--tool-arg", - "invalid_format_no_equals", - ); - - console.log( - `\n${colors.YELLOW}=== Running Prompt JSON Argument Tests ===${colors.NC}`, - ); - - // Test 24: Prompt with JSON arguments - await runBasicTest( - "prompt_json_args_mixed", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "prompts/get", - "--prompt-name", - "complex_prompt", - "--prompt-args", - "temperature=0.7", - 'style="concise"', - 'options={"format":"json","max_tokens":100}', - ); - - // Test 25: Prompt with simple arguments - await runBasicTest( - "prompt_json_args_simple", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "prompts/get", - "--prompt-name", - "simple_prompt", - "--prompt-args", - "name=test", - "count=5", - ); - - console.log( - `\n${colors.YELLOW}=== Running Backward Compatibility Tests ===${colors.NC}`, - ); - - // Test 26: Ensure existing string-only usage still works - await runBasicTest( - "backward_compatibility_strings", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "echo", - "--tool-arg", - "message=hello", - ); - - // Test 27: Multiple string arguments (existing pattern) - using add tool - await runBasicTest( - "backward_compatibility_multiple_strings", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "add", - "--tool-arg", - "a=10", - "b=20", - ); - - // 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 tool 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); -}); diff --git a/cli/scripts/make-executable.js b/cli/scripts/make-executable.js deleted file mode 100755 index f3b8c902..00000000 --- a/cli/scripts/make-executable.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * 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(); diff --git a/cli/src/cli.ts b/cli/src/cli.ts deleted file mode 100644 index f4187e02..00000000 --- a/cli/src/cli.ts +++ /dev/null @@ -1,394 +0,0 @@ -#!/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; - transport?: "stdio" | "sse" | "streamable-http"; - serverUrl?: string; - headers?: Record; -}; - -type CliOptions = { - e?: Record; - config?: string; - server?: string; - cli?: boolean; - transport?: string; - serverUrl?: string; - header?: Record; -}; - -type ServerConfig = - | { - type: "stdio"; - command: string; - args?: string[]; - env?: Record; - } - | { - type: "sse" | "streamable-http"; - url: string; - note?: string; - }; - -function handleError(error: unknown): never { - let message: string; - - if (error instanceof Error) { - message = error.message; - } else if (typeof error === "string") { - message = error; - } else { - message = "Unknown error"; - } - - console.error(message); - - process.exit(1); -} - -function delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms, true)); -} - -async function runWebClient(args: Args): Promise { - // Path to the client entry point - const inspectorClientPath = resolve( - __dirname, - "../../", - "client", - "bin", - "start.js", - ); - - const abort = new AbortController(); - let cancelled: boolean = false; - process.on("SIGINT", () => { - cancelled = true; - abort.abort(); - }); - - // Build arguments to pass to start.js - const startArgs: string[] = []; - - // Pass environment variables - for (const [key, value] of Object.entries(args.envArgs)) { - startArgs.push("-e", `${key}=${value}`); - } - - // Pass transport type if specified - if (args.transport) { - startArgs.push("--transport", args.transport); - } - - // Pass server URL if specified - if (args.serverUrl) { - startArgs.push("--server-url", args.serverUrl); - } - - // Pass command and args (using -- to separate them) - if (args.command) { - startArgs.push("--", args.command, ...args.args); - } - - try { - await spawnPromise("node", [inspectorClientPath, ...startArgs], { - signal: abort.signal, - echoOutput: true, - // pipe the stdout through here, prevents issues with buffering and - // dropping the end of console.out after 8192 chars due to node - // closing the stdout pipe before the output has finished flushing - stdio: "inherit", - }); - } 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 { - // Build CLI arguments - const cliArgs = [cliPath]; - - // Add target URL/command first - cliArgs.push(args.command, ...args.args); - - // Add transport flag if specified - if (args.transport && args.transport !== "stdio") { - // Convert streamable-http back to http for CLI mode - const cliTransport = - args.transport === "streamable-http" ? "http" : args.transport; - cliArgs.push("--transport", cliTransport); - } - - // Add headers if specified - if (args.headers) { - for (const [key, value] of Object.entries(args.headers)) { - cliArgs.push("--header", `${key}: ${value}`); - } - } - - await spawnPromise("node", cliArgs, { - env: { ...process.env, ...args.envArgs }, - signal: abort.signal, - echoOutput: true, - // pipe the stdout through here, prevents issues with buffering and - // dropping the end of console.out after 8192 chars due to node - // closing the stdout pipe before the output has finished flushing - stdio: "inherit", - }); - } 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 parseHeaderPair( - value: string, - previous: Record = {}, -): Record { - const colonIndex = value.indexOf(":"); - - if (colonIndex === -1) { - throw new Error( - `Invalid header format: ${value}. Use "HeaderName: Value" format.`, - ); - } - - const key = value.slice(0, colonIndex).trim(); - const val = value.slice(colonIndex + 1).trim(); - - if (key === "" || val === "") { - throw new Error( - `Invalid header format: ${value}. Use "HeaderName: Value" format.`, - ); - } - - return { ...previous, [key]: 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") - .option("--transport ", "transport type (stdio, sse, http)") - .option("--server-url ", "server URL for SSE/HTTP transport") - .option( - "--header ", - 'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)', - parseHeaderPair, - {}, - ); - - // 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 config and server options - if (!options.config && options.server) { - throw new Error("--server requires --config to be specified"); - } - - // If config is provided without server, try to auto-select - if (options.config && !options.server) { - const configContent = fs.readFileSync( - path.isAbsolute(options.config) - ? options.config - : path.resolve(process.cwd(), options.config), - "utf8", - ); - const parsedConfig = JSON.parse(configContent); - const servers = Object.keys(parsedConfig.mcpServers || {}); - - if (servers.length === 1) { - // Use the only server if there's just one - options.server = servers[0]; - } else if (servers.length === 0) { - throw new Error("No servers found in config file"); - } else { - // Multiple servers, require explicit selection - throw new Error( - `Multiple servers found in config file. Please specify one with --server.\nAvailable servers: ${servers.join(", ")}`, - ); - } - } - - // 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); - - if (config.type === "stdio") { - return { - command: config.command, - args: [...(config.args || []), ...finalArgs], - envArgs: { ...(config.env || {}), ...(options.e || {}) }, - cli: options.cli || false, - transport: "stdio", - headers: options.header, - }; - } else if (config.type === "sse" || config.type === "streamable-http") { - return { - command: config.url, - args: finalArgs, - envArgs: options.e || {}, - cli: options.cli || false, - transport: config.type, - serverUrl: config.url, - headers: options.header, - }; - } else { - // Backwards compatibility: if no type field, assume stdio - return { - command: (config as any).command || "", - args: [...((config as any).args || []), ...finalArgs], - envArgs: { ...((config as any).env || {}), ...(options.e || {}) }, - cli: options.cli || false, - transport: "stdio", - headers: options.header, - }; - } - } - - // Otherwise use command line arguments - const command = finalArgs[0] || ""; - const args = finalArgs.slice(1); - - // Map "http" shorthand to "streamable-http" - let transport = options.transport; - if (transport === "http") { - transport = "streamable-http"; - } - - return { - command, - args, - envArgs: options.e || {}, - cli: options.cli || false, - transport: transport as "stdio" | "sse" | "streamable-http" | undefined, - serverUrl: options.serverUrl, - headers: options.header, - }; -} - -async function main(): Promise { - process.on("uncaughtException", (error) => { - handleError(error); - }); - - try { - const args = parseArgs(); - - if (args.cli) { - await runCli(args); - } else { - await runWebClient(args); - } - } catch (error) { - handleError(error); - } -} - -main(); diff --git a/cli/src/client/connection.ts b/cli/src/client/connection.ts deleted file mode 100644 index dcbe8e51..00000000 --- a/cli/src/client/connection.ts +++ /dev/null @@ -1,57 +0,0 @@ -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); - - if (client.getServerCapabilities()?.logging) { - // default logging level is undefined in the spec, but the user of the - // inspector most likely wants debug. - await client.setLoggingLevel("debug"); - } - } 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)}`, - ); - } -} diff --git a/cli/src/client/index.ts b/cli/src/client/index.ts deleted file mode 100644 index 095d716b..00000000 --- a/cli/src/client/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// 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"; diff --git a/cli/src/client/prompts.ts b/cli/src/client/prompts.ts deleted file mode 100644 index e7a1cf2f..00000000 --- a/cli/src/client/prompts.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { McpResponse } from "./types.js"; - -// JSON value type matching the client utils -type JsonValue = - | string - | number - | boolean - | null - | undefined - | JsonValue[] - | { [key: string]: JsonValue }; - -// List available prompts -export async function listPrompts( - client: Client, - metadata?: Record, -): Promise { - try { - const params = - metadata && Object.keys(metadata).length > 0 ? { _meta: metadata } : {}; - const response = await client.listPrompts(params); - 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, - metadata?: Record, -): Promise { - try { - // Convert all arguments to strings for prompt arguments - const stringArgs: Record = {}; - if (args) { - for (const [key, value] of Object.entries(args)) { - if (typeof value === "string") { - stringArgs[key] = value; - } else if (value === null || value === undefined) { - stringArgs[key] = String(value); - } else { - stringArgs[key] = JSON.stringify(value); - } - } - } - - const params: any = { - name, - arguments: stringArgs, - }; - - if (metadata && Object.keys(metadata).length > 0) { - params._meta = metadata; - } - - const response = await client.getPrompt(params); - - return response; - } catch (error) { - throw new Error( - `Failed to get prompt: ${error instanceof Error ? error.message : String(error)}`, - ); - } -} diff --git a/cli/src/client/resources.ts b/cli/src/client/resources.ts deleted file mode 100644 index 3e44820c..00000000 --- a/cli/src/client/resources.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { McpResponse } from "./types.js"; - -// List available resources -export async function listResources( - client: Client, - metadata?: Record, -): Promise { - try { - const params = - metadata && Object.keys(metadata).length > 0 ? { _meta: metadata } : {}; - const response = await client.listResources(params); - 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, - metadata?: Record, -): Promise { - try { - const params: any = { uri }; - if (metadata && Object.keys(metadata).length > 0) { - params._meta = metadata; - } - const response = await client.readResource(params); - 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, - metadata?: Record, -): Promise { - try { - const params = - metadata && Object.keys(metadata).length > 0 ? { _meta: metadata } : {}; - const response = await client.listResourceTemplates(params); - return response; - } catch (error) { - throw new Error( - `Failed to list resource templates: ${error instanceof Error ? error.message : String(error)}`, - ); - } -} diff --git a/cli/src/client/tools.ts b/cli/src/client/tools.ts deleted file mode 100644 index 51681411..00000000 --- a/cli/src/client/tools.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { Tool } from "@modelcontextprotocol/sdk/types.js"; -import { McpResponse } from "./types.js"; - -// JSON value type matching the client utils -type JsonValue = - | string - | number - | boolean - | null - | undefined - | JsonValue[] - | { [key: string]: JsonValue }; - -type JsonSchemaType = { - type: "string" | "number" | "integer" | "boolean" | "array" | "object"; - description?: string; - properties?: Record; - items?: JsonSchemaType; -}; - -export async function listTools( - client: Client, - metadata?: Record, -): Promise { - try { - const params = - metadata && Object.keys(metadata).length > 0 ? { _meta: metadata } : {}; - const response = await client.listTools(params); - return response; - } catch (error) { - throw new Error( - `Failed to list tools: ${error instanceof Error ? error.message : String(error)}`, - ); - } -} - -function convertParameterValue( - value: string, - schema: JsonSchemaType, -): JsonValue { - 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) as JsonValue; - } 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, - generalMetadata?: Record, - toolSpecificMetadata?: Record, -): Promise { - try { - const toolsResponse = await listTools(client, generalMetadata); - 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, but only for string values - // since we now accept pre-parsed values from the CLI - const stringArgs: Record = {}; - for (const [key, value] of Object.entries(args)) { - if (typeof value === "string") { - stringArgs[key] = value; - } - } - - if (Object.keys(stringArgs).length > 0) { - const convertedStringArgs = convertParameters(tool, stringArgs); - convertedArgs = { ...args, ...convertedStringArgs }; - } - } - - // Merge general metadata with tool-specific metadata - // Tool-specific metadata takes precedence over general metadata - let mergedMetadata: Record | undefined; - if (generalMetadata || toolSpecificMetadata) { - mergedMetadata = { - ...(generalMetadata || {}), - ...(toolSpecificMetadata || {}), - }; - } - - const response = await client.callTool({ - name: name, - arguments: convertedArgs, - _meta: - mergedMetadata && Object.keys(mergedMetadata).length > 0 - ? mergedMetadata - : undefined, - }); - return response; - } catch (error) { - throw new Error( - `Failed to call tool ${name}: ${error instanceof Error ? error.message : String(error)}`, - ); - } -} diff --git a/cli/src/client/types.ts b/cli/src/client/types.ts deleted file mode 100644 index bbbe1bf4..00000000 --- a/cli/src/client/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type McpResponse = Record; diff --git a/cli/src/error-handler.ts b/cli/src/error-handler.ts deleted file mode 100644 index 97257745..00000000 --- a/cli/src/error-handler.ts +++ /dev/null @@ -1,20 +0,0 @@ -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); -} diff --git a/cli/src/index.ts b/cli/src/index.ts deleted file mode 100644 index 45a71a05..00000000 --- a/cli/src/index.ts +++ /dev/null @@ -1,420 +0,0 @@ -#!/usr/bin/env node - -import * as fs from "fs"; -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"; -import { awaitableLog } from "./utils/awaitable-log.js"; - -// JSON value type for CLI arguments -type JsonValue = - | string - | number - | boolean - | null - | undefined - | JsonValue[] - | { [key: string]: JsonValue }; - -type Args = { - target: string[]; - method?: string; - promptName?: string; - promptArgs?: Record; - uri?: string; - logLevel?: LogLevel; - toolName?: string; - toolArg?: Record; - toolMeta?: Record; - transport?: "sse" | "stdio" | "http"; - headers?: Record; - metadata?: Record; -}; - -function createTransportOptions( - target: string[], - transport?: "sse" | "stdio" | "http", - headers?: Record, -): TransportOptions { - if (target.length === 0) { - throw new Error( - "Target is required. Specify a URL or a command to execute.", - ); - } - - const [command, ...commandArgs] = target; - - if (!command) { - throw new Error("Command is required."); - } - - const isUrl = command.startsWith("http://") || command.startsWith("https://"); - - if (isUrl && commandArgs.length > 0) { - throw new Error("Arguments cannot be passed to a URL-based MCP server."); - } - - let transportType: "sse" | "stdio" | "http"; - if (transport) { - if (!isUrl && transport !== "stdio") { - throw new Error("Only stdio transport can be used with local commands."); - } - if (isUrl && transport === "stdio") { - throw new Error("stdio transport cannot be used with URLs."); - } - transportType = transport; - } else if (isUrl) { - const url = new URL(command); - if (url.pathname.endsWith("/mcp")) { - transportType = "http"; - } else if (url.pathname.endsWith("/sse")) { - transportType = "sse"; - } else { - transportType = "sse"; - } - } else { - transportType = "stdio"; - } - - return { - transportType, - command: isUrl ? undefined : command, - args: isUrl ? undefined : commandArgs, - url: isUrl ? command : undefined, - headers, - }; -} - -async function callMethod(args: Args): Promise { - // Read package.json to get name and version for client identity - const pathA = "../package.json"; // We're in package @modelcontextprotocol/inspector-cli - const pathB = "../../package.json"; // We're in package @modelcontextprotocol/inspector - let packageJson: { name: string; version: string }; - let packageJsonData = await import(fs.existsSync(pathA) ? pathA : pathB, { - with: { type: "json" }, - }); - packageJson = packageJsonData.default; - - const transportOptions = createTransportOptions( - args.target, - args.transport, - args.headers, - ); - const transport = createTransport(transportOptions); - - const [, name = packageJson.name] = packageJson.name.split("/"); - const version = packageJson.version; - const clientIdentity = { name, version }; - - const client = new Client(clientIdentity); - - try { - await connect(client, transport); - - let result: McpResponse; - - // Tools methods - if (args.method === "tools/list") { - result = await listTools(client, args.metadata); - } 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 || {}, - args.metadata, - args.toolMeta, - ); - } - // Resources methods - else if (args.method === "resources/list") { - result = await listResources(client, args.metadata); - } 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, args.metadata); - } else if (args.method === "resources/templates/list") { - result = await listResourceTemplates(client, args.metadata); - } - // Prompts methods - else if (args.method === "prompts/list") { - result = await listPrompts(client, args.metadata); - } 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 || {}, - args.metadata, - ); - } - // 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`, - ); - } - - await awaitableLog(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.`, - ); - } - - // Try to parse as JSON first - let parsedValue: JsonValue; - try { - parsedValue = JSON.parse(val) as JsonValue; - } catch { - // If JSON parsing fails, keep as string - parsedValue = val; - } - - return { ...previous, [key as string]: parsedValue }; -} - -function parseHeaderPair( - value: string, - previous: Record = {}, -): Record { - const colonIndex = value.indexOf(":"); - - if (colonIndex === -1) { - throw new Error( - `Invalid header format: ${value}. Use "HeaderName: Value" format.`, - ); - } - - const key = value.slice(0, colonIndex).trim(); - const val = value.slice(colonIndex + 1).trim(); - - if (key === "" || val === "") { - throw new Error( - `Invalid header format: ${value}. Use "HeaderName: Value" format.`, - ); - } - - return { ...previous, [key]: 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; - }, - ) - // - // Transport options - // - .option( - "--transport ", - "Transport type (sse, http, or stdio). Auto-detected from URL: /mcp β†’ http, /sse β†’ sse, commands β†’ stdio", - (value: string) => { - const validTransports = ["sse", "http", "stdio"]; - if (!validTransports.includes(value)) { - throw new Error( - `Invalid transport type: ${value}. Valid types are: ${validTransports.join(", ")}`, - ); - } - return value as "sse" | "http" | "stdio"; - }, - ) - // - // HTTP headers - // - .option( - "--header ", - 'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)', - parseHeaderPair, - {}, - ) - // - // Metadata options - // - .option( - "--metadata ", - "General metadata as key=value pairs (applied to all methods)", - parseKeyValuePair, - {}, - ) - .option( - "--tool-metadata ", - "Tool-specific metadata as key=value pairs (for tools/call method only)", - parseKeyValuePair, - {}, - ); - - // Parse only the arguments before -- - program.parse(preArgs); - - const options = program.opts() as Omit & { - header?: Record; - metadata?: Record; - toolMetadata?: Record; - }; - - 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, - headers: options.header, // commander.js uses 'header' field, map to 'headers' - metadata: options.metadata - ? Object.fromEntries( - Object.entries(options.metadata).map(([key, value]) => [ - key, - String(value), - ]), - ) - : undefined, - toolMeta: options.toolMetadata - ? Object.fromEntries( - Object.entries(options.toolMetadata).map(([key, value]) => [ - key, - String(value), - ]), - ) - : undefined, - }; -} - -async function main(): Promise { - process.on("uncaughtException", (error) => { - handleError(error); - }); - - try { - const args = parseArgs(); - await callMethod(args); - - // Explicitly exit to ensure process terminates in CI - process.exit(0); - } catch (error) { - handleError(error); - } -} - -main(); diff --git a/cli/src/transport.ts b/cli/src/transport.ts deleted file mode 100644 index 84af393b..00000000 --- a/cli/src/transport.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; -import { - getDefaultEnvironment, - StdioClientTransport, -} from "@modelcontextprotocol/sdk/client/stdio.js"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; -import { findActualExecutable } from "spawn-rx"; - -export type TransportOptions = { - transportType: "sse" | "stdio" | "http"; - command?: string; - args?: string[]; - url?: string; - headers?: Record; -}; - -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 = { - ...defaultEnv, - ...processEnv, - }; - - 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 not STDIO, then it must be either SSE or HTTP. - if (!options.url) { - throw new Error("URL must be provided for SSE or HTTP transport types."); - } - const url = new URL(options.url); - - if (transportType === "sse") { - const transportOptions = options.headers - ? { - requestInit: { - headers: options.headers, - }, - } - : undefined; - return new SSEClientTransport(url, transportOptions); - } - - if (transportType === "http") { - const transportOptions = options.headers - ? { - requestInit: { - headers: options.headers, - }, - } - : undefined; - return new StreamableHTTPClientTransport(url, transportOptions); - } - - throw new Error(`Unsupported transport type: ${transportType}`); - } catch (error) { - throw new Error( - `Failed to create transport: ${error instanceof Error ? error.message : String(error)}`, - ); - } -} diff --git a/cli/src/utils/awaitable-log.ts b/cli/src/utils/awaitable-log.ts deleted file mode 100644 index 144f0112..00000000 --- a/cli/src/utils/awaitable-log.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function awaitableLog(logValue: string): Promise { - return new Promise((resolve) => { - process.stdout.write(logValue, () => { - resolve(); - }); - }); -} diff --git a/cli/tsconfig.json b/cli/tsconfig.json deleted file mode 100644 index effa34f2..00000000 --- a/cli/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "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"] -} diff --git a/client/.gitignore b/client/.gitignore deleted file mode 100644 index a547bf36..00000000 --- a/client/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# 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? diff --git a/client/README.md b/client/README.md deleted file mode 100644 index 780c92d8..00000000 --- a/client/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# 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, - }, -}); -``` diff --git a/client/bin/client.js b/client/bin/client.js deleted file mode 100755 index 2a7419e6..00000000 --- a/client/bin/client.js +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env node - -import open from "open"; -import { join, dirname } from "path"; -import { fileURLToPath } from "url"; -import handler from "serve-handler"; -import http from "http"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const distPath = join(__dirname, "../dist"); - -const server = http.createServer((request, response) => { - const handlerOptions = { - public: distPath, - rewrites: [{ source: "/**", destination: "/index.html" }], - headers: [ - { - // Ensure index.html is never cached - source: "index.html", - headers: [ - { - key: "Cache-Control", - value: "no-cache, no-store, max-age=0", - }, - ], - }, - { - // Allow long-term caching for hashed assets - source: "assets/**", - headers: [ - { - key: "Cache-Control", - value: "public, max-age=31536000, immutable", - }, - ], - }, - ], - }; - - return handler(request, response, handlerOptions); -}); - -const port = parseInt(process.env.CLIENT_PORT || "6274", 10); -const host = process.env.HOST || "localhost"; -server.on("listening", () => { - const url = process.env.INSPECTOR_URL || `http://${host}:${port}`; - console.log(`\nπŸš€ MCP Inspector is up and running at:\n ${url}\n`); - if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") { - console.log(`🌐 Opening browser...`); - open(url); - } -}); -server.on("error", (err) => { - if (err.message.includes(`EADDRINUSE`)) { - console.error( - `❌ MCP Inspector PORT IS IN USE at http://${host}:${port} ❌ `, - ); - } else { - throw err; - } -}); -server.listen(port, host); diff --git a/client/bin/start.js b/client/bin/start.js deleted file mode 100755 index f67301de..00000000 --- a/client/bin/start.js +++ /dev/null @@ -1,340 +0,0 @@ -#!/usr/bin/env node - -import open from "open"; -import { resolve, dirname } from "path"; -import { spawnPromise, spawn } from "spawn-rx"; -import { fileURLToPath } from "url"; -import { randomBytes } from "crypto"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277"; - -function delay(ms) { - return new Promise((resolve) => setTimeout(resolve, ms, true)); -} - -function getClientUrl(port, authDisabled, sessionToken, serverPort) { - const host = process.env.HOST || "localhost"; - const baseUrl = `http://${host}:${port}`; - - const params = new URLSearchParams(); - if (serverPort && serverPort !== DEFAULT_MCP_PROXY_LISTEN_PORT) { - params.set("MCP_PROXY_PORT", serverPort); - } - if (!authDisabled) { - params.set("MCP_PROXY_AUTH_TOKEN", sessionToken); - } - return params.size > 0 ? `${baseUrl}/?${params.toString()}` : baseUrl; -} - -async function startDevServer(serverOptions) { - const { - SERVER_PORT, - CLIENT_PORT, - sessionToken, - envVars, - abort, - transport, - serverUrl, - } = serverOptions; - const serverCommand = "npx"; - const serverArgs = ["tsx", "watch", "--clear-screen=false", "src/index.ts"]; - const isWindows = process.platform === "win32"; - - const spawnOptions = { - cwd: resolve(__dirname, "../..", "server"), - env: { - ...process.env, - SERVER_PORT, - CLIENT_PORT, - MCP_PROXY_AUTH_TOKEN: sessionToken, - MCP_ENV_VARS: JSON.stringify(envVars), - ...(transport ? { MCP_TRANSPORT: transport } : {}), - ...(serverUrl ? { MCP_SERVER_URL: serverUrl } : {}), - }, - signal: abort.signal, - echoOutput: true, - }; - - // For Windows, we need to use stdin: 'ignore' to simulate < NUL - if (isWindows) { - spawnOptions.stdin = "ignore"; - } - - const server = spawn(serverCommand, serverArgs, spawnOptions); - - // Give server time to start - const serverOk = await Promise.race([ - new Promise((resolve) => { - server.subscribe({ - complete: () => resolve(false), - error: () => resolve(false), - next: () => {}, // We're using echoOutput - }); - }), - delay(3000).then(() => true), - ]); - - return { server, serverOk }; -} - -async function startProdServer(serverOptions) { - const { - SERVER_PORT, - CLIENT_PORT, - sessionToken, - envVars, - abort, - command, - mcpServerArgs, - transport, - serverUrl, - } = serverOptions; - const inspectorServerPath = resolve( - __dirname, - "../..", - "server", - "build", - "index.js", - ); - - const server = spawnPromise( - "node", - [ - inspectorServerPath, - ...(command ? [`--command=${command}`] : []), - ...(mcpServerArgs && mcpServerArgs.length > 0 - ? [`--args=${mcpServerArgs.join(" ")}`] - : []), - ...(transport ? [`--transport=${transport}`] : []), - ...(serverUrl ? [`--server-url=${serverUrl}`] : []), - ], - { - env: { - ...process.env, - SERVER_PORT, - CLIENT_PORT, - MCP_PROXY_AUTH_TOKEN: sessionToken, - MCP_ENV_VARS: JSON.stringify(envVars), - }, - signal: abort.signal, - echoOutput: true, - }, - ); - - // Make sure server started before starting client - const serverOk = await Promise.race([server, delay(2 * 1000)]); - - return { server, serverOk }; -} - -async function startDevClient(clientOptions) { - const { - CLIENT_PORT, - SERVER_PORT, - authDisabled, - sessionToken, - abort, - cancelled, - } = clientOptions; - const clientCommand = "npx"; - const host = process.env.HOST || "localhost"; - const clientArgs = ["vite", "--port", CLIENT_PORT, "--host", host]; - - const client = spawn(clientCommand, clientArgs, { - cwd: resolve(__dirname, ".."), - env: { ...process.env, CLIENT_PORT }, - signal: abort.signal, - echoOutput: true, - }); - - const url = getClientUrl( - CLIENT_PORT, - authDisabled, - sessionToken, - SERVER_PORT, - ); - - // Give vite time to start before opening or logging the URL - setTimeout(() => { - console.log(`\nπŸš€ MCP Inspector is up and running at:\n ${url}\n`); - if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") { - console.log("🌐 Opening browser..."); - open(url); - } - }, 3000); - - await new Promise((resolve) => { - client.subscribe({ - complete: resolve, - error: (err) => { - if (!cancelled || process.env.DEBUG) { - console.error("Client error:", err); - } - resolve(null); - }, - next: () => {}, // We're using echoOutput - }); - }); -} - -async function startProdClient(clientOptions) { - const { - CLIENT_PORT, - SERVER_PORT, - authDisabled, - sessionToken, - abort, - cancelled, - } = clientOptions; - const inspectorClientPath = resolve( - __dirname, - "../..", - "client", - "bin", - "client.js", - ); - - const url = getClientUrl( - CLIENT_PORT, - authDisabled, - sessionToken, - SERVER_PORT, - ); - - await spawnPromise("node", [inspectorClientPath], { - env: { - ...process.env, - CLIENT_PORT, - INSPECTOR_URL: url, - }, - signal: abort.signal, - echoOutput: true, - }); -} - -async function main() { - // Parse command line arguments - const args = process.argv.slice(2); - const envVars = {}; - const mcpServerArgs = []; - let command = null; - let parsingFlags = true; - let isDev = false; - let transport = null; - let serverUrl = null; - - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - - if (parsingFlags && arg === "--") { - parsingFlags = false; - continue; - } - - if (parsingFlags && arg === "--dev") { - isDev = true; - continue; - } - - if (parsingFlags && arg === "--transport" && i + 1 < args.length) { - transport = args[++i]; - continue; - } - - if (parsingFlags && arg === "--server-url" && i + 1 < args.length) { - serverUrl = args[++i]; - continue; - } - - if (parsingFlags && arg === "-e" && i + 1 < args.length) { - const envVar = args[++i]; - const equalsIndex = envVar.indexOf("="); - - if (equalsIndex !== -1) { - const key = envVar.substring(0, equalsIndex); - const value = envVar.substring(equalsIndex + 1); - envVars[key] = value; - } else { - envVars[envVar] = ""; - } - } else if (!command && !isDev) { - command = arg; - } else if (!isDev) { - mcpServerArgs.push(arg); - } - } - - const CLIENT_PORT = process.env.CLIENT_PORT ?? "6274"; - const SERVER_PORT = process.env.SERVER_PORT ?? DEFAULT_MCP_PROXY_LISTEN_PORT; - - console.log( - isDev - ? "Starting MCP inspector in development mode..." - : "Starting MCP inspector...", - ); - - // Use provided token from environment or generate a new one - const sessionToken = - process.env.MCP_PROXY_AUTH_TOKEN || randomBytes(32).toString("hex"); - const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH; - - const abort = new AbortController(); - - let cancelled = false; - process.on("SIGINT", () => { - cancelled = true; - abort.abort(); - }); - - let server, serverOk; - - try { - const serverOptions = { - SERVER_PORT, - CLIENT_PORT, - sessionToken, - envVars, - abort, - command, - mcpServerArgs, - transport, - serverUrl, - }; - - const result = isDev - ? await startDevServer(serverOptions) - : await startProdServer(serverOptions); - - server = result.server; - serverOk = result.serverOk; - } catch (error) {} - - if (serverOk) { - try { - const clientOptions = { - CLIENT_PORT, - SERVER_PORT, - authDisabled, - sessionToken, - abort, - cancelled, - }; - - await (isDev - ? startDevClient(clientOptions) - : startProdClient(clientOptions)); - } catch (e) { - if (!cancelled || process.env.DEBUG) throw e; - } - } - - return 0; -} - -main() - .then((_) => process.exit(0)) - .catch((e) => { - console.error(e); - process.exit(1); - }); diff --git a/client/components.json b/client/components.json deleted file mode 100644 index d296ddc6..00000000 --- a/client/components.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$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" - } -} diff --git a/client/e2e/cli-arguments.spec.ts b/client/e2e/cli-arguments.spec.ts deleted file mode 100644 index a4dcdcce..00000000 --- a/client/e2e/cli-arguments.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { test, expect } from "@playwright/test"; - -// These tests verify that CLI arguments correctly set URL parameters -// The CLI should parse config files and pass transport/serverUrl as URL params -test.describe("CLI Arguments @cli", () => { - test("should pass transport parameter from command line", async ({ - page, - }) => { - // Simulate: npx . --transport sse --server-url http://localhost:3000/sse - await page.goto( - "http://localhost:6274/?transport=sse&serverUrl=http://localhost:3000/sse", - ); - - // Wait for the Transport Type dropdown to be visible - const selectTrigger = page.getByLabel("Transport Type"); - await expect(selectTrigger).toBeVisible(); - - // Verify transport dropdown shows SSE - await expect(selectTrigger).toContainText("SSE"); - - // Verify URL field is visible and populated - const urlInput = page.locator("#sse-url-input"); - await expect(urlInput).toBeVisible(); - await expect(urlInput).toHaveValue("http://localhost:3000/sse"); - }); - - test("should pass transport parameter for streamable-http", async ({ - page, - }) => { - // Simulate config with streamable-http transport - await page.goto( - "http://localhost:6274/?transport=streamable-http&serverUrl=http://localhost:3000/mcp", - ); - - // Wait for the Transport Type dropdown to be visible - const selectTrigger = page.getByLabel("Transport Type"); - await expect(selectTrigger).toBeVisible(); - - // Verify transport dropdown shows Streamable HTTP - await expect(selectTrigger).toContainText("Streamable HTTP"); - - // Verify URL field is visible and populated - const urlInput = page.locator("#sse-url-input"); - await expect(urlInput).toBeVisible(); - await expect(urlInput).toHaveValue("http://localhost:3000/mcp"); - }); - - test("should not pass transport parameter for stdio config", async ({ - page, - }) => { - // Simulate stdio config (no transport param needed) - await page.goto("http://localhost:6274/"); - - // Wait for the Transport Type dropdown to be visible - const selectTrigger = page.getByLabel("Transport Type"); - await expect(selectTrigger).toBeVisible(); - - // Verify transport dropdown defaults to STDIO - await expect(selectTrigger).toContainText("STDIO"); - - // Verify command/args fields are visible - await expect(page.locator("#command-input")).toBeVisible(); - await expect(page.locator("#arguments-input")).toBeVisible(); - }); -}); diff --git a/client/e2e/global-teardown.js b/client/e2e/global-teardown.js deleted file mode 100644 index 9308d940..00000000 --- a/client/e2e/global-teardown.js +++ /dev/null @@ -1,18 +0,0 @@ -import { rimraf } from "rimraf"; - -async function globalTeardown() { - if (!process.env.CI) { - console.log("Cleaning up test-results directory..."); - // Add a small delay to ensure all Playwright files are written - await new Promise((resolve) => setTimeout(resolve, 100)); - await rimraf("./e2e/test-results"); - console.log("Test-results directory cleaned up."); - } -} - -export default globalTeardown; - -// Call the function when this script is run directly -if (import.meta.url === `file://${process.argv[1]}`) { - globalTeardown().catch(console.error); -} diff --git a/client/e2e/startup-state.spec.ts b/client/e2e/startup-state.spec.ts deleted file mode 100644 index 414ebd3e..00000000 --- a/client/e2e/startup-state.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { test, expect } from "@playwright/test"; - -// Adjust the URL if your dev server runs on a different port -const APP_URL = "http://localhost:6274/"; - -test.describe("Startup State", () => { - test("should not navigate to a tab when Inspector first opens", async ({ - page, - }) => { - await page.goto(APP_URL); - - // Check that there is no hash fragment in the URL - const url = page.url(); - expect(url).not.toContain("#"); - }); -}); diff --git a/client/e2e/transport-type-dropdown.spec.ts b/client/e2e/transport-type-dropdown.spec.ts deleted file mode 100644 index e4ac8fbb..00000000 --- a/client/e2e/transport-type-dropdown.spec.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { test, expect } from "@playwright/test"; - -// Adjust the URL if your dev server runs on a different port -const APP_URL = "http://localhost:6274/"; - -test.describe("Transport Type Dropdown", () => { - test("should have options for STDIO, SSE, and Streamable HTTP", async ({ - page, - }) => { - await page.goto(APP_URL); - - // Wait for the Transport Type dropdown to be visible - const selectTrigger = page.getByLabel("Transport Type"); - await expect(selectTrigger).toBeVisible(); - - // Open the dropdown - await selectTrigger.click(); - - // Check for the three options - await expect(page.getByRole("option", { name: "STDIO" })).toBeVisible(); - await expect(page.getByRole("option", { name: "SSE" })).toBeVisible(); - await expect( - page.getByRole("option", { name: "Streamable HTTP" }), - ).toBeVisible(); - }); - - test("should show Command and Arguments fields and hide URL field when Transport Type is STDIO", async ({ - page, - }) => { - await page.goto(APP_URL); - - // Wait for the Transport Type dropdown to be visible - const selectTrigger = page.getByLabel("Transport Type"); - await expect(selectTrigger).toBeVisible(); - - // Open the dropdown and select STDIO - await selectTrigger.click(); - await page.getByRole("option", { name: "STDIO" }).click(); - - // Wait for the form to update - await page.waitForTimeout(100); - - // Check that Command and Arguments fields are visible - await expect(page.locator("#command-input")).toBeVisible(); - await expect(page.locator("#arguments-input")).toBeVisible(); - - // Check that URL field is not visible - await expect(page.locator("#sse-url-input")).not.toBeVisible(); - - // Also verify the labels are present - await expect(page.getByText("Command")).toBeVisible(); - await expect(page.getByText("Arguments")).toBeVisible(); - await expect(page.getByText("URL")).not.toBeVisible(); - }); - - test("should show URL field and hide Command and Arguments fields when Transport Type is SSE", async ({ - page, - }) => { - await page.goto(APP_URL); - - // Wait for the Transport Type dropdown to be visible - const selectTrigger = page.getByLabel("Transport Type"); - await expect(selectTrigger).toBeVisible(); - - // Open the dropdown and select SSE - await selectTrigger.click(); - await page.getByRole("option", { name: "SSE" }).click(); - - // Wait for the form to update - await page.waitForTimeout(100); - - // Check that URL field is visible - await expect(page.locator("#sse-url-input")).toBeVisible(); - - // Check that Command and Arguments fields are not visible - await expect(page.locator("#command-input")).not.toBeVisible(); - await expect(page.locator("#arguments-input")).not.toBeVisible(); - - // Also verify the labels are present/absent - await expect(page.getByText("URL")).toBeVisible(); - await expect(page.getByText("Command")).not.toBeVisible(); - await expect(page.getByText("Arguments")).not.toBeVisible(); - }); - - test("should show URL field and hide Command and Arguments fields when Transport Type is Streamable HTTP", async ({ - page, - }) => { - await page.goto(APP_URL); - - // Wait for the Transport Type dropdown to be visible - const selectTrigger = page.getByLabel("Transport Type"); - await expect(selectTrigger).toBeVisible(); - - // Open the dropdown and select Streamable HTTP - await selectTrigger.click(); - await page.getByRole("option", { name: "Streamable HTTP" }).click(); - - // Wait for the form to update - await page.waitForTimeout(100); - - // Check that URL field is visible - await expect(page.locator("#sse-url-input")).toBeVisible(); - - // Check that Command and Arguments fields are not visible - await expect(page.locator("#command-input")).not.toBeVisible(); - await expect(page.locator("#arguments-input")).not.toBeVisible(); - - // Also verify the labels are present/absent - await expect(page.getByText("URL")).toBeVisible(); - await expect(page.getByText("Command")).not.toBeVisible(); - await expect(page.getByText("Arguments")).not.toBeVisible(); - }); -}); diff --git a/client/eslint.config.js b/client/eslint.config.js deleted file mode 100644 index 79a552ea..00000000 --- a/client/eslint.config.js +++ /dev/null @@ -1,28 +0,0 @@ -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 }, - ], - }, - }, -); diff --git a/client/index.html b/client/index.html deleted file mode 100644 index b3736a82..00000000 --- a/client/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - MCP Inspector - - -
- - - diff --git a/client/jest.config.cjs b/client/jest.config.cjs deleted file mode 100644 index 9b793eae..00000000 --- a/client/jest.config.cjs +++ /dev/null @@ -1,36 +0,0 @@ -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/", - "/e2e/", - "\\.config\\.(js|ts|cjs|mjs)$", - ], - // Exclude the same patterns from coverage reports - coveragePathIgnorePatterns: [ - "/node_modules/", - "/dist/", - "/bin/", - "/e2e/", - "\\.config\\.(js|ts|cjs|mjs)$", - ], - randomize: true, -}; diff --git a/client/package.json b/client/package.json deleted file mode 100644 index 4b25305a..00000000 --- a/client/package.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "name": "@modelcontextprotocol/inspector-client", - "version": "0.17.4", - "description": "Client-side application for the Model Context Protocol inspector", - "license": "MIT", - "author": "Anthropic, PBC (https://anthropic.com)", - "homepage": "https://modelcontextprotocol.io", - "bugs": "https://github.com/modelcontextprotocol/inspector/issues", - "type": "module", - "bin": { - "mcp-inspector-client": "./bin/start.js" - }, - "files": [ - "bin", - "dist" - ], - "scripts": { - "dev": "vite --port 6274", - "build": "tsc -b && vite build", - "lint": "eslint .", - "preview": "vite preview --port 6274", - "test": "jest --config jest.config.cjs", - "test:watch": "jest --config jest.config.cjs --watch", - "test:e2e": "playwright test e2e && npm run cleanup:e2e", - "cleanup:e2e": "node e2e/global-teardown.js" - }, - "dependencies": { - "@modelcontextprotocol/sdk": "^1.20.1", - "@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-switch": "^1.2.6", - "@radix-ui/react-tabs": "^1.1.1", - "@radix-ui/react-toast": "^1.2.6", - "@radix-ui/react-tooltip": "^1.1.8", - "ajv": "^6.12.6", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.1", - "cmdk": "^1.0.4", - "lucide-react": "^0.523.0", - "pkce-challenge": "^4.1.0", - "prismjs": "^1.30.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-simple-code-editor": "^0.14.1", - "serve-handler": "^6.1.6", - "tailwind-merge": "^2.5.3", - "zod": "^3.25.76" - }, - "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.17.0", - "@types/prismjs": "^1.26.5", - "@types/react": "^18.3.23", - "@types/react-dom": "^18.3.0", - "@types/serve-handler": "^6.1.4", - "@vitejs/plugin-react": "^5.0.4", - "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", - "jest-fixed-jsdom": "^0.0.9", - "postcss": "^8.5.6", - "tailwindcss": "^3.4.13", - "tailwindcss-animate": "^1.0.7", - "ts-jest": "^29.4.0", - "typescript": "^5.5.3", - "typescript-eslint": "^8.38.0", - "vite": "^7.1.11" - } -} diff --git a/client/playwright.config.ts b/client/playwright.config.ts deleted file mode 100644 index 570dd054..00000000 --- a/client/playwright.config.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { defineConfig, devices } from "@playwright/test"; - -/** - * @see https://playwright.dev/docs/test-configuration - */ -export default defineConfig({ - /* Run your local dev server before starting the tests */ - webServer: { - cwd: "..", - command: "npm run dev", - url: "http://localhost:6274", - reuseExistingServer: !process.env.CI, - }, - - testDir: "./e2e", - outputDir: "./e2e/test-results", - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: process.env.CI - ? [ - ["html", { outputFolder: "playwright-report" }], - ["json", { outputFile: "results.json" }], - ["line"], - ] - : [["line"]], - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: "http://localhost:6274", - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: "on-first-retry", - - /* Take screenshots on failure */ - screenshot: "only-on-failure", - - /* Record video on failure */ - video: "retain-on-failure", - }, - - /* Configure projects for major browsers */ - projects: [ - { - name: "chromium", - use: { ...devices["Desktop Chrome"] }, - }, - - { - name: "firefox", - use: { ...devices["Desktop Firefox"] }, - }, - - // Skip WebKit on macOS due to compatibility issues - ...(process.platform !== "darwin" - ? [ - { - name: "webkit", - use: { ...devices["Desktop Safari"] }, - }, - ] - : []), - ], -}); diff --git a/client/postcss.config.js b/client/postcss.config.js deleted file mode 100644 index 2aa7205d..00000000 --- a/client/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/client/public/mcp.svg b/client/public/mcp.svg deleted file mode 100644 index 03d9f85d..00000000 --- a/client/public/mcp.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/client/src/App.css b/client/src/App.css deleted file mode 100644 index 0d669ffa..00000000 --- a/client/src/App.css +++ /dev/null @@ -1,35 +0,0 @@ -.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; -} diff --git a/client/src/App.tsx b/client/src/App.tsx deleted file mode 100644 index fda6b2a8..00000000 --- a/client/src/App.tsx +++ /dev/null @@ -1,1287 +0,0 @@ -import { - ClientRequest, - CompatibilityCallToolResult, - CompatibilityCallToolResultSchema, - CreateMessageResult, - EmptyResultSchema, - GetPromptResultSchema, - ListPromptsResultSchema, - ListResourcesResultSchema, - ListResourceTemplatesResultSchema, - ListToolsResultSchema, - ReadResourceResultSchema, - Resource, - ResourceTemplate, - Root, - ServerNotification, - Tool, - LoggingLevel, -} from "@modelcontextprotocol/sdk/types.js"; -import { OAuthTokensSchema } from "@modelcontextprotocol/sdk/shared/auth.js"; -import { SESSION_KEYS, getServerSpecificKey } from "./lib/constants"; -import { - hasValidMetaName, - hasValidMetaPrefix, - isReservedMetaKey, -} from "@/utils/metaUtils"; -import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "./lib/auth-types"; -import { OAuthStateMachine } from "./lib/oauth-state-machine"; -import { cacheToolOutputSchemas } from "./utils/schemaUtils"; -import { cleanParams } from "./utils/paramUtils"; -import type { JsonSchemaType } from "./utils/jsonUtils"; -import React, { - Suspense, - useCallback, - useEffect, - useRef, - useState, -} from "react"; -import { useConnection } from "./lib/hooks/useConnection"; -import { - useDraggablePane, - useDraggableSidebar, -} from "./lib/hooks/useDraggablePane"; - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Button } from "@/components/ui/button"; -import { - Bell, - Files, - FolderTree, - Hammer, - Hash, - Key, - MessageSquare, - Settings, -} from "lucide-react"; - -import { z } from "zod"; -import "./App.css"; -import AuthDebugger from "./components/AuthDebugger"; -import ConsoleTab from "./components/ConsoleTab"; -import HistoryAndNotifications from "./components/HistoryAndNotifications"; -import PingTab from "./components/PingTab"; -import PromptsTab, { Prompt } from "./components/PromptsTab"; -import ResourcesTab from "./components/ResourcesTab"; -import RootsTab from "./components/RootsTab"; -import SamplingTab, { PendingRequest } from "./components/SamplingTab"; -import Sidebar from "./components/Sidebar"; -import ToolsTab from "./components/ToolsTab"; -import { InspectorConfig } from "./lib/configurationTypes"; -import { - getMCPProxyAddress, - getMCPProxyAuthToken, - getInitialSseUrl, - getInitialTransportType, - getInitialCommand, - getInitialArgs, - initializeInspectorConfig, - saveInspectorConfig, -} from "./utils/configUtils"; -import ElicitationTab, { - PendingElicitationRequest, - ElicitationResponse, -} from "./components/ElicitationTab"; -import { - CustomHeaders, - migrateFromLegacyAuth, -} from "./lib/types/customHeaders"; -import MetadataTab from "./components/MetadataTab"; - -const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1"; - -const filterReservedMetadata = ( - metadata: Record, -): Record => { - return Object.entries(metadata).reduce>( - (acc, [key, value]) => { - if ( - !isReservedMetaKey(key) && - hasValidMetaPrefix(key) && - hasValidMetaName(key) - ) { - acc[key] = value; - } - return acc; - }, - {}, - ); -}; - -const App = () => { - const [resources, setResources] = useState([]); - const [resourceTemplates, setResourceTemplates] = useState< - ResourceTemplate[] - >([]); - const [resourceContent, setResourceContent] = useState(""); - const [resourceContentMap, setResourceContentMap] = useState< - Record - >({}); - 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(getInitialCommand); - const [args, setArgs] = useState(getInitialArgs); - - const [sseUrl, setSseUrl] = useState(getInitialSseUrl); - const [transportType, setTransportType] = useState< - "stdio" | "sse" | "streamable-http" - >(getInitialTransportType); - const [connectionType, setConnectionType] = useState<"direct" | "proxy">( - () => { - return ( - (localStorage.getItem("lastConnectionType") as "direct" | "proxy") || - "proxy" - ); - }, - ); - const [logLevel, setLogLevel] = useState("debug"); - const [notifications, setNotifications] = useState([]); - const [roots, setRoots] = useState([]); - const [env, setEnv] = useState>({}); - - const [config, setConfig] = useState(() => - initializeInspectorConfig(CONFIG_LOCAL_STORAGE_KEY), - ); - const [bearerToken, setBearerToken] = useState(() => { - return localStorage.getItem("lastBearerToken") || ""; - }); - - const [headerName, setHeaderName] = useState(() => { - return localStorage.getItem("lastHeaderName") || ""; - }); - - const [oauthClientId, setOauthClientId] = useState(() => { - return localStorage.getItem("lastOauthClientId") || ""; - }); - - const [oauthScope, setOauthScope] = useState(() => { - return localStorage.getItem("lastOauthScope") || ""; - }); - - const [oauthClientSecret, setOauthClientSecret] = useState(() => { - return localStorage.getItem("lastOauthClientSecret") || ""; - }); - - // Custom headers state with migration from legacy auth - const [customHeaders, setCustomHeaders] = useState(() => { - const savedHeaders = localStorage.getItem("lastCustomHeaders"); - if (savedHeaders) { - try { - return JSON.parse(savedHeaders); - } catch (error) { - console.warn( - `Failed to parse custom headers: "${savedHeaders}", will try legacy migration`, - error, - ); - // Fall back to migration if JSON parsing fails - } - } - - // Migrate from legacy auth if available - const legacyToken = localStorage.getItem("lastBearerToken") || ""; - const legacyHeaderName = localStorage.getItem("lastHeaderName") || ""; - - if (legacyToken) { - return migrateFromLegacyAuth(legacyToken, legacyHeaderName); - } - - // Default to empty array - return [ - { - name: "Authorization", - value: "Bearer ", - enabled: false, - }, - ]; - }); - - const [pendingSampleRequests, setPendingSampleRequests] = useState< - Array< - PendingRequest & { - resolve: (result: CreateMessageResult) => void; - reject: (error: Error) => void; - } - > - >([]); - const [pendingElicitationRequests, setPendingElicitationRequests] = useState< - Array< - PendingElicitationRequest & { - resolve: (response: ElicitationResponse) => void; - decline: (error: Error) => void; - } - > - >([]); - const [isAuthDebuggerVisible, setIsAuthDebuggerVisible] = useState(false); - - const [authState, setAuthState] = - useState(EMPTY_DEBUGGER_STATE); - - // Metadata state - persisted in localStorage - const [metadata, setMetadata] = useState>(() => { - const savedMetadata = localStorage.getItem("lastMetadata"); - if (savedMetadata) { - try { - const parsed = JSON.parse(savedMetadata); - if (parsed && typeof parsed === "object") { - return filterReservedMetadata(parsed); - } - } catch (error) { - console.warn("Failed to parse saved metadata:", error); - } - } - return {}; - }); - - const updateAuthState = (updates: Partial) => { - setAuthState((prev) => ({ ...prev, ...updates })); - }; - - const handleMetadataChange = (newMetadata: Record) => { - const sanitizedMetadata = filterReservedMetadata(newMetadata); - setMetadata(sanitizedMetadata); - localStorage.setItem("lastMetadata", JSON.stringify(sanitizedMetadata)); - }; - 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 [activeTab, setActiveTab] = useState(() => { - const hash = window.location.hash.slice(1); - const initialTab = hash || "resources"; - return initialTab; - }); - - const currentTabRef = useRef(activeTab); - const lastToolCallOriginTabRef = useRef(activeTab); - - useEffect(() => { - currentTabRef.current = activeTab; - }, [activeTab]); - - const { height: historyPaneHeight, handleDragStart } = useDraggablePane(300); - const { - width: sidebarWidth, - isDragging: isSidebarDragging, - handleDragStart: handleSidebarDragStart, - } = useDraggableSidebar(320); - - const { - connectionStatus, - serverCapabilities, - mcpClient, - requestHistory, - clearRequestHistory, - makeRequest, - sendNotification, - handleCompletion, - completionsSupported, - connect: connectMcpServer, - disconnect: disconnectMcpServer, - } = useConnection({ - transportType, - command, - args, - sseUrl, - env, - customHeaders, - oauthClientId, - oauthClientSecret, - oauthScope, - config, - connectionType, - onNotification: (notification) => { - setNotifications((prev) => [...prev, notification as ServerNotification]); - }, - onPendingRequest: (request, resolve, reject) => { - setPendingSampleRequests((prev) => [ - ...prev, - { id: nextRequestId.current++, request, resolve, reject }, - ]); - }, - onElicitationRequest: (request, resolve) => { - const currentTab = lastToolCallOriginTabRef.current; - - setPendingElicitationRequests((prev) => [ - ...prev, - { - id: nextRequestId.current++, - request: { - id: nextRequestId.current, - message: request.params.message, - requestedSchema: request.params.requestedSchema, - }, - originatingTab: currentTab, - resolve, - decline: (error: Error) => { - console.error("Elicitation request rejected:", error); - }, - }, - ]); - - setActiveTab("elicitations"); - window.location.hash = "elicitations"; - }, - getRoots: () => rootsRef.current, - defaultLoggingLevel: logLevel, - metadata, - }); - - useEffect(() => { - if (serverCapabilities) { - const hash = window.location.hash.slice(1); - - const validTabs = [ - ...(serverCapabilities?.resources ? ["resources"] : []), - ...(serverCapabilities?.prompts ? ["prompts"] : []), - ...(serverCapabilities?.tools ? ["tools"] : []), - "ping", - "sampling", - "elicitations", - "roots", - "auth", - ]; - - const isValidTab = validTabs.includes(hash); - - if (!isValidTab) { - const defaultTab = serverCapabilities?.resources - ? "resources" - : serverCapabilities?.prompts - ? "prompts" - : serverCapabilities?.tools - ? "tools" - : "ping"; - - setActiveTab(defaultTab); - window.location.hash = defaultTab; - } - } - }, [serverCapabilities]); - - 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("lastConnectionType", connectionType); - }, [connectionType]); - - useEffect(() => { - if (bearerToken) { - localStorage.setItem("lastBearerToken", bearerToken); - } else { - localStorage.removeItem("lastBearerToken"); - } - }, [bearerToken]); - - useEffect(() => { - if (headerName) { - localStorage.setItem("lastHeaderName", headerName); - } else { - localStorage.removeItem("lastHeaderName"); - } - }, [headerName]); - - useEffect(() => { - localStorage.setItem("lastCustomHeaders", JSON.stringify(customHeaders)); - }, [customHeaders]); - - // Auto-migrate from legacy auth when custom headers are empty but legacy auth exists - useEffect(() => { - if (customHeaders.length === 0 && (bearerToken || headerName)) { - const migratedHeaders = migrateFromLegacyAuth(bearerToken, headerName); - if (migratedHeaders.length > 0) { - setCustomHeaders(migratedHeaders); - // Clear legacy auth after migration - setBearerToken(""); - setHeaderName(""); - } - } - }, [bearerToken, headerName, customHeaders, setCustomHeaders]); - - useEffect(() => { - localStorage.setItem("lastOauthClientId", oauthClientId); - }, [oauthClientId]); - - useEffect(() => { - localStorage.setItem("lastOauthScope", oauthScope); - }, [oauthScope]); - - useEffect(() => { - localStorage.setItem("lastOauthClientSecret", oauthClientSecret); - }, [oauthClientSecret]); - - useEffect(() => { - saveInspectorConfig(CONFIG_LOCAL_STORAGE_KEY, config); - }, [config]); - - const onOAuthConnect = useCallback( - (serverUrl: string) => { - setSseUrl(serverUrl); - setIsAuthDebuggerVisible(false); - void connectMcpServer(); - }, - [connectMcpServer], - ); - - const onOAuthDebugConnect = useCallback( - async ({ - authorizationCode, - errorMsg, - restoredState, - }: { - authorizationCode?: string; - errorMsg?: string; - restoredState?: AuthDebuggerState; - }) => { - setIsAuthDebuggerVisible(true); - - if (errorMsg) { - updateAuthState({ - latestError: new Error(errorMsg), - }); - return; - } - - if (restoredState && authorizationCode) { - let currentState: AuthDebuggerState = { - ...restoredState, - authorizationCode, - oauthStep: "token_request", - isInitiatingAuth: true, - statusMessage: null, - latestError: null, - }; - - try { - const stateMachine = new OAuthStateMachine(sseUrl, (updates) => { - currentState = { ...currentState, ...updates }; - }); - - while ( - currentState.oauthStep !== "complete" && - currentState.oauthStep !== "authorization_code" - ) { - await stateMachine.executeStep(currentState); - } - - if (currentState.oauthStep === "complete") { - updateAuthState({ - ...currentState, - statusMessage: { - type: "success", - message: "Authentication completed successfully", - }, - isInitiatingAuth: false, - }); - } - } catch (error) { - console.error("OAuth continuation error:", error); - updateAuthState({ - latestError: - error instanceof Error ? error : new Error(String(error)), - statusMessage: { - type: "error", - message: `Failed to complete OAuth flow: ${error instanceof Error ? error.message : String(error)}`, - }, - isInitiatingAuth: false, - }); - } - } else if (authorizationCode) { - updateAuthState({ - authorizationCode, - oauthStep: "token_request", - }); - } - }, - [sseUrl], - ); - - useEffect(() => { - const loadOAuthTokens = async () => { - try { - if (sseUrl) { - const key = getServerSpecificKey(SESSION_KEYS.TOKENS, sseUrl); - const tokens = sessionStorage.getItem(key); - if (tokens) { - const parsedTokens = await OAuthTokensSchema.parseAsync( - JSON.parse(tokens), - ); - updateAuthState({ - oauthTokens: parsedTokens, - oauthStep: "complete", - }); - } - } - } catch (error) { - console.error("Error loading OAuth tokens:", error); - } - }; - - loadOAuthTokens(); - }, [sseUrl]); - - useEffect(() => { - const headers: HeadersInit = {}; - const { token: proxyAuthToken, header: proxyAuthTokenHeader } = - getMCPProxyAuthToken(config); - if (proxyAuthToken) { - headers[proxyAuthTokenHeader] = `Bearer ${proxyAuthToken}`; - } - - fetch(`${getMCPProxyAddress(config)}/config`, { headers }) - .then((response) => response.json()) - .then((data) => { - setEnv(data.defaultEnvironment); - if (data.defaultCommand) { - setCommand(data.defaultCommand); - } - if (data.defaultArgs) { - setArgs(data.defaultArgs); - } - if (data.defaultTransport) { - setTransportType( - data.defaultTransport as "stdio" | "sse" | "streamable-http", - ); - } - if (data.defaultServerUrl) { - setSseUrl(data.defaultServerUrl); - } - }) - .catch((error) => - console.error("Error fetching default environment:", error), - ); - }, [config]); - - useEffect(() => { - rootsRef.current = roots; - }, [roots]); - - useEffect(() => { - if (mcpClient && !window.location.hash) { - const defaultTab = serverCapabilities?.resources - ? "resources" - : serverCapabilities?.prompts - ? "prompts" - : serverCapabilities?.tools - ? "tools" - : "ping"; - window.location.hash = defaultTab; - } else if (!mcpClient && window.location.hash) { - // Clear hash when disconnected - completely remove the fragment - window.history.replaceState( - null, - "", - window.location.pathname + window.location.search, - ); - } - }, [mcpClient, serverCapabilities]); - - useEffect(() => { - const handleHashChange = () => { - const hash = window.location.hash.slice(1); - if (hash && hash !== activeTab) { - setActiveTab(hash); - } - }; - - window.addEventListener("hashchange", handleHashChange); - return () => window.removeEventListener("hashchange", handleHashChange); - }, [activeTab]); - - 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 handleResolveElicitation = ( - id: number, - response: ElicitationResponse, - ) => { - setPendingElicitationRequests((prev) => { - const request = prev.find((r) => r.id === id); - if (request) { - request.resolve(response); - - if (request.originatingTab) { - const originatingTab = request.originatingTab; - - const validTabs = [ - ...(serverCapabilities?.resources ? ["resources"] : []), - ...(serverCapabilities?.prompts ? ["prompts"] : []), - ...(serverCapabilities?.tools ? ["tools"] : []), - "ping", - "sampling", - "elicitations", - "roots", - "auth", - ]; - - if (validTabs.includes(originatingTab)) { - setActiveTab(originatingTab); - window.location.hash = originatingTab; - - setTimeout(() => { - setActiveTab(originatingTab); - window.location.hash = originatingTab; - }, 100); - } - } - } - 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 getPrompt = async (name: string, args: Record = {}) => { - lastToolCallOriginTabRef.current = currentTabRef.current; - - const response = await sendMCPRequest( - { - method: "prompts/get" as const, - params: { name, arguments: args }, - }, - GetPromptResultSchema, - "prompts", - ); - setPromptContent(JSON.stringify(response, null, 2)); - }; - - const readResource = async (uri: string) => { - lastToolCallOriginTabRef.current = currentTabRef.current; - - const response = await sendMCPRequest( - { - method: "resources/read" as const, - params: { uri }, - }, - ReadResourceResultSchema, - "resources", - ); - const content = JSON.stringify(response, null, 2); - setResourceContent(content); - setResourceContentMap((prev) => ({ - ...prev, - [uri]: content, - })); - }; - - 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 listTools = async () => { - const response = await sendMCPRequest( - { - method: "tools/list" as const, - params: nextToolCursor ? { cursor: nextToolCursor } : {}, - }, - ListToolsResultSchema, - "tools", - ); - setTools(response.tools); - setNextToolCursor(response.nextCursor); - cacheToolOutputSchemas(response.tools); - }; - - const callTool = async ( - name: string, - params: Record, - toolMetadata?: Record, - ) => { - lastToolCallOriginTabRef.current = currentTabRef.current; - - try { - // Find the tool schema to clean parameters properly - const tool = tools.find((t) => t.name === name); - const cleanedParams = tool?.inputSchema - ? cleanParams(params, tool.inputSchema as JsonSchemaType) - : params; - - // Merge general metadata with tool-specific metadata - // Tool-specific metadata takes precedence over general metadata - const mergedMetadata = { - ...metadata, // General metadata - progressToken: progressTokenRef.current++, - ...toolMetadata, // Tool-specific metadata - }; - - const response = await sendMCPRequest( - { - method: "tools/call" as const, - params: { - name, - arguments: cleanedParams, - _meta: mergedMetadata, - }, - }, - CompatibilityCallToolResultSchema, - "tools", - ); - - setToolResult(response); - // Clear any validation errors since tool execution completed - setErrors((prev) => ({ ...prev, tools: null })); - } catch (e) { - const toolResult: CompatibilityCallToolResult = { - content: [ - { - type: "text", - text: (e as Error).message ?? String(e), - }, - ], - isError: true, - }; - setToolResult(toolResult); - // Clear validation errors - tool execution errors are shown in ToolResults - setErrors((prev) => ({ ...prev, tools: null })); - } - }; - - const handleRootsChange = async () => { - await sendNotification({ method: "notifications/roots/list_changed" }); - }; - - const handleClearNotifications = () => { - setNotifications([]); - }; - - const sendLogLevelRequest = async (level: LoggingLevel) => { - await sendMCPRequest( - { - method: "logging/setLevel" as const, - params: { level }, - }, - z.object({}), - ); - setLogLevel(level); - }; - - const AuthDebuggerWrapper = () => ( - - setIsAuthDebuggerVisible(false)} - authState={authState} - updateAuthState={updateAuthState} - /> - - ); - - if (window.location.pathname === "/oauth/callback") { - const OAuthCallback = React.lazy( - () => import("./components/OAuthCallback"), - ); - return ( - Loading...}> - - - ); - } - - if (window.location.pathname === "/oauth/callback/debug") { - const OAuthDebugCallback = React.lazy( - () => import("./components/OAuthDebugCallback"), - ); - return ( - Loading...}> - - - ); - } - - return ( -
-
- -
-
-
-
- {mcpClient ? ( - { - setActiveTab(value); - window.location.hash = value; - }} - > - - - - Resources - - - - Prompts - - - - Tools - - - - Ping - - - - Sampling - {pendingSampleRequests.length > 0 && ( - - {pendingSampleRequests.length} - - )} - - - - Elicitations - {pendingElicitationRequests.length > 0 && ( - - {pendingElicitationRequests.length} - - )} - - - - Roots - - - - Auth - - - - Metadata - - - -
- {!serverCapabilities?.resources && - !serverCapabilities?.prompts && - !serverCapabilities?.tools ? ( - <> -
-

- The connected server does not support any MCP - capabilities -

-
- { - void sendMCPRequest( - { - method: "ping" as const, - }, - EmptyResultSchema, - ); - }} - /> - - ) : ( - <> - { - 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); - cacheToolOutputSchemas([]); - }} - callTool={async ( - name: string, - params: Record, - metadata?: Record, - ) => { - clearError("tools"); - setToolResult(null); - await callTool(name, params, metadata); - }} - selectedTool={selectedTool} - setSelectedTool={(tool) => { - clearError("tools"); - setSelectedTool(tool); - setToolResult(null); - }} - toolResult={toolResult} - nextCursor={nextToolCursor} - error={errors.tools} - resourceContent={resourceContentMap} - onReadResource={(uri: string) => { - clearError("resources"); - readResource(uri); - }} - /> - - { - void sendMCPRequest( - { - method: "ping" as const, - }, - EmptyResultSchema, - ); - }} - /> - - - - - - - )} -
-
- ) : isAuthDebuggerVisible ? ( - (window.location.hash = value)} - > - - - ) : ( -
-

- Connect to an MCP server to start inspecting -

-
-

- Need to configure authentication? -

- -
-
- )} -
-
-
-
-
-
- -
-
-
-
- ); -}; - -export default App; diff --git a/client/src/__mocks__/styleMock.js b/client/src/__mocks__/styleMock.js deleted file mode 100644 index f053ebf7..00000000 --- a/client/src/__mocks__/styleMock.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {}; diff --git a/client/src/__tests__/App.config.test.tsx b/client/src/__tests__/App.config.test.tsx deleted file mode 100644 index 7458c205..00000000 --- a/client/src/__tests__/App.config.test.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import { render, waitFor } from "@testing-library/react"; -import App from "../App"; -import { DEFAULT_INSPECTOR_CONFIG } from "../lib/constants"; -import { InspectorConfig } from "../lib/configurationTypes"; -import * as configUtils from "../utils/configUtils"; - -// Mock auth dependencies first -jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({ - auth: jest.fn(), -})); - -jest.mock("../lib/oauth-state-machine", () => ({ - OAuthStateMachine: jest.fn(), -})); - -jest.mock("../lib/auth", () => ({ - InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({ - tokens: jest.fn().mockResolvedValue(null), - clear: jest.fn(), - })), - DebugInspectorOAuthClientProvider: jest.fn(), -})); - -// Mock the config utils -jest.mock("../utils/configUtils", () => ({ - ...jest.requireActual("../utils/configUtils"), - getMCPProxyAddress: jest.fn(() => "http://localhost:6277"), - getMCPProxyAuthToken: jest.fn((config: InspectorConfig) => ({ - token: config.MCP_PROXY_AUTH_TOKEN.value, - header: "X-MCP-Proxy-Auth", - })), - getInitialTransportType: jest.fn(() => "stdio"), - getInitialSseUrl: jest.fn(() => "http://localhost:3001/sse"), - getInitialCommand: jest.fn(() => "mcp-server-everything"), - getInitialArgs: jest.fn(() => ""), - initializeInspectorConfig: jest.fn(() => DEFAULT_INSPECTOR_CONFIG), - saveInspectorConfig: jest.fn(), -})); - -// Get references to the mocked functions -const mockGetMCPProxyAuthToken = configUtils.getMCPProxyAuthToken as jest.Mock; -const mockInitializeInspectorConfig = - configUtils.initializeInspectorConfig as jest.Mock; - -// Mock other dependencies -jest.mock("../lib/hooks/useConnection", () => ({ - useConnection: () => ({ - connectionStatus: "disconnected", - serverCapabilities: null, - mcpClient: null, - requestHistory: [], - clearRequestHistory: jest.fn(), - makeRequest: jest.fn(), - sendNotification: jest.fn(), - handleCompletion: jest.fn(), - completionsSupported: false, - connect: jest.fn(), - disconnect: jest.fn(), - }), -})); - -jest.mock("../lib/hooks/useDraggablePane", () => ({ - useDraggablePane: () => ({ - height: 300, - handleDragStart: jest.fn(), - }), - useDraggableSidebar: () => ({ - width: 320, - isDragging: false, - handleDragStart: jest.fn(), - }), -})); - -jest.mock("../components/Sidebar", () => ({ - __esModule: true, - default: () =>
Sidebar
, -})); - -// Mock fetch -global.fetch = jest.fn(); - -describe("App - Config Endpoint", () => { - beforeEach(() => { - jest.clearAllMocks(); - (global.fetch as jest.Mock).mockResolvedValue({ - json: () => - Promise.resolve({ - defaultEnvironment: { TEST_ENV: "test" }, - defaultCommand: "test-command", - defaultArgs: "test-args", - }), - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - - // Reset getMCPProxyAuthToken to default behavior - mockGetMCPProxyAuthToken.mockImplementation((config: InspectorConfig) => ({ - token: config.MCP_PROXY_AUTH_TOKEN.value, - header: "X-MCP-Proxy-Auth", - })); - }); - - test("sends X-MCP-Proxy-Auth header when fetching config with proxy auth token", async () => { - const mockConfig = { - ...DEFAULT_INSPECTOR_CONFIG, - MCP_PROXY_AUTH_TOKEN: { - ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN, - value: "test-proxy-token", - }, - }; - - // Mock initializeInspectorConfig to return our test config - mockInitializeInspectorConfig.mockReturnValue(mockConfig); - - render(); - - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledWith( - "http://localhost:6277/config", - { - headers: { - "X-MCP-Proxy-Auth": "Bearer test-proxy-token", - }, - }, - ); - }); - }); - - test("does not send auth header when proxy auth token is empty", async () => { - const mockConfig = { - ...DEFAULT_INSPECTOR_CONFIG, - MCP_PROXY_AUTH_TOKEN: { - ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN, - value: "", - }, - }; - - // Mock initializeInspectorConfig to return our test config - mockInitializeInspectorConfig.mockReturnValue(mockConfig); - - render(); - - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledWith( - "http://localhost:6277/config", - { - headers: {}, - }, - ); - }); - }); - - test("uses custom header name if getMCPProxyAuthToken returns different header", async () => { - const mockConfig = { - ...DEFAULT_INSPECTOR_CONFIG, - MCP_PROXY_AUTH_TOKEN: { - ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN, - value: "test-proxy-token", - }, - }; - - // Mock to return a custom header name - mockGetMCPProxyAuthToken.mockReturnValue({ - token: "test-proxy-token", - header: "X-Custom-Auth", - }); - mockInitializeInspectorConfig.mockReturnValue(mockConfig); - - render(); - - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledWith( - "http://localhost:6277/config", - { - headers: { - "X-Custom-Auth": "Bearer test-proxy-token", - }, - }, - ); - }); - }); - - test("config endpoint response updates app state", async () => { - const mockConfig = { - ...DEFAULT_INSPECTOR_CONFIG, - MCP_PROXY_AUTH_TOKEN: { - ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN, - value: "test-proxy-token", - }, - }; - - mockInitializeInspectorConfig.mockReturnValue(mockConfig); - - render(); - - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledTimes(1); - }); - - // Verify the fetch was called with correct parameters - expect(global.fetch).toHaveBeenCalledWith( - "http://localhost:6277/config", - expect.objectContaining({ - headers: expect.objectContaining({ - "X-MCP-Proxy-Auth": "Bearer test-proxy-token", - }), - }), - ); - }); - - test("handles config endpoint errors gracefully", async () => { - const mockConfig = { - ...DEFAULT_INSPECTOR_CONFIG, - MCP_PROXY_AUTH_TOKEN: { - ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN, - value: "test-proxy-token", - }, - }; - - mockInitializeInspectorConfig.mockReturnValue(mockConfig); - - // Mock fetch to reject - (global.fetch as jest.Mock).mockRejectedValue(new Error("Network error")); - - // Spy on console.error - const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(); - - render(); - - await waitFor(() => { - expect(consoleErrorSpy).toHaveBeenCalledWith( - "Error fetching default environment:", - expect.any(Error), - ); - }); - - consoleErrorSpy.mockRestore(); - }); -}); diff --git a/client/src/__tests__/App.routing.test.tsx b/client/src/__tests__/App.routing.test.tsx deleted file mode 100644 index cc80f77d..00000000 --- a/client/src/__tests__/App.routing.test.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { render, waitFor } from "@testing-library/react"; -import App from "../App"; -import { useConnection } from "../lib/hooks/useConnection"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; - -// Mock auth dependencies first -jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({ - auth: jest.fn(), -})); - -jest.mock("../lib/oauth-state-machine", () => ({ - OAuthStateMachine: jest.fn(), -})); - -jest.mock("../lib/auth", () => ({ - InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({ - tokens: jest.fn().mockResolvedValue(null), - clear: jest.fn(), - })), - DebugInspectorOAuthClientProvider: jest.fn(), -})); - -// Mock the config utils -jest.mock("../utils/configUtils", () => ({ - ...jest.requireActual("../utils/configUtils"), - getMCPProxyAddress: jest.fn(() => "http://localhost:6277"), - getMCPProxyAuthToken: jest.fn(() => ({ - token: "", - header: "X-MCP-Proxy-Auth", - })), - getInitialTransportType: jest.fn(() => "stdio"), - getInitialSseUrl: jest.fn(() => "http://localhost:3001/sse"), - getInitialCommand: jest.fn(() => "mcp-server-everything"), - getInitialArgs: jest.fn(() => ""), - initializeInspectorConfig: jest.fn(() => ({})), - saveInspectorConfig: jest.fn(), -})); - -// Default connection state is disconnected -const disconnectedConnectionState = { - connectionStatus: "disconnected" as const, - serverCapabilities: null, - mcpClient: null, - requestHistory: [], - clearRequestHistory: jest.fn(), - makeRequest: jest.fn(), - sendNotification: jest.fn(), - handleCompletion: jest.fn(), - completionsSupported: false, - connect: jest.fn(), - disconnect: jest.fn(), -}; - -// Connected state for tests that need an active connection -const connectedConnectionState = { - ...disconnectedConnectionState, - connectionStatus: "connected" as const, - serverCapabilities: {}, - mcpClient: { - request: jest.fn(), - notification: jest.fn(), - close: jest.fn(), - } as unknown as Client, -}; - -// Mock required dependencies, but unrelated to routing. -jest.mock("../lib/hooks/useDraggablePane", () => ({ - useDraggablePane: () => ({ - height: 300, - handleDragStart: jest.fn(), - }), - useDraggableSidebar: () => ({ - width: 320, - isDragging: false, - handleDragStart: jest.fn(), - }), -})); - -jest.mock("../components/Sidebar", () => ({ - __esModule: true, - default: () =>
Sidebar
, -})); - -// Mock fetch -global.fetch = jest.fn().mockResolvedValue({ json: () => Promise.resolve({}) }); - -// Use an empty module mock, so that mock state can be reset between tests. -jest.mock("../lib/hooks/useConnection", () => ({ - useConnection: jest.fn(), -})); - -describe("App - URL Fragment Routing", () => { - const mockUseConnection = jest.mocked(useConnection); - - beforeEach(() => { - jest.restoreAllMocks(); - - // Inspector starts disconnected. - mockUseConnection.mockReturnValue(disconnectedConnectionState); - }); - - test("does not set hash when starting disconnected", async () => { - render(); - - await waitFor(() => { - expect(window.location.hash).toBe(""); - }); - }); - - test("sets default hash based on server capabilities priority", async () => { - // Tab priority follows UI order: Resources | Prompts | Tools | Ping | Sampling | Roots | Auth - // - // Server capabilities determine the first three tabs; if none are present, falls back to Ping. - - const testCases = [ - { - capabilities: { resources: { listChanged: true, subscribe: true } }, - expected: "#resources", - }, - { - capabilities: { prompts: { listChanged: true, subscribe: true } }, - expected: "#prompts", - }, - { - capabilities: { tools: { listChanged: true, subscribe: true } }, - expected: "#tools", - }, - { capabilities: {}, expected: "#ping" }, - ]; - - const { rerender } = render(); - - for (const { capabilities, expected } of testCases) { - window.location.hash = ""; - mockUseConnection.mockReturnValue({ - ...connectedConnectionState, - serverCapabilities: capabilities, - }); - - rerender(); - - await waitFor(() => { - expect(window.location.hash).toBe(expected); - }); - } - }); - - test("clears hash when disconnected", async () => { - // Start with a hash set (simulating a connection) - window.location.hash = "#resources"; - - // App starts disconnected (default mock) - render(); - - // Should clear the hash when disconnected - await waitFor(() => { - expect(window.location.hash).toBe(""); - }); - }); -}); diff --git a/client/src/components/AuthDebugger.tsx b/client/src/components/AuthDebugger.tsx deleted file mode 100644 index 6252c116..00000000 --- a/client/src/components/AuthDebugger.tsx +++ /dev/null @@ -1,323 +0,0 @@ -import { useCallback, useMemo, useEffect } from "react"; -import { Button } from "@/components/ui/button"; -import { DebugInspectorOAuthClientProvider } from "../lib/auth"; -import { AlertCircle } from "lucide-react"; -import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "../lib/auth-types"; -import { OAuthFlowProgress } from "./OAuthFlowProgress"; -import { OAuthStateMachine } from "../lib/oauth-state-machine"; -import { SESSION_KEYS } from "../lib/constants"; -import { validateRedirectUrl } from "@/utils/urlValidation"; - -export interface AuthDebuggerProps { - serverUrl: string; - onBack: () => void; - authState: AuthDebuggerState; - updateAuthState: (updates: Partial) => void; -} - -interface StatusMessageProps { - message: { type: "error" | "success" | "info"; message: string }; -} - -const StatusMessage = ({ message }: StatusMessageProps) => { - let bgColor: string; - let textColor: string; - let borderColor: string; - - switch (message.type) { - case "error": - bgColor = "bg-red-50"; - textColor = "text-red-700"; - borderColor = "border-red-200"; - break; - case "success": - bgColor = "bg-green-50"; - textColor = "text-green-700"; - borderColor = "border-green-200"; - break; - case "info": - default: - bgColor = "bg-blue-50"; - textColor = "text-blue-700"; - borderColor = "border-blue-200"; - break; - } - - return ( -
-
- -

{message.message}

-
-
- ); -}; - -const AuthDebugger = ({ - serverUrl: serverUrl, - onBack, - authState, - updateAuthState, -}: AuthDebuggerProps) => { - // Check for existing tokens on mount - useEffect(() => { - if (serverUrl && !authState.oauthTokens) { - const checkTokens = async () => { - try { - const provider = new DebugInspectorOAuthClientProvider(serverUrl); - const existingTokens = await provider.tokens(); - if (existingTokens) { - updateAuthState({ - oauthTokens: existingTokens, - oauthStep: "complete", - }); - } - } catch (error) { - console.error("Failed to load existing OAuth tokens:", error); - } - }; - checkTokens(); - } - }, [serverUrl, updateAuthState, authState.oauthTokens]); - - const startOAuthFlow = useCallback(() => { - if (!serverUrl) { - updateAuthState({ - statusMessage: { - type: "error", - message: - "Please enter a server URL in the sidebar before authenticating", - }, - }); - return; - } - - updateAuthState({ - oauthStep: "metadata_discovery", - authorizationUrl: null, - statusMessage: null, - latestError: null, - }); - }, [serverUrl, updateAuthState]); - - const stateMachine = useMemo( - () => new OAuthStateMachine(serverUrl, updateAuthState), - [serverUrl, updateAuthState], - ); - - const proceedToNextStep = useCallback(async () => { - if (!serverUrl) return; - - try { - updateAuthState({ - isInitiatingAuth: true, - statusMessage: null, - latestError: null, - }); - - await stateMachine.executeStep(authState); - } catch (error) { - console.error("OAuth flow error:", error); - updateAuthState({ - latestError: error instanceof Error ? error : new Error(String(error)), - }); - } finally { - updateAuthState({ isInitiatingAuth: false }); - } - }, [serverUrl, authState, updateAuthState, stateMachine]); - - const handleQuickOAuth = useCallback(async () => { - if (!serverUrl) { - updateAuthState({ - statusMessage: { - type: "error", - message: - "Please enter a server URL in the sidebar before authenticating", - }, - }); - return; - } - - updateAuthState({ isInitiatingAuth: true, statusMessage: null }); - try { - // Step through the OAuth flow using the state machine instead of the auth() function - let currentState: AuthDebuggerState = { - ...authState, - oauthStep: "metadata_discovery", - authorizationUrl: null, - latestError: null, - }; - - const oauthMachine = new OAuthStateMachine(serverUrl, (updates) => { - // Update our temporary state during the process - currentState = { ...currentState, ...updates }; - // But don't call updateAuthState yet - }); - - // Manually step through each stage of the OAuth flow - while (currentState.oauthStep !== "complete") { - await oauthMachine.executeStep(currentState); - // In quick mode, we'll just redirect to the authorization URL - if ( - currentState.oauthStep === "authorization_code" && - currentState.authorizationUrl - ) { - // Validate the URL before redirecting - try { - validateRedirectUrl(currentState.authorizationUrl); - } catch (error) { - updateAuthState({ - ...currentState, - isInitiatingAuth: false, - latestError: - error instanceof Error ? error : new Error(String(error)), - statusMessage: { - type: "error", - message: `Invalid authorization URL: ${error instanceof Error ? error.message : String(error)}`, - }, - }); - return; - } - - // Store the current auth state before redirecting - sessionStorage.setItem( - SESSION_KEYS.AUTH_DEBUGGER_STATE, - JSON.stringify(currentState), - ); - // Open the authorization URL automatically - window.location.href = currentState.authorizationUrl.toString(); - break; - } - } - - // After the flow completes or reaches a user-input step, update the app state - updateAuthState({ - ...currentState, - statusMessage: { - type: "info", - message: - currentState.oauthStep === "complete" - ? "Authentication completed successfully" - : "Please complete authentication in the opened window and enter the code", - }, - }); - } catch (error) { - console.error("OAuth initialization error:", error); - updateAuthState({ - statusMessage: { - type: "error", - message: `Failed to start OAuth flow: ${error instanceof Error ? error.message : String(error)}`, - }, - }); - } finally { - updateAuthState({ isInitiatingAuth: false }); - } - }, [serverUrl, updateAuthState, authState]); - - const handleClearOAuth = useCallback(() => { - if (serverUrl) { - const serverAuthProvider = new DebugInspectorOAuthClientProvider( - serverUrl, - ); - serverAuthProvider.clear(); - updateAuthState({ - ...EMPTY_DEBUGGER_STATE, - statusMessage: { - type: "success", - message: "OAuth tokens cleared successfully", - }, - }); - - // Clear success message after 3 seconds - setTimeout(() => { - updateAuthState({ statusMessage: null }); - }, 3000); - } - }, [serverUrl, updateAuthState]); - - return ( -
-
-

Authentication Settings

- -
- -
-
-
-

- Configure authentication settings for your MCP server connection. -

- -
-

OAuth Authentication

-

- Use OAuth to securely authenticate with the MCP server. -

- - {authState.statusMessage && ( - - )} - -
- {authState.oauthTokens && ( -
-

Access Token:

-
- {authState.oauthTokens.access_token.substring(0, 25)}... -
-
- )} - -
- - - - - -
- -

- Choose "Guided" for step-by-step instructions or "Quick" for - the standard automatic flow. -

-
-
- - -
-
-
-
- ); -}; - -export default AuthDebugger; diff --git a/client/src/components/ConsoleTab.tsx b/client/src/components/ConsoleTab.tsx deleted file mode 100644 index 8f05f704..00000000 --- a/client/src/components/ConsoleTab.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { TabsContent } from "@/components/ui/tabs"; - -const ConsoleTab = () => ( - -
-
Welcome to MCP Client Console
- {/* Console output would go here */} -
-
-); - -export default ConsoleTab; diff --git a/client/src/components/CustomHeaders.tsx b/client/src/components/CustomHeaders.tsx deleted file mode 100644 index 463f7333..00000000 --- a/client/src/components/CustomHeaders.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import { useState } from "react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import { Switch } from "@/components/ui/switch"; -import { Plus, Trash2, Eye, EyeOff } from "lucide-react"; -import { - CustomHeaders as CustomHeadersType, - CustomHeader, - createEmptyHeader, -} from "@/lib/types/customHeaders"; - -interface CustomHeadersProps { - headers: CustomHeadersType; - onChange: (headers: CustomHeadersType) => void; - className?: string; -} - -const CustomHeaders = ({ - headers, - onChange, - className, -}: CustomHeadersProps) => { - const [isJsonMode, setIsJsonMode] = useState(false); - const [jsonValue, setJsonValue] = useState(""); - const [jsonError, setJsonError] = useState(null); - const [visibleValues, setVisibleValues] = useState>(new Set()); - - const updateHeader = ( - index: number, - field: keyof CustomHeader, - value: string | boolean, - ) => { - const newHeaders = [...headers]; - newHeaders[index] = { ...newHeaders[index], [field]: value }; - onChange(newHeaders); - }; - - const addHeader = () => { - onChange([...headers, createEmptyHeader()]); - }; - - const removeHeader = (index: number) => { - const newHeaders = headers.filter((_, i) => i !== index); - onChange(newHeaders); - }; - - const toggleValueVisibility = (index: number) => { - const newVisible = new Set(visibleValues); - if (newVisible.has(index)) { - newVisible.delete(index); - } else { - newVisible.add(index); - } - setVisibleValues(newVisible); - }; - - const switchToJsonMode = () => { - const jsonObject: Record = {}; - headers.forEach((header) => { - if (header.enabled && header.name.trim() && header.value.trim()) { - jsonObject[header.name.trim()] = header.value.trim(); - } - }); - setJsonValue(JSON.stringify(jsonObject, null, 2)); - setJsonError(null); - setIsJsonMode(true); - }; - - const switchToFormMode = () => { - try { - const parsed = JSON.parse(jsonValue); - if ( - typeof parsed !== "object" || - parsed === null || - Array.isArray(parsed) - ) { - setJsonError("JSON must be an object with string key-value pairs"); - return; - } - - const newHeaders: CustomHeadersType = Object.entries(parsed).map( - ([name, value]) => ({ - name, - value: String(value), - enabled: true, - }), - ); - - onChange(newHeaders); - setJsonError(null); - setIsJsonMode(false); - } catch { - setJsonError("Invalid JSON format"); - } - }; - - const handleJsonChange = (value: string) => { - setJsonValue(value); - setJsonError(null); - }; - - if (isJsonMode) { - return ( -
-
-

- Custom Headers (JSON) -

- -
-
-