[
  {
    "path": ".dockerignore",
    "content": "# Version control\n.git\n.gitignore\n\n# Node.js\nnode_modules\nnpm-debug.log\n\n# Build artifacts\nclient/dist\nclient/build\nserver/dist\nserver/build\n\n# Environment variables\n.env\n.env.local\n.env.development\n.env.test\n.env.production\n\n# Editor files\n.vscode\n.idea\n\n# Logs\nlogs\n*.log\n\n# Testing\ncoverage\n\n# Docker\nDockerfile\n.dockerignore"
  },
  {
    "path": ".git-blame-ignore-revs",
    "content": ""
  },
  {
    "path": ".gitattributes",
    "content": "package-lock.json linguist-generated=true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"\"\nlabels: \"\"\nassignees: \"\"\n---\n\n**Inspector Version**\n\n- [e.g. 0.16.5)\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Environment (please complete the following information):**\n\n- OS: [e.g. iOS]\n- Browser [e.g. chrome, safari]\n\n**Additional context**\nAdd any other context about the problem here.\n\n**Version Consideration**\n\nInspector V2 is under development to address architectural and UX improvements. During this time, V1 contributions should focus on **bug fixes and MCP spec compliance**. See [CONTRIBUTING.md](../../CONTRIBUTING.md) for more details.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "## Summary\n\n<!-- Provide a brief description of what this PR does -->\n\n> **Note:** Inspector V2 is under development to address architectural and UX improvements. During this time, V1 contributions should focus on **bug fixes and MCP spec compliance**. See [CONTRIBUTING.md](../CONTRIBUTING.md) for more details.\n\n## Type of Change\n\n<!-- Mark the relevant option with an \"x\" -->\n\n- [ ] Bug fix (non-breaking change that fixes an issue)\n- [ ] New feature (non-breaking change that adds functionality)\n- [ ] Documentation update\n- [ ] Refactoring (no functional changes)\n- [ ] Test updates\n- [ ] Build/CI improvements\n\n## Changes Made\n\n<!-- Describe the changes in detail. Include screenshots/recordings if applicable -->\n\n## Related Issues\n\n<!-- Link to related issues using #issue_number or \"Fixes #issue_number\" -->\n\n## Testing\n\n<!-- Describe how you tested these changes, where applicable -->\n\n- [ ] Tested in UI mode\n- [ ] Tested in CLI mode\n- [ ] Tested with STDIO transport\n- [ ] Tested with SSE transport\n- [ ] Tested with Streamable HTTP transport\n- [ ] Added/updated automated tests\n- [ ] Manual testing performed\n\n### Test Results and/or Instructions\n\n<!-- Provide steps for reviewers to test your changes -->\n\nScreenshots are encouraged to share your testing results for this change.\n\n## Checklist\n\n- [ ] Code follows the style guidelines (ran `npm run prettier-fix`)\n- [ ] Self-review completed\n- [ ] Code is commented where necessary\n- [ ] Documentation updated (README, comments, etc.)\n\n## Breaking Changes\n\n<!-- If this is a breaking change, describe the impact and migration path -->\n\n## Additional Context\n\n<!-- Add any other context, screenshots, or information about the PR here -->\n"
  },
  {
    "path": ".github/workflows/claude.yml",
    "content": "name: Claude Code\n\non:\n  issue_comment:\n    types: [created]\n  pull_request_review_comment:\n    types: [created]\n  issues:\n    types: [opened, assigned]\n  pull_request_review:\n    types: [submitted]\n\njobs:\n  claude:\n    if: |\n      (\n        (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||\n        (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||\n        (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||\n        (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))\n      )\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pull-requests: read\n      issues: read\n      id-token: write\n      actions: read\n    steps:\n      - name: Get PR details\n        if: |\n          (github.event_name == 'issue_comment' && github.event.issue.pull_request) ||\n          github.event_name == 'pull_request_review_comment' ||\n          github.event_name == 'pull_request_review'\n        id: pr\n        uses: actions/github-script@v8\n        with:\n          script: |\n            let prNumber;\n            if (context.eventName === 'issue_comment') {\n              prNumber = context.issue.number;\n            } else {\n              prNumber = context.payload.pull_request.number;\n            }\n\n            const pr = await github.rest.pulls.get({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              pull_number: prNumber\n            });\n\n            core.setOutput('sha', pr.data.head.sha);\n            core.setOutput('repo', pr.data.head.repo.full_name);\n\n      - name: Checkout PR branch\n        if: steps.pr.outcome == 'success'\n        uses: actions/checkout@v6\n        with:\n          ref: ${{ steps.pr.outputs.sha }}\n          repository: ${{ steps.pr.outputs.repo }}\n          fetch-depth: 0\n\n      - name: Checkout repository\n        if: steps.pr.outcome != 'success'\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Run Claude Code\n        id: claude\n        uses: anthropics/claude-code-action@v1\n        with:\n          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}\n\n          # Allow Claude to read CI results on PRs\n          additional_permissions: |\n            actions: read\n\n          # Trigger when assigned to an issue\n          assignee_trigger: \"claude\"\n\n          claude_args: |\n            --mcp-config .mcp.json\n            --allowedTools \"Bash,mcp__mcp-docs\"\n            --append-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 <details> block. When working on MCP-related code or reviewing MCP-related changes, use the mcp-docs MCP server to look up the latest protocol documentation. For schema details, reference https://github.com/modelcontextprotocol/modelcontextprotocol/tree/main/schema which contains versioned schemas in JSON (schema.json) and TypeScript (schema.ts) formats.\"\n"
  },
  {
    "path": ".github/workflows/cli_tests.yml",
    "content": "name: CLI Tests\n\non:\n  push:\n    paths:\n      - \"cli/**\"\n  pull_request:\n    paths:\n      - \"cli/**\"\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: ./cli\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Set up Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version-file: package.json\n          cache: npm\n\n      - name: Install dependencies\n        run: |\n          cd ..\n          npm ci --ignore-scripts\n\n      - name: Build CLI\n        run: npm run build\n\n      - name: Run tests\n        run: npm test\n        env:\n          NPM_CONFIG_YES: true\n          CI: true\n"
  },
  {
    "path": ".github/workflows/e2e_tests.yml",
    "content": "name: Playwright Tests\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  test:\n    # Installing Playwright dependencies can take quite awhile, and also depends on GitHub CI load.\n    timeout-minutes: 15\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Install dependencies\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y libwoff1\n\n      - uses: actions/checkout@v6\n\n      - uses: actions/setup-node@v6\n        id: setup_node\n        with:\n          node-version-file: package.json\n          cache: npm\n\n      # Cache Playwright browsers\n      - name: Cache Playwright browsers\n        id: cache-playwright\n        uses: actions/cache@v5\n        with:\n          path: ~/.cache/ms-playwright # The default Playwright cache path\n          key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }} # Cache key based on OS and package-lock.json\n          restore-keys: |\n            ${{ runner.os }}-playwright-\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Install Playwright dependencies\n        run: npx playwright install-deps\n\n      - name: Install Playwright and browsers unless cached\n        run: npx playwright install --with-deps\n        if: steps.cache-playwright.outputs.cache-hit != 'true'\n\n      - name: Run Playwright tests\n        id: playwright-tests\n        run: npm run test:e2e\n\n      - name: Upload Playwright Report and Screenshots\n        uses: actions/upload-artifact@v6\n        if: steps.playwright-tests.conclusion != 'skipped'\n        with:\n          name: playwright-report\n          path: |\n            client/playwright-report/\n            client/test-results/\n            client/results.json\n          retention-days: 2\n\n      - name: Publish Playwright Test Summary\n        uses: daun/playwright-report-summary@v3\n        if: steps.playwright-tests.conclusion != 'skipped'\n        with:\n          create-comment: ${{ github.event.pull_request.head.repo.full_name == github.repository }}\n          report-file: client/results.json\n          comment-title: \"🎭 Playwright E2E Test Results\"\n          job-summary: true\n          icon-style: \"emojis\"\n          custom-info: |\n            **Test Environment:** Ubuntu Latest, Node.js ${{ steps.setup_node.outputs.node-version }}\n            **Browsers:** Chromium, Firefox\n\n            📊 [View Detailed HTML Report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) (download artifacts)\n          test-command: \"npm run test:e2e\"\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "on:\n  push:\n    branches:\n      - main\n\n  pull_request:\n  release:\n    types: [published]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Check formatting\n        run: npx prettier --check .\n\n      - uses: actions/setup-node@v6\n        with:\n          node-version-file: package.json\n          cache: npm\n\n      # Working around https://github.com/npm/cli/issues/4828\n      # - run: npm ci\n      - run: npm install --no-package-lock\n\n      - name: Check version consistency\n        run: npm run check-version\n\n      - name: Check linting\n        working-directory: ./client\n        run: npm run lint\n\n      - name: Run client tests\n        working-directory: ./client\n        run: npm test\n\n      - run: npm run build\n\n  publish:\n    runs-on: ubuntu-latest\n    if: github.event_name == 'release'\n    environment: release\n    needs: build\n\n    permissions:\n      contents: read\n      id-token: write\n\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-node@v6\n        with:\n          node-version-file: package.json\n          cache: npm\n          registry-url: \"https://registry.npmjs.org\"\n\n      # Working around https://github.com/npm/cli/issues/4828\n      # - run: npm ci\n      - run: npm install --no-package-lock\n\n      # TODO: Add --provenance once the repo is public\n      - run: npm run publish-all\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n\n  publish-github-container-registry:\n    runs-on: ubuntu-latest\n    if: github.event_name == 'release'\n    environment: release\n    needs: build\n    permissions:\n      contents: read\n      packages: write\n      attestations: write\n      id-token: write\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Log in to the Container registry\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract metadata (tags, labels) for Docker\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: ghcr.io/${{ github.repository }}\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Build and push Docker image\n        id: push\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          push: true\n          platforms: linux/amd64,linux/arm64\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n\n      - name: Generate artifact attestation\n        uses: actions/attest-build-provenance@v3\n        with:\n          subject-name: ghcr.io/${{ github.repository }}\n          subject-digest: ${{ steps.push.outputs.digest }}\n          push-to-registry: true\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n.vscode\n.idea\nnode_modules/\n*-workspace/\nserver/build\nclient/dist\nclient/tsconfig.app.tsbuildinfo\nclient/tsconfig.node.tsbuildinfo\ncli/build\ntest-output\ntool-test-output\nmetadata-test-output\n# symlinked by `npm run link:sdk`:\nsdk\nclient/playwright-report/\nclient/results.json\nclient/test-results/\nclient/e2e/test-results/\nmcp.json\n.claude/settings.local.json\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "npx lint-staged\ngit update-index --again\n"
  },
  {
    "path": ".mcp.json",
    "content": "{\n  \"mcpServers\": {\n    \"mcp-docs\": {\n      \"type\": \"http\",\n      \"url\": \"https://modelcontextprotocol.io/mcp\"\n    }\n  }\n}\n"
  },
  {
    "path": ".node-version",
    "content": "22.x.x\n"
  },
  {
    "path": ".npmrc",
    "content": "registry=\"https://registry.npmjs.org/\"\n@modelcontextprotocol:registry=\"https://registry.npmjs.org/\"\n"
  },
  {
    "path": ".prettierignore",
    "content": "packages\nserver/build\nCODE_OF_CONDUCT.md\nSECURITY.md\nmcp.json\n.claude/settings.local.json"
  },
  {
    "path": ".prettierrc",
    "content": ""
  },
  {
    "path": "AGENTS.md",
    "content": "# MCP Inspector Development Guide\n\n> **Note:** Inspector V2 is under development to address architectural and UX improvements. During this time, V1 contributions should focus on **bug fixes and MCP spec compliance**. See [CONTRIBUTING.md](CONTRIBUTING.md) for more details.\n\n## Build Commands\n\n- Build all: `npm run build`\n- Build client: `npm run build-client`\n- Build server: `npm run build-server`\n- Development mode: `npm run dev` (use `npm run dev:windows` on Windows)\n- Format code: `npm run prettier-fix`\n- Client lint: `cd client && npm run lint`\n\n## Code Style Guidelines\n\n- Use TypeScript with proper type annotations\n- Follow React functional component patterns with hooks\n- Use ES modules (import/export) not CommonJS\n- Use Prettier for formatting (auto-formatted on commit)\n- Follow existing naming conventions:\n  - camelCase for variables and functions\n  - PascalCase for component names and types\n  - kebab-case for file names\n- Use async/await for asynchronous operations\n- Implement proper error handling with try/catch blocks\n- Use Tailwind CSS for styling in the client\n- Keep components small and focused on a single responsibility\n\n## Project Organization\n\nThe project is organized as a monorepo with workspaces:\n\n- `client/`: React frontend with Vite, TypeScript and Tailwind\n- `server/`: Express backend with TypeScript\n- `cli/`: Command-line interface for testing and invoking MCP server methods directly\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "@./AGENTS.md\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\nmcp-coc@anthropic.com.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Model Context Protocol Inspector\n\nThanks for your interest in contributing! This guide explains how to get involved.\n\n## Getting Started\n\n1. Fork the repository and clone it locally\n2. Install dependencies with `npm install`\n3. Run `npm run dev` to start both client and server in development mode\n4. Use the web UI at http://localhost:6274 to interact with the inspector\n\n## Inspector V2 Development\n\nWe'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).\n\n**Current version (V1) contribution scope:**\n\n- Bug fixes and MCP spec compliance are actively maintained\n- Documentation updates are always appreciated\n- Major changes will be directed to V2 development\n\n## Development Process & Pull Requests\n\n1. Create a new branch for your changes\n2. Make your changes following existing code style and conventions. You can run `npm run prettier-check` and `npm run prettier-fix` as applicable.\n3. Test changes locally by running `npm test` and `npm run test:e2e`\n4. Update documentation as needed\n5. Use clear commit messages explaining your changes\n6. Verify all changes work as expected\n7. Submit a pull request\n8. PRs will be reviewed by maintainers\n\n## Code of Conduct\n\nThis project follows our [Code of Conduct](CODE_OF_CONDUCT.md). Please read it before contributing.\n\n## Security\n\nIf you find a security vulnerability, please refer to our [Security Policy](SECURITY.md) for reporting instructions.\n\n## Questions?\n\nFeel free to [open an issue](https://github.com/modelcontextprotocol/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.\n\n## License\n\nBy contributing, you agree that your contributions will be licensed under the MIT license.\n"
  },
  {
    "path": "Dockerfile",
    "content": "# Build stage\nFROM node:current-alpine3.22 AS builder\n\n# Set working directory\nWORKDIR /app\n\n# Copy package files for installation\nCOPY package*.json ./\nCOPY .npmrc ./\nCOPY client/package*.json ./client/\nCOPY server/package*.json ./server/\nCOPY cli/package*.json ./cli/\n\n# Install dependencies\nRUN npm ci --ignore-scripts\n\n# Copy source files\nCOPY . .\n\n# Build the application\nRUN npm run build\n\n# Production stage\nFROM node:24-slim\n\nWORKDIR /app\n\n# Copy package files for production\nCOPY package*.json ./\nCOPY .npmrc ./\nCOPY client/package*.json ./client/\nCOPY server/package*.json ./server/\nCOPY cli/package*.json ./cli/\n\n# Install only production dependencies\nRUN npm ci --omit=dev --ignore-scripts\n\n# Copy built files from builder stage\nCOPY --from=builder /app/client/dist ./client/dist\nCOPY --from=builder /app/client/bin ./client/bin\nCOPY --from=builder /app/server/build ./server/build\nCOPY --from=builder /app/cli/build ./cli/build\n\n# Set default port values as environment variables\nENV CLIENT_PORT=6274\nENV SERVER_PORT=6277\n\n# Document which ports the application uses internally\nEXPOSE ${CLIENT_PORT} ${SERVER_PORT}\n\n# Use ENTRYPOINT with CMD for arguments\nENTRYPOINT [\"npm\", \"start\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 (\"Apache-2.0\"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.\n\nContributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License.\n\nNo rights beyond those granted by the applicable original license are conveyed for such contributions.\n\n---\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to the Licensor for inclusion in the Work by the copyright\n      owner or by an individual or Legal Entity authorized to submit on behalf\n      of the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n---\n\nMIT License\n\nCopyright (c) 2024-2025 Model Context Protocol a Series of LF Projects, LLC.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n---\n\nCreative Commons Attribution 4.0 International (CC-BY-4.0)\n\nDocumentation in this project (excluding specifications) is licensed under\nCC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for\nthe full license text.\n"
  },
  {
    "path": "README.md",
    "content": "# MCP Inspector\n\nThe MCP inspector is a developer tool for testing and debugging MCP servers.\n\n![MCP Inspector Screenshot](https://raw.githubusercontent.com/modelcontextprotocol/inspector/main/mcp-inspector.png)\n\n## Architecture Overview\n\nThe MCP Inspector consists of two main components that work together:\n\n- **MCP Inspector Client (MCPI)**: A React-based web UI that provides an interactive interface for testing and debugging MCP servers\n- **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)\n\nNote 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.\n\n## Running the Inspector\n\n### Requirements\n\n- Node.js: ^22.7.5\n\n### Quick Start (UI mode)\n\nTo get up and running right away with the UI, just execute the following:\n\n```bash\nnpx @modelcontextprotocol/inspector\n```\n\nThe server will start up and the UI will be accessible at `http://localhost:6274`.\n\n### Docker Container\n\nYou can also start it in a Docker container with the following command:\n\n```bash\ndocker run --rm \\\n  -p 127.0.0.1:6274:6274 \\\n  -p 127.0.0.1:6277:6277 \\\n  -e HOST=0.0.0.0 \\\n  -e MCP_AUTO_OPEN_ENABLED=false \\\n  ghcr.io/modelcontextprotocol/inspector:latest\n```\n\n### From an MCP server repository\n\nTo 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`:\n\n```bash\nnpx @modelcontextprotocol/inspector node build/index.js\n```\n\nYou 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:\n\n```bash\n# Pass arguments only\nnpx @modelcontextprotocol/inspector node build/index.js arg1 arg2\n\n# Pass environment variables only\nnpx @modelcontextprotocol/inspector -e key=value -e key2=$VALUE2 node build/index.js\n\n# Pass both environment variables and arguments\nnpx @modelcontextprotocol/inspector -e key=value -e key2=$VALUE2 node build/index.js arg1 arg2\n\n# Use -- to separate inspector flags from server arguments\nnpx @modelcontextprotocol/inspector -e key=$VALUE -- node build/index.js -e server-flag\n```\n\nThe 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:\n\n```bash\nCLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector node build/index.js\n```\n\nFor 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).\n\n### Servers File Export\n\nThe 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`.\n\n- **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.\n\n  **STDIO transport example:**\n\n  ```json\n  {\n    \"command\": \"node\",\n    \"args\": [\"build/index.js\", \"--debug\"],\n    \"env\": {\n      \"API_KEY\": \"your-api-key\",\n      \"DEBUG\": \"true\"\n    }\n  }\n  ```\n\n  **SSE transport example:**\n\n  ```json\n  {\n    \"type\": \"sse\",\n    \"url\": \"http://localhost:3000/events\",\n    \"note\": \"For SSE connections, add this URL directly in Client\"\n  }\n  ```\n\n  **Streamable HTTP transport example:**\n\n  ```json\n  {\n    \"type\": \"streamable-http\",\n    \"url\": \"http://localhost:3000/mcp\",\n    \"note\": \"For Streamable HTTP connections, add this URL directly in your MCP Client\"\n  }\n  ```\n\n- **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`.\n\n  **STDIO transport example:**\n\n  ```json\n  {\n    \"mcpServers\": {\n      \"default-server\": {\n        \"command\": \"node\",\n        \"args\": [\"build/index.js\", \"--debug\"],\n        \"env\": {\n          \"API_KEY\": \"your-api-key\",\n          \"DEBUG\": \"true\"\n        }\n      }\n    }\n  }\n  ```\n\n  **SSE transport example:**\n\n  ```json\n  {\n    \"mcpServers\": {\n      \"default-server\": {\n        \"type\": \"sse\",\n        \"url\": \"http://localhost:3000/events\",\n        \"note\": \"For SSE connections, add this URL directly in Client\"\n      }\n    }\n  }\n  ```\n\n  **Streamable HTTP transport example:**\n\n  ```json\n  {\n    \"mcpServers\": {\n      \"default-server\": {\n        \"type\": \"streamable-http\",\n        \"url\": \"http://localhost:3000/mcp\",\n        \"note\": \"For Streamable HTTP connections, add this URL directly in your MCP Client\"\n      }\n    }\n  }\n  ```\n\nThese buttons appear in the Inspector UI after you've configured your server settings, making it easy to save and reuse your configurations.\n\nFor 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.\n\nYou 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.\n\n### Authentication\n\nThe 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.\n\n### Security Considerations\n\nThe 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.\n\n#### Authentication\n\nThe MCP Inspector proxy server requires authentication by default. When starting the server, a random session token is generated and printed to the console:\n\n```\n🔑 Session token: 3a1c267fad21f7150b7d624c160b7f09b0b8c4f623c7107bbf13378f051538d4\n\n🔗 Open inspector with token pre-filled:\n   http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=3a1c267fad21f7150b7d624c160b7f09b0b8c4f623c7107bbf13378f051538d4\n```\n\nThis 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.\n\n**Automatic browser opening** - The inspector now automatically opens your browser with the token pre-filled in the URL when authentication is enabled.\n\n**Alternative: Manual configuration** - If you already have the inspector open:\n\n1. Click the \"Configuration\" button in the sidebar\n2. Find \"Proxy Session Token\" and enter the token displayed in the proxy console\n3. Click \"Save\" to apply the configuration\n\nThe token will be saved in your browser's local storage for future use.\n\nIf you need to disable authentication (NOT RECOMMENDED), you can set the `DANGEROUSLY_OMIT_AUTH` environment variable:\n\n```bash\nDANGEROUSLY_OMIT_AUTH=true npm start\n```\n\n---\n\n**🚨 WARNING 🚨**\n\nDisabling 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.\n\nRead 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)\n\n---\n\nYou can also set the token via the `MCP_PROXY_AUTH_TOKEN` environment variable when starting the server:\n\n```bash\nMCP_PROXY_AUTH_TOKEN=$(openssl rand -hex 32) npm start\n```\n\n#### Local-only Binding\n\nBy 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:\n\n```bash\nHOST=0.0.0.0 npm start\n```\n\n**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.\n\n#### DNS Rebinding Protection\n\nTo 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):\n\n```bash\nALLOWED_ORIGINS=http://localhost:6274,http://localhost:8000 npm start\n```\n\n### Configuration\n\nThe MCP Inspector supports the following configuration settings. To change them, click on the `Configuration` button in the MCP Inspector UI:\n\n| Setting                                 | Description                                                                                                                                         | Default |\n| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |\n| `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  |\n| `MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS` | Reset timeout on progress notifications                                                                                                             | true    |\n| `MCP_REQUEST_MAX_TOTAL_TIMEOUT`         | Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)                                                    | 60000   |\n| `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                                        | \"\"      |\n| `MCP_AUTO_OPEN_ENABLED`                 | Enable automatic browser opening when inspector starts (works with authentication enabled). Only as environment var, not configurable in browser.   | true    |\n\n**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.\n\nThese settings can be adjusted in real-time through the UI and will persist across sessions.\n\nThe inspector also supports configuration files to store settings for different MCP servers. This is useful when working with multiple servers or complex configurations:\n\n```bash\nnpx @modelcontextprotocol/inspector --config path/to/config.json --server everything\n```\n\nExample server configuration file:\n\n```json\n{\n  \"mcpServers\": {\n    \"everything\": {\n      \"command\": \"npx\",\n      \"args\": [\"@modelcontextprotocol/server-everything\"],\n      \"env\": {\n        \"hello\": \"Hello MCP!\"\n      }\n    },\n    \"my-server\": {\n      \"command\": \"node\",\n      \"args\": [\"build/index.js\", \"arg1\", \"arg2\"],\n      \"env\": {\n        \"key\": \"value\",\n        \"key2\": \"value2\"\n      }\n    }\n  }\n}\n```\n\n#### Transport Types in Config Files\n\nThe inspector automatically detects the transport type from your config file. You can specify different transport types:\n\n**STDIO (default):**\n\n```json\n{\n  \"mcpServers\": {\n    \"my-stdio-server\": {\n      \"type\": \"stdio\",\n      \"command\": \"npx\",\n      \"args\": [\"@modelcontextprotocol/server-everything\"]\n    }\n  }\n}\n```\n\n**SSE (Server-Sent Events):**\n\n```json\n{\n  \"mcpServers\": {\n    \"my-sse-server\": {\n      \"type\": \"sse\",\n      \"url\": \"http://localhost:3000/sse\"\n    }\n  }\n}\n```\n\n**Streamable HTTP:**\n\n```json\n{\n  \"mcpServers\": {\n    \"my-http-server\": {\n      \"type\": \"streamable-http\",\n      \"url\": \"http://localhost:3000/mcp\"\n    }\n  }\n}\n```\n\n#### Default Server Selection\n\nYou can launch the inspector without specifying a server name if your config has:\n\n1. **A single server** - automatically selected:\n\n```bash\n# Automatically uses \"my-server\" if it's the only one\nnpx @modelcontextprotocol/inspector --config mcp.json\n```\n\n2. **A server named \"default-server\"** - automatically selected:\n\n```json\n{\n  \"mcpServers\": {\n    \"default-server\": {\n      \"command\": \"npx\",\n      \"args\": [\"@modelcontextprotocol/server-everything\"]\n    },\n    \"other-server\": {\n      \"command\": \"node\",\n      \"args\": [\"other.js\"]\n    }\n  }\n}\n```\n\n> **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.\n\nYou can also set the initial `transport` type, `serverUrl`, `serverCommand`, and `serverArgs` via query params, for example:\n\n```\nhttp://localhost:6274/?transport=sse&serverUrl=http://localhost:8787/sse\nhttp://localhost:6274/?transport=streamable-http&serverUrl=http://localhost:8787/mcp\nhttp://localhost:6274/?transport=stdio&serverCommand=npx&serverArgs=arg1%20arg2\n```\n\nYou can also set initial config settings via query params, for example:\n\n```\nhttp://localhost:6274/?MCP_SERVER_REQUEST_TIMEOUT=60000&MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS=false&MCP_PROXY_FULL_ADDRESS=http://10.1.1.22:5577\n```\n\nNote that if both the query param and the corresponding localStorage item are set, the query param will take precedence.\n\n### From this repository\n\nIf you're working on the inspector itself:\n\nDevelopment mode:\n\n```bash\nnpm run dev\n\n# To co-develop with the typescript-sdk package (assuming it's cloned in ../typescript-sdk; set MCP_SDK otherwise):\nnpm run dev:sdk \"cd sdk && npm run examples:simple-server:w\"\n# then open http://localhost:3000/mcp as SHTTP in the inspector.\n# To go back to the deployed SDK version:\n#   npm run unlink:sdk && npm i\n```\n\n> **Note for Windows users:**\n> On Windows, use the following command instead:\n>\n> ```bash\n> npm run dev:windows\n> ```\n\nProduction mode:\n\n```bash\nnpm run build\nnpm start\n```\n\n### CLI Mode\n\nCLI 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.\n\n```bash\nnpx @modelcontextprotocol/inspector --cli node build/index.js\n```\n\nThe CLI mode supports most operations across tools, resources, and prompts. A few examples:\n\n```bash\n# Basic usage\nnpx @modelcontextprotocol/inspector --cli node build/index.js\n\n# With config file\nnpx @modelcontextprotocol/inspector --cli --config path/to/config.json --server myserver\n\n# List available tools\nnpx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list\n\n# Call a specific tool\nnpx @modelcontextprotocol/inspector --cli node build/index.js --method tools/call --tool-name mytool --tool-arg key=value --tool-arg another=value2\n\n# Call a tool with JSON arguments\nnpx @modelcontextprotocol/inspector --cli node build/index.js --method tools/call --tool-name mytool --tool-arg 'options={\"format\": \"json\", \"max_tokens\": 100}'\n\n# List available resources\nnpx @modelcontextprotocol/inspector --cli node build/index.js --method resources/list\n\n# List available prompts\nnpx @modelcontextprotocol/inspector --cli node build/index.js --method prompts/list\n\n# Connect to a remote MCP server (default is SSE transport)\nnpx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com\n\n# Connect to a remote MCP server (with Streamable HTTP transport)\nnpx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --transport http --method tools/list\n\n# Connect to a remote MCP server (with custom headers)\nnpx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --transport http --method tools/list --header \"X-API-Key: your-api-key\"\n\n# Call a tool on a remote server\nnpx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --method tools/call --tool-name remotetool --tool-arg param=value\n\n# List resources from a remote server\nnpx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --method resources/list\n```\n\n### UI Mode vs CLI Mode: When to Use Each\n\n| Use Case                 | UI Mode                                                                   | CLI Mode                                                                                                                                             |\n| ------------------------ | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **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 |\n| **Resource Exploration** | Interactive browser with hierarchical navigation and JSON visualization   | Programmatic listing and reading for automation and scripting                                                                                        |\n| **Tool Testing**         | Form-based parameter input with real-time response visualization          | Command-line tool execution with JSON output for scripting                                                                                           |\n| **Prompt Engineering**   | Interactive sampling with streaming responses and visual comparison       | Batch processing of prompts with machine-readable output                                                                                             |\n| **Debugging**            | Request history, visualized errors, and real-time notifications           | Direct JSON output for log analysis and integration with other tools                                                                                 |\n| **Automation**           | N/A                                                                       | Ideal for CI/CD pipelines, batch processing, and integration with coding assistants                                                                  |\n| **Learning MCP**         | Rich visual interface helps new users understand server capabilities      | Simplified commands for focused learning of specific endpoints                                                                                       |\n\n## Tool Input Validation Guidelines\n\nWhen implementing or modifying tool input parameter handling in the Inspector:\n\n- **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\n- **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\n- **Always include required fields** - Preserve required field values even when empty, allowing the MCP server to validate and return appropriate error messages\n- **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\n\nThese guidelines maintain clean parameter passing and proper separation of concerns between the Inspector client and MCP servers.\n\n## License\n\nThis project is licensed under the MIT License—see the [LICENSE](LICENSE) file for details.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\nThank you for helping keep the Model Context Protocol and its ecosystem secure.\n\n## Reporting Security Issues\n\nIf you discover a security vulnerability in this repository, please report it through\nthe [GitHub Security Advisory process](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability)\nfor this repository.\n\nPlease **do not** report security vulnerabilities through public GitHub issues, discussions,\nor pull requests.\n\n## What to Include\n\nTo help us triage and respond quickly, please include:\n\n- A description of the vulnerability\n- Steps to reproduce the issue\n- The potential impact\n- Any suggested fixes (optional)\n"
  },
  {
    "path": "cli/LICENSE",
    "content": "The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 (\"Apache-2.0\"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.\n\nContributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License.\n\nNo rights beyond those granted by the applicable original license are conveyed for such contributions.\n\n---\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to the Licensor for inclusion in the Work by the copyright\n      owner or by an individual or Legal Entity authorized to submit on behalf\n      of the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n---\n\nMIT License\n\nCopyright (c) 2024-2025 Model Context Protocol a Series of LF Projects, LLC.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n---\n\nCreative Commons Attribution 4.0 International (CC-BY-4.0)\n\nDocumentation in this project (excluding specifications) is licensed under\nCC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for\nthe full license text.\n"
  },
  {
    "path": "cli/__tests__/README.md",
    "content": "# CLI Tests\n\n## Running Tests\n\n```bash\n# Run all tests\nnpm test\n\n# Run in watch mode (useful for test file changes; won't work on CLI source changes without rebuild)\nnpm run test:watch\n\n# Run specific test file\nnpm run test:cli          # cli.test.ts\nnpm run test:cli-tools   # tools.test.ts\nnpm run test:cli-headers # headers.test.ts\nnpm run test:cli-metadata # metadata.test.ts\n```\n\n## Test Files\n\n- `cli.test.ts` - Basic CLI functionality: CLI mode, environment variables, config files, resources, prompts, logging, transport types\n- `tools.test.ts` - Tool-related tests: Tool discovery, JSON argument parsing, error handling, prompts\n- `headers.test.ts` - Header parsing and validation\n- `metadata.test.ts` - Metadata functionality: General metadata, tool-specific metadata, parsing, merging, validation\n\n## Helpers\n\nThe `helpers/` directory contains shared utilities:\n\n- `cli-runner.ts` - Spawns CLI as subprocess and captures output\n- `test-mcp-server.ts` - Standalone stdio MCP server script for stdio transport testing\n- `instrumented-server.ts` - In-process MCP test server for HTTP/SSE transports with request recording\n- `assertions.ts` - Custom assertion helpers for CLI output validation\n- `fixtures.ts` - Test config file generators and temporary directory management\n\n## Notes\n\n- Tests run in parallel across files (Vitest default)\n- Tests within a file run sequentially (we have isolated config files and ports, so we could get more aggressive if desired)\n- Config files use `crypto.randomUUID()` for uniqueness in parallel execution\n- HTTP/SSE servers use dynamic port allocation to avoid conflicts\n- Coverage is not used because much of the code that we want to measure is run by a spawned process, so it can't be tracked by Vitest\n- /sample-config.json is no longer used by tests - not clear if this file serves some other purpose so leaving it for now\n- All tests now use built-in MCP test servers, there are no external dependencies on servers from a registry\n"
  },
  {
    "path": "cli/__tests__/cli.test.ts",
    "content": "import { describe, it, beforeAll, afterAll, expect } from \"vitest\";\nimport { runCli } from \"./helpers/cli-runner.js\";\nimport {\n  expectCliSuccess,\n  expectCliFailure,\n  expectValidJson,\n} from \"./helpers/assertions.js\";\nimport {\n  NO_SERVER_SENTINEL,\n  createSampleTestConfig,\n  createTestConfig,\n  createInvalidConfig,\n  deleteConfigFile,\n} from \"./helpers/fixtures.js\";\nimport { getTestMcpServerCommand } from \"./helpers/test-server-stdio.js\";\nimport { createTestServerHttp } from \"./helpers/test-server-http.js\";\nimport {\n  createEchoTool,\n  createTestServerInfo,\n} from \"./helpers/test-fixtures.js\";\n\ndescribe(\"CLI Tests\", () => {\n  describe(\"Basic CLI Mode\", () => {\n    it(\"should execute tools/list successfully\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"tools/list\",\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"tools\");\n      expect(Array.isArray(json.tools)).toBe(true);\n\n      // Validate expected tools from test-mcp-server\n      const toolNames = json.tools.map((tool: any) => tool.name);\n      expect(toolNames).toContain(\"echo\");\n      expect(toolNames).toContain(\"get-sum\");\n      expect(toolNames).toContain(\"get-annotated-message\");\n    });\n\n    it(\"should fail with nonexistent method\", async () => {\n      const result = await runCli([\n        NO_SERVER_SENTINEL,\n        \"--cli\",\n        \"--method\",\n        \"nonexistent/method\",\n      ]);\n\n      expectCliFailure(result);\n    });\n\n    it(\"should fail without method\", async () => {\n      const result = await runCli([NO_SERVER_SENTINEL, \"--cli\"]);\n\n      expectCliFailure(result);\n    });\n  });\n\n  describe(\"Environment Variables\", () => {\n    it(\"should accept environment variables\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"-e\",\n        \"KEY1=value1\",\n        \"-e\",\n        \"KEY2=value2\",\n        \"--cli\",\n        \"--method\",\n        \"resources/read\",\n        \"--uri\",\n        \"test://env\",\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"contents\");\n      expect(Array.isArray(json.contents)).toBe(true);\n      expect(json.contents.length).toBeGreaterThan(0);\n\n      // Parse the env vars from the resource\n      const envVars = JSON.parse(json.contents[0].text);\n      expect(envVars.KEY1).toBe(\"value1\");\n      expect(envVars.KEY2).toBe(\"value2\");\n    });\n\n    it(\"should reject invalid environment variable format\", async () => {\n      const result = await runCli([\n        NO_SERVER_SENTINEL,\n        \"-e\",\n        \"INVALID_FORMAT\",\n        \"--cli\",\n        \"--method\",\n        \"tools/list\",\n      ]);\n\n      expectCliFailure(result);\n    });\n\n    it(\"should handle environment variable with equals sign in value\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"-e\",\n        \"API_KEY=abc123=xyz789==\",\n        \"--cli\",\n        \"--method\",\n        \"resources/read\",\n        \"--uri\",\n        \"test://env\",\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      const envVars = JSON.parse(json.contents[0].text);\n      expect(envVars.API_KEY).toBe(\"abc123=xyz789==\");\n    });\n\n    it(\"should handle environment variable with base64-encoded value\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"-e\",\n        \"JWT_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=\",\n        \"--cli\",\n        \"--method\",\n        \"resources/read\",\n        \"--uri\",\n        \"test://env\",\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      const envVars = JSON.parse(json.contents[0].text);\n      expect(envVars.JWT_TOKEN).toBe(\n        \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=\",\n      );\n    });\n  });\n\n  describe(\"Config File\", () => {\n    it(\"should use config file with CLI mode\", async () => {\n      const configPath = createSampleTestConfig();\n      try {\n        const result = await runCli([\n          \"--config\",\n          configPath,\n          \"--server\",\n          \"test-stdio\",\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n        ]);\n\n        expectCliSuccess(result);\n        const json = expectValidJson(result);\n        expect(json).toHaveProperty(\"tools\");\n        expect(Array.isArray(json.tools)).toBe(true);\n        expect(json.tools.length).toBeGreaterThan(0);\n      } finally {\n        deleteConfigFile(configPath);\n      }\n    });\n\n    it(\"should fail when using config file without server name\", async () => {\n      const configPath = createSampleTestConfig();\n      try {\n        const result = await runCli([\n          \"--config\",\n          configPath,\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n        ]);\n\n        expectCliFailure(result);\n      } finally {\n        deleteConfigFile(configPath);\n      }\n    });\n\n    it(\"should fail when using server name without config file\", async () => {\n      const result = await runCli([\n        \"--server\",\n        \"test-stdio\",\n        \"--cli\",\n        \"--method\",\n        \"tools/list\",\n      ]);\n\n      expectCliFailure(result);\n    });\n\n    it(\"should fail with nonexistent config file\", async () => {\n      const result = await runCli([\n        \"--config\",\n        \"./nonexistent-config.json\",\n        \"--server\",\n        \"test-stdio\",\n        \"--cli\",\n        \"--method\",\n        \"tools/list\",\n      ]);\n\n      expectCliFailure(result);\n    });\n\n    it(\"should fail with invalid config file format\", async () => {\n      // Create invalid config temporarily\n      const invalidConfigPath = createInvalidConfig();\n      try {\n        const result = await runCli([\n          \"--config\",\n          invalidConfigPath,\n          \"--server\",\n          \"test-stdio\",\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n        ]);\n\n        expectCliFailure(result);\n      } finally {\n        deleteConfigFile(invalidConfigPath);\n      }\n    });\n\n    it(\"should fail with nonexistent server in config\", async () => {\n      const configPath = createSampleTestConfig();\n      try {\n        const result = await runCli([\n          \"--config\",\n          configPath,\n          \"--server\",\n          \"nonexistent\",\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n        ]);\n\n        expectCliFailure(result);\n      } finally {\n        deleteConfigFile(configPath);\n      }\n    });\n  });\n\n  describe(\"Resource Options\", () => {\n    it(\"should read resource with URI\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"resources/read\",\n        \"--uri\",\n        \"demo://resource/static/document/architecture.md\",\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"contents\");\n      expect(Array.isArray(json.contents)).toBe(true);\n      expect(json.contents.length).toBeGreaterThan(0);\n      expect(json.contents[0]).toHaveProperty(\n        \"uri\",\n        \"demo://resource/static/document/architecture.md\",\n      );\n      expect(json.contents[0]).toHaveProperty(\"mimeType\", \"text/markdown\");\n      expect(json.contents[0]).toHaveProperty(\"text\");\n      expect(json.contents[0].text).toContain(\"Architecture Documentation\");\n    });\n\n    it(\"should fail when reading resource without URI\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"resources/read\",\n      ]);\n\n      expectCliFailure(result);\n    });\n  });\n\n  describe(\"Prompt Options\", () => {\n    it(\"should get prompt by name\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"prompts/get\",\n        \"--prompt-name\",\n        \"simple-prompt\",\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"messages\");\n      expect(Array.isArray(json.messages)).toBe(true);\n      expect(json.messages.length).toBeGreaterThan(0);\n      expect(json.messages[0]).toHaveProperty(\"role\", \"user\");\n      expect(json.messages[0]).toHaveProperty(\"content\");\n      expect(json.messages[0].content).toHaveProperty(\"type\", \"text\");\n      expect(json.messages[0].content.text).toBe(\n        \"This is a simple prompt for testing purposes.\",\n      );\n    });\n\n    it(\"should get prompt with arguments\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"prompts/get\",\n        \"--prompt-name\",\n        \"args-prompt\",\n        \"--prompt-args\",\n        \"city=New York\",\n        \"state=NY\",\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"messages\");\n      expect(Array.isArray(json.messages)).toBe(true);\n      expect(json.messages.length).toBeGreaterThan(0);\n      expect(json.messages[0]).toHaveProperty(\"role\", \"user\");\n      expect(json.messages[0]).toHaveProperty(\"content\");\n      expect(json.messages[0].content).toHaveProperty(\"type\", \"text\");\n      // Verify that the arguments were actually used in the response\n      expect(json.messages[0].content.text).toContain(\"city=New York\");\n      expect(json.messages[0].content.text).toContain(\"state=NY\");\n    });\n\n    it(\"should fail when getting prompt without name\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"prompts/get\",\n      ]);\n\n      expectCliFailure(result);\n    });\n  });\n\n  describe(\"Logging Options\", () => {\n    it(\"should set log level\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        logging: true,\n      });\n\n      try {\n        const port = await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"logging/setLevel\",\n          \"--log-level\",\n          \"debug\",\n          \"--transport\",\n          \"http\",\n        ]);\n\n        expectCliSuccess(result);\n        // Validate the response - logging/setLevel should return an empty result\n        const json = expectValidJson(result);\n        expect(json).toEqual({});\n\n        // Validate that the server actually received and recorded the log level\n        expect(server.getCurrentLogLevel()).toBe(\"debug\");\n      } finally {\n        await server.stop();\n      }\n    });\n\n    it(\"should reject invalid log level\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"logging/setLevel\",\n        \"--log-level\",\n        \"invalid\",\n      ]);\n\n      expectCliFailure(result);\n    });\n  });\n\n  describe(\"Combined Options\", () => {\n    it(\"should handle config file with environment variables\", async () => {\n      const configPath = createSampleTestConfig();\n      try {\n        const result = await runCli([\n          \"--config\",\n          configPath,\n          \"--server\",\n          \"test-stdio\",\n          \"-e\",\n          \"CLI_ENV_VAR=cli_value\",\n          \"--cli\",\n          \"--method\",\n          \"resources/read\",\n          \"--uri\",\n          \"test://env\",\n        ]);\n\n        expectCliSuccess(result);\n        const json = expectValidJson(result);\n        expect(json).toHaveProperty(\"contents\");\n        expect(Array.isArray(json.contents)).toBe(true);\n        expect(json.contents.length).toBeGreaterThan(0);\n\n        // Parse the env vars from the resource\n        const envVars = JSON.parse(json.contents[0].text);\n        expect(envVars).toHaveProperty(\"CLI_ENV_VAR\");\n        expect(envVars.CLI_ENV_VAR).toBe(\"cli_value\");\n      } finally {\n        deleteConfigFile(configPath);\n      }\n    });\n\n    it(\"should handle all options together\", async () => {\n      const configPath = createSampleTestConfig();\n      try {\n        const result = await runCli([\n          \"--config\",\n          configPath,\n          \"--server\",\n          \"test-stdio\",\n          \"-e\",\n          \"CLI_ENV_VAR=cli_value\",\n          \"--cli\",\n          \"--method\",\n          \"tools/call\",\n          \"--tool-name\",\n          \"echo\",\n          \"--tool-arg\",\n          \"message=Hello\",\n          \"--log-level\",\n          \"debug\",\n        ]);\n\n        expectCliSuccess(result);\n        const json = expectValidJson(result);\n        expect(json).toHaveProperty(\"content\");\n        expect(Array.isArray(json.content)).toBe(true);\n        expect(json.content.length).toBeGreaterThan(0);\n        expect(json.content[0]).toHaveProperty(\"type\", \"text\");\n        expect(json.content[0].text).toBe(\"Echo: Hello\");\n      } finally {\n        deleteConfigFile(configPath);\n      }\n    });\n  });\n\n  describe(\"Config Transport Types\", () => {\n    it(\"should work with stdio transport type\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const configPath = createTestConfig({\n        mcpServers: {\n          \"test-stdio\": {\n            type: \"stdio\",\n            command,\n            args,\n            env: {\n              TEST_ENV: \"test-value\",\n            },\n          },\n        },\n      });\n      try {\n        // First validate tools/list works\n        const toolsResult = await runCli([\n          \"--config\",\n          configPath,\n          \"--server\",\n          \"test-stdio\",\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n        ]);\n\n        expectCliSuccess(toolsResult);\n        const toolsJson = expectValidJson(toolsResult);\n        expect(toolsJson).toHaveProperty(\"tools\");\n        expect(Array.isArray(toolsJson.tools)).toBe(true);\n        expect(toolsJson.tools.length).toBeGreaterThan(0);\n\n        // Then validate env vars from config are passed to server\n        const envResult = await runCli([\n          \"--config\",\n          configPath,\n          \"--server\",\n          \"test-stdio\",\n          \"--cli\",\n          \"--method\",\n          \"resources/read\",\n          \"--uri\",\n          \"test://env\",\n        ]);\n\n        expectCliSuccess(envResult);\n        const envJson = expectValidJson(envResult);\n        const envVars = JSON.parse(envJson.contents[0].text);\n        expect(envVars).toHaveProperty(\"TEST_ENV\");\n        expect(envVars.TEST_ENV).toBe(\"test-value\");\n      } finally {\n        deleteConfigFile(configPath);\n      }\n    });\n\n    it(\"should fail with SSE transport type in CLI mode (connection error)\", async () => {\n      const configPath = createTestConfig({\n        mcpServers: {\n          \"test-sse\": {\n            type: \"sse\",\n            url: \"http://localhost:3000/sse\",\n            note: \"Test SSE server\",\n          },\n        },\n      });\n      try {\n        const result = await runCli([\n          \"--config\",\n          configPath,\n          \"--server\",\n          \"test-sse\",\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n        ]);\n\n        expectCliFailure(result);\n      } finally {\n        deleteConfigFile(configPath);\n      }\n    });\n\n    it(\"should fail with HTTP transport type in CLI mode (connection error)\", async () => {\n      const configPath = createTestConfig({\n        mcpServers: {\n          \"test-http\": {\n            type: \"streamable-http\",\n            url: \"http://localhost:3001/mcp\",\n            note: \"Test HTTP server\",\n          },\n        },\n      });\n      try {\n        const result = await runCli([\n          \"--config\",\n          configPath,\n          \"--server\",\n          \"test-http\",\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n        ]);\n\n        expectCliFailure(result);\n      } finally {\n        deleteConfigFile(configPath);\n      }\n    });\n\n    it(\"should work with legacy config without type field\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const configPath = createTestConfig({\n        mcpServers: {\n          \"test-legacy\": {\n            command,\n            args,\n            env: {\n              LEGACY_ENV: \"legacy-value\",\n            },\n          },\n        },\n      });\n      try {\n        // First validate tools/list works\n        const toolsResult = await runCli([\n          \"--config\",\n          configPath,\n          \"--server\",\n          \"test-legacy\",\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n        ]);\n\n        expectCliSuccess(toolsResult);\n        const toolsJson = expectValidJson(toolsResult);\n        expect(toolsJson).toHaveProperty(\"tools\");\n        expect(Array.isArray(toolsJson.tools)).toBe(true);\n        expect(toolsJson.tools.length).toBeGreaterThan(0);\n\n        // Then validate env vars from config are passed to server\n        const envResult = await runCli([\n          \"--config\",\n          configPath,\n          \"--server\",\n          \"test-legacy\",\n          \"--cli\",\n          \"--method\",\n          \"resources/read\",\n          \"--uri\",\n          \"test://env\",\n        ]);\n\n        expectCliSuccess(envResult);\n        const envJson = expectValidJson(envResult);\n        const envVars = JSON.parse(envJson.contents[0].text);\n        expect(envVars).toHaveProperty(\"LEGACY_ENV\");\n        expect(envVars.LEGACY_ENV).toBe(\"legacy-value\");\n      } finally {\n        deleteConfigFile(configPath);\n      }\n    });\n  });\n\n  describe(\"Default Server Selection\", () => {\n    it(\"should auto-select single server\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const configPath = createTestConfig({\n        mcpServers: {\n          \"only-server\": {\n            command,\n            args,\n          },\n        },\n      });\n      try {\n        const result = await runCli([\n          \"--config\",\n          configPath,\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n        ]);\n\n        expectCliSuccess(result);\n        const json = expectValidJson(result);\n        expect(json).toHaveProperty(\"tools\");\n        expect(Array.isArray(json.tools)).toBe(true);\n        expect(json.tools.length).toBeGreaterThan(0);\n      } finally {\n        deleteConfigFile(configPath);\n      }\n    });\n\n    it(\"should require explicit server selection even with default-server key (multiple servers)\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const configPath = createTestConfig({\n        mcpServers: {\n          \"default-server\": {\n            command,\n            args,\n          },\n          \"other-server\": {\n            command: \"node\",\n            args: [\"other.js\"],\n          },\n        },\n      });\n      try {\n        const result = await runCli([\n          \"--config\",\n          configPath,\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n        ]);\n\n        expectCliFailure(result);\n      } finally {\n        deleteConfigFile(configPath);\n      }\n    });\n\n    it(\"should require explicit server selection with multiple servers\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const configPath = createTestConfig({\n        mcpServers: {\n          server1: {\n            command,\n            args,\n          },\n          server2: {\n            command: \"node\",\n            args: [\"other.js\"],\n          },\n        },\n      });\n      try {\n        const result = await runCli([\n          \"--config\",\n          configPath,\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n        ]);\n\n        expectCliFailure(result);\n      } finally {\n        deleteConfigFile(configPath);\n      }\n    });\n  });\n\n  describe(\"HTTP Transport\", () => {\n    it(\"should infer HTTP transport from URL ending with /mcp\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createEchoTool()],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n        ]);\n\n        expectCliSuccess(result);\n        const json = expectValidJson(result);\n        expect(json).toHaveProperty(\"tools\");\n        expect(Array.isArray(json.tools)).toBe(true);\n        expect(json.tools.length).toBeGreaterThan(0);\n      } finally {\n        await server.stop();\n      }\n    });\n\n    it(\"should work with explicit --transport http flag\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createEchoTool()],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--transport\",\n          \"http\",\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n        ]);\n\n        expectCliSuccess(result);\n        const json = expectValidJson(result);\n        expect(json).toHaveProperty(\"tools\");\n        expect(Array.isArray(json.tools)).toBe(true);\n        expect(json.tools.length).toBeGreaterThan(0);\n      } finally {\n        await server.stop();\n      }\n    });\n\n    it(\"should work with explicit transport flag and URL suffix\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createEchoTool()],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--transport\",\n          \"http\",\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n        ]);\n\n        expectCliSuccess(result);\n        const json = expectValidJson(result);\n        expect(json).toHaveProperty(\"tools\");\n        expect(Array.isArray(json.tools)).toBe(true);\n        expect(json.tools.length).toBeGreaterThan(0);\n      } finally {\n        await server.stop();\n      }\n    });\n\n    it(\"should fail when SSE transport is given to HTTP server\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createEchoTool()],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--transport\",\n          \"sse\",\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n        ]);\n\n        expectCliFailure(result);\n      } finally {\n        await server.stop();\n      }\n    });\n\n    it(\"should fail when HTTP transport is specified without URL\", async () => {\n      const result = await runCli([\n        \"--transport\",\n        \"http\",\n        \"--cli\",\n        \"--method\",\n        \"tools/list\",\n      ]);\n\n      expectCliFailure(result);\n    });\n\n    it(\"should fail when SSE transport is specified without URL\", async () => {\n      const result = await runCli([\n        \"--transport\",\n        \"sse\",\n        \"--cli\",\n        \"--method\",\n        \"tools/list\",\n      ]);\n\n      expectCliFailure(result);\n    });\n  });\n});\n"
  },
  {
    "path": "cli/__tests__/headers.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport { runCli } from \"./helpers/cli-runner.js\";\nimport {\n  expectCliFailure,\n  expectOutputContains,\n  expectCliSuccess,\n} from \"./helpers/assertions.js\";\nimport { createTestServerHttp } from \"./helpers/test-server-http.js\";\nimport {\n  createEchoTool,\n  createTestServerInfo,\n} from \"./helpers/test-fixtures.js\";\n\ndescribe(\"Header Parsing and Validation\", () => {\n  describe(\"Valid Headers\", () => {\n    it(\"should parse valid single header and send it to server\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createEchoTool()],\n      });\n\n      try {\n        const port = await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n          \"--transport\",\n          \"http\",\n          \"--header\",\n          \"Authorization: Bearer token123\",\n        ]);\n\n        expectCliSuccess(result);\n\n        // Check that the server received the request with the correct headers\n        const recordedRequests = server.getRecordedRequests();\n        expect(recordedRequests.length).toBeGreaterThan(0);\n\n        // Find the tools/list request (should be the last one)\n        const toolsListRequest = recordedRequests[recordedRequests.length - 1];\n        expect(toolsListRequest).toBeDefined();\n        expect(toolsListRequest.method).toBe(\"tools/list\");\n\n        // Express normalizes headers to lowercase\n        expect(toolsListRequest.headers).toHaveProperty(\"authorization\");\n        expect(toolsListRequest.headers?.authorization).toBe(\"Bearer token123\");\n      } finally {\n        await server.stop();\n      }\n    });\n\n    it(\"should parse multiple headers\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createEchoTool()],\n      });\n\n      try {\n        const port = await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n          \"--transport\",\n          \"http\",\n          \"--header\",\n          \"Authorization: Bearer token123\",\n          \"--header\",\n          \"X-API-Key: secret123\",\n        ]);\n\n        expectCliSuccess(result);\n\n        const recordedRequests = server.getRecordedRequests();\n        const toolsListRequest = recordedRequests[recordedRequests.length - 1];\n        expect(toolsListRequest.method).toBe(\"tools/list\");\n        expect(toolsListRequest.headers?.authorization).toBe(\"Bearer token123\");\n        expect(toolsListRequest.headers?.[\"x-api-key\"]).toBe(\"secret123\");\n      } finally {\n        await server.stop();\n      }\n    });\n\n    it(\"should handle header with colons in value\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createEchoTool()],\n      });\n\n      try {\n        const port = await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n          \"--transport\",\n          \"http\",\n          \"--header\",\n          \"X-Time: 2023:12:25:10:30:45\",\n        ]);\n\n        expectCliSuccess(result);\n\n        const recordedRequests = server.getRecordedRequests();\n        const toolsListRequest = recordedRequests[recordedRequests.length - 1];\n        expect(toolsListRequest.method).toBe(\"tools/list\");\n        expect(toolsListRequest.headers?.[\"x-time\"]).toBe(\n          \"2023:12:25:10:30:45\",\n        );\n      } finally {\n        await server.stop();\n      }\n    });\n\n    it(\"should handle whitespace in headers\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createEchoTool()],\n      });\n\n      try {\n        const port = await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n          \"--transport\",\n          \"http\",\n          \"--header\",\n          \"  X-Header  :  value with spaces  \",\n        ]);\n\n        expectCliSuccess(result);\n\n        const recordedRequests = server.getRecordedRequests();\n        const toolsListRequest = recordedRequests[recordedRequests.length - 1];\n        expect(toolsListRequest.method).toBe(\"tools/list\");\n        // Header values should be trimmed by the CLI parser\n        expect(toolsListRequest.headers?.[\"x-header\"]).toBe(\n          \"value with spaces\",\n        );\n      } finally {\n        await server.stop();\n      }\n    });\n  });\n\n  describe(\"Invalid Header Formats\", () => {\n    it(\"should reject header format without colon\", async () => {\n      const result = await runCli([\n        \"https://example.com\",\n        \"--cli\",\n        \"--method\",\n        \"tools/list\",\n        \"--transport\",\n        \"http\",\n        \"--header\",\n        \"InvalidHeader\",\n      ]);\n\n      expectCliFailure(result);\n      expectOutputContains(result, \"Invalid header format\");\n    });\n\n    it(\"should reject header format with empty name\", async () => {\n      const result = await runCli([\n        \"https://example.com\",\n        \"--cli\",\n        \"--method\",\n        \"tools/list\",\n        \"--transport\",\n        \"http\",\n        \"--header\",\n        \": value\",\n      ]);\n\n      expectCliFailure(result);\n      expectOutputContains(result, \"Invalid header format\");\n    });\n\n    it(\"should reject header format with empty value\", async () => {\n      const result = await runCli([\n        \"https://example.com\",\n        \"--cli\",\n        \"--method\",\n        \"tools/list\",\n        \"--transport\",\n        \"http\",\n        \"--header\",\n        \"Header:\",\n      ]);\n\n      expectCliFailure(result);\n      expectOutputContains(result, \"Invalid header format\");\n    });\n  });\n});\n"
  },
  {
    "path": "cli/__tests__/helpers/assertions.ts",
    "content": "import { expect } from \"vitest\";\nimport type { CliResult } from \"./cli-runner.js\";\n\nfunction formatCliOutput(result: CliResult): string {\n  const out = result.stdout?.trim() || \"(empty)\";\n  const err = result.stderr?.trim() || \"(empty)\";\n  return `stdout: ${out}\\nstderr: ${err}`;\n}\n\n/**\n * Assert that CLI command succeeded (exit code 0)\n */\nexport function expectCliSuccess(result: CliResult) {\n  expect(\n    result.exitCode,\n    `CLI exited with code ${result.exitCode}. ${formatCliOutput(result)}`,\n  ).toBe(0);\n}\n\n/**\n * Assert that CLI command failed (non-zero exit code)\n */\nexport function expectCliFailure(result: CliResult) {\n  expect(\n    result.exitCode,\n    `CLI unexpectedly exited with code ${result.exitCode}. ${formatCliOutput(result)}`,\n  ).not.toBe(0);\n}\n\n/**\n * Assert that output contains expected text\n */\nexport function expectOutputContains(result: CliResult, text: string) {\n  expect(result.output).toContain(text);\n}\n\n/**\n * Assert that output contains valid JSON\n * Uses stdout (not stderr) since JSON is written to stdout and warnings go to stderr\n */\nexport function expectValidJson(result: CliResult) {\n  expect(() => JSON.parse(result.stdout)).not.toThrow();\n  return JSON.parse(result.stdout);\n}\n\n/**\n * Assert that output contains JSON with error flag\n */\nexport function expectJsonError(result: CliResult) {\n  const json = expectValidJson(result);\n  expect(json.isError).toBe(true);\n  return json;\n}\n\n/**\n * Assert that output contains expected JSON structure\n */\nexport function expectJsonStructure(result: CliResult, expectedKeys: string[]) {\n  const json = expectValidJson(result);\n  expectedKeys.forEach((key) => {\n    expect(json).toHaveProperty(key);\n  });\n  return json;\n}\n"
  },
  {
    "path": "cli/__tests__/helpers/cli-runner.ts",
    "content": "import { spawn } from \"child_process\";\nimport { resolve } from \"path\";\nimport { fileURLToPath } from \"url\";\nimport { dirname } from \"path\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst CLI_PATH = resolve(__dirname, \"../../build/cli.js\");\n\nexport interface CliResult {\n  exitCode: number | null;\n  stdout: string;\n  stderr: string;\n  output: string; // Combined stdout + stderr\n}\n\nexport interface CliOptions {\n  timeout?: number;\n  cwd?: string;\n  env?: Record<string, string>;\n  signal?: AbortSignal;\n}\n\n/**\n * Run the CLI with given arguments and capture output\n */\nexport async function runCli(\n  args: string[],\n  options: CliOptions = {},\n): Promise<CliResult> {\n  return new Promise((resolve, reject) => {\n    const child = spawn(\"node\", [CLI_PATH, ...args], {\n      stdio: [\"pipe\", \"pipe\", \"pipe\"],\n      cwd: options.cwd,\n      env: { ...process.env, ...options.env },\n      signal: options.signal,\n      // Kill child process tree on exit\n      detached: false,\n    });\n\n    let stdout = \"\";\n    let stderr = \"\";\n    let resolved = false;\n\n    // Default timeout of 10 seconds (less than vitest's 15s)\n    const timeoutMs = options.timeout ?? 10000;\n    const timeout = setTimeout(() => {\n      if (!resolved) {\n        resolved = true;\n        // Kill the process and all its children\n        try {\n          if (process.platform === \"win32\") {\n            child.kill(\"SIGTERM\");\n          } else {\n            // On Unix, kill the process group\n            process.kill(-child.pid!, \"SIGTERM\");\n          }\n        } catch (e) {\n          // Process might already be dead, try direct kill\n          try {\n            child.kill(\"SIGKILL\");\n          } catch (e2) {\n            // Process is definitely dead\n          }\n        }\n        reject(new Error(`CLI command timed out after ${timeoutMs}ms`));\n      }\n    }, timeoutMs);\n\n    child.stdout.on(\"data\", (data) => {\n      stdout += data.toString();\n    });\n\n    child.stderr.on(\"data\", (data) => {\n      stderr += data.toString();\n    });\n\n    child.on(\"close\", (code) => {\n      if (!resolved) {\n        resolved = true;\n        clearTimeout(timeout);\n        resolve({\n          exitCode: code,\n          stdout,\n          stderr,\n          output: stdout + stderr,\n        });\n      }\n    });\n\n    child.on(\"error\", (error) => {\n      if (!resolved) {\n        resolved = true;\n        clearTimeout(timeout);\n        reject(error);\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "cli/__tests__/helpers/fixtures.ts",
    "content": "import * as fs from \"fs\";\nimport * as path from \"path\";\nimport * as os from \"os\";\nimport * as crypto from \"crypto\";\nimport { getTestMcpServerCommand } from \"./test-server-stdio.js\";\n\n/**\n * Sentinel value for tests that don't need a real server\n * (tests that expect failure before connecting)\n */\nexport const NO_SERVER_SENTINEL = \"invalid-command-that-does-not-exist\";\n\n/**\n * Create a sample test config with test-stdio and test-http servers\n * Returns a temporary config file path that should be cleaned up with deleteConfigFile()\n * @param httpUrl - Optional full URL (including /mcp path) for test-http server.\n *                  If not provided, uses a placeholder URL. The test-http server exists\n *                  to test server selection logic and may not actually be used.\n */\nexport function createSampleTestConfig(httpUrl?: string): string {\n  const { command, args } = getTestMcpServerCommand();\n  return createTestConfig({\n    mcpServers: {\n      \"test-stdio\": {\n        type: \"stdio\",\n        command,\n        args,\n        env: {\n          HELLO: \"Hello MCP!\",\n        },\n      },\n      \"test-http\": {\n        type: \"streamable-http\",\n        url: httpUrl || \"http://localhost:3001/mcp\",\n      },\n    },\n  });\n}\n\n/**\n * Create a temporary directory for test files\n * Uses crypto.randomUUID() to ensure uniqueness even when called in parallel\n */\nfunction createTempDir(prefix: string = \"mcp-inspector-test-\"): string {\n  const uniqueId = crypto.randomUUID();\n  const tempDir = path.join(os.tmpdir(), `${prefix}${uniqueId}`);\n  fs.mkdirSync(tempDir, { recursive: true });\n  return tempDir;\n}\n\n/**\n * Clean up temporary directory\n */\nfunction cleanupTempDir(dir: string) {\n  try {\n    fs.rmSync(dir, { recursive: true, force: true });\n  } catch (err) {\n    // Ignore cleanup errors\n  }\n}\n\n/**\n * Create a test config file\n */\nexport function createTestConfig(config: {\n  mcpServers: Record<string, any>;\n}): string {\n  const tempDir = createTempDir(\"mcp-inspector-config-\");\n  const configPath = path.join(tempDir, \"config.json\");\n  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));\n  return configPath;\n}\n\n/**\n * Create an invalid config file (malformed JSON)\n */\nexport function createInvalidConfig(): string {\n  const tempDir = createTempDir(\"mcp-inspector-config-\");\n  const configPath = path.join(tempDir, \"invalid-config.json\");\n  fs.writeFileSync(configPath, '{\\n  \"mcpServers\": {\\n    \"invalid\": {');\n  return configPath;\n}\n\n/**\n * Delete a config file and its containing directory\n */\nexport function deleteConfigFile(configPath: string): void {\n  cleanupTempDir(path.dirname(configPath));\n}\n"
  },
  {
    "path": "cli/__tests__/helpers/test-fixtures.ts",
    "content": "/**\n * Shared types and test fixtures for composable MCP test servers\n */\n\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { Implementation } from \"@modelcontextprotocol/sdk/types.js\";\nimport * as z from \"zod/v4\";\nimport { ZodRawShapeCompat } from \"@modelcontextprotocol/sdk/server/zod-compat.js\";\n\ntype ToolInputSchema = ZodRawShapeCompat;\n\nexport interface ToolDefinition {\n  name: string;\n  description: string;\n  inputSchema?: ToolInputSchema;\n  handler: (params: Record<string, any>) => Promise<any>;\n}\n\nexport interface ResourceDefinition {\n  uri: string;\n  name: string;\n  description?: string;\n  mimeType?: string;\n  text?: string;\n}\n\ntype PromptArgsSchema = ZodRawShapeCompat;\n\nexport interface PromptDefinition {\n  name: string;\n  description?: string;\n  argsSchema?: PromptArgsSchema;\n}\n\n// This allows us to compose tests servers using the metadata and features we want in a given scenario\nexport interface ServerConfig {\n  serverInfo: Implementation; // Server metadata (name, version, etc.) - required\n  tools?: ToolDefinition[]; // Tools to register (optional, empty array means no tools, but tools capability is still advertised)\n  resources?: ResourceDefinition[]; // Resources to register (optional, empty array means no resources, but resources capability is still advertised)\n  prompts?: PromptDefinition[]; // Prompts to register (optional, empty array means no prompts, but prompts capability is still advertised)\n  logging?: boolean; // Whether to advertise logging capability (default: false)\n}\n\n/**\n * Create an \"echo\" tool that echoes back the input message\n */\nexport function createEchoTool(): ToolDefinition {\n  return {\n    name: \"echo\",\n    description: \"Echo back the input message\",\n    inputSchema: {\n      message: z.string().describe(\"Message to echo back\"),\n    },\n    handler: async (params: Record<string, any>) => {\n      return { message: `Echo: ${params.message as string}` };\n    },\n  };\n}\n\n/**\n * Create an \"add\" tool that adds two numbers together\n */\nexport function createAddTool(): ToolDefinition {\n  return {\n    name: \"add\",\n    description: \"Add two numbers together\",\n    inputSchema: {\n      a: z.number().describe(\"First number\"),\n      b: z.number().describe(\"Second number\"),\n    },\n    handler: async (params: Record<string, any>) => {\n      const a = params.a as number;\n      const b = params.b as number;\n      return { result: a + b };\n    },\n  };\n}\n\n/**\n * Create a \"get-sum\" tool that returns the sum of two numbers (alias for add)\n */\nexport function createGetSumTool(): ToolDefinition {\n  return {\n    name: \"get-sum\",\n    description: \"Get the sum of two numbers\",\n    inputSchema: {\n      a: z.number().describe(\"First number\"),\n      b: z.number().describe(\"Second number\"),\n    },\n    handler: async (params: Record<string, any>) => {\n      const a = params.a as number;\n      const b = params.b as number;\n      return { result: a + b };\n    },\n  };\n}\n\n/**\n * Create a \"get-annotated-message\" tool that returns a message with optional image\n */\nexport function createGetAnnotatedMessageTool(): ToolDefinition {\n  return {\n    name: \"get-annotated-message\",\n    description: \"Get an annotated message\",\n    inputSchema: {\n      messageType: z\n        .enum([\"success\", \"error\", \"warning\", \"info\"])\n        .describe(\"Type of message\"),\n      includeImage: z\n        .boolean()\n        .optional()\n        .describe(\"Whether to include an image\"),\n    },\n    handler: async (params: Record<string, any>) => {\n      const messageType = params.messageType as string;\n      const includeImage = params.includeImage as boolean | undefined;\n      const message = `This is a ${messageType} message`;\n      const content: Array<\n        | { type: \"text\"; text: string }\n        | { type: \"image\"; data: string; mimeType: string }\n      > = [\n        {\n          type: \"text\",\n          text: message,\n        },\n      ];\n\n      if (includeImage) {\n        content.push({\n          type: \"image\",\n          data: \"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\", // 1x1 transparent PNG\n          mimeType: \"image/png\",\n        });\n      }\n\n      return { content };\n    },\n  };\n}\n\n/**\n * Create a \"simple-prompt\" prompt definition\n */\nexport function createSimplePrompt(): PromptDefinition {\n  return {\n    name: \"simple-prompt\",\n    description: \"A simple prompt for testing\",\n  };\n}\n\n/**\n * Create an \"args-prompt\" prompt that accepts arguments\n */\nexport function createArgsPrompt(): PromptDefinition {\n  return {\n    name: \"args-prompt\",\n    description: \"A prompt that accepts arguments for testing\",\n    argsSchema: {\n      city: z.string().describe(\"City name\"),\n      state: z.string().describe(\"State name\"),\n    },\n  };\n}\n\n/**\n * Create an \"architecture\" resource definition\n */\nexport function createArchitectureResource(): ResourceDefinition {\n  return {\n    name: \"architecture\",\n    uri: \"demo://resource/static/document/architecture.md\",\n    description: \"Architecture documentation\",\n    mimeType: \"text/markdown\",\n    text: `# Architecture Documentation\n\nThis is a test resource for the MCP test server.\n\n## Overview\n\nThis resource is used for testing resource reading functionality in the CLI.\n\n## Sections\n\n- Introduction\n- Design\n- Implementation\n- Testing\n\n## Notes\n\nThis is a static resource provided by the test MCP server.\n`,\n  };\n}\n\n/**\n * Create a \"test-cwd\" resource that exposes the current working directory (generally useful when testing with the stdio test server)\n */\nexport function createTestCwdResource(): ResourceDefinition {\n  return {\n    name: \"test-cwd\",\n    uri: \"test://cwd\",\n    description: \"Current working directory of the test server\",\n    mimeType: \"text/plain\",\n    text: process.cwd(),\n  };\n}\n\n/**\n * Create a \"test-env\" resource that exposes environment variables (generally useful when testing with the stdio test server)\n */\nexport function createTestEnvResource(): ResourceDefinition {\n  return {\n    name: \"test-env\",\n    uri: \"test://env\",\n    description: \"Environment variables available to the test server\",\n    mimeType: \"application/json\",\n    text: JSON.stringify(process.env, null, 2),\n  };\n}\n\n/**\n * Create a \"test-argv\" resource that exposes command-line arguments (generally useful when testing with the stdio test server)\n */\nexport function createTestArgvResource(): ResourceDefinition {\n  return {\n    name: \"test-argv\",\n    uri: \"test://argv\",\n    description: \"Command-line arguments the test server was started with\",\n    mimeType: \"application/json\",\n    text: JSON.stringify(process.argv, null, 2),\n  };\n}\n\n/**\n * Create minimal server info for test servers\n */\nexport function createTestServerInfo(\n  name: string = \"test-server\",\n  version: string = \"1.0.0\",\n): Implementation {\n  return {\n    name,\n    version,\n  };\n}\n\n/**\n * Get default server config with common test tools, prompts, and resources\n */\nexport function getDefaultServerConfig(): ServerConfig {\n  return {\n    serverInfo: createTestServerInfo(\"test-mcp-server\", \"1.0.0\"),\n    tools: [\n      createEchoTool(),\n      createGetSumTool(),\n      createGetAnnotatedMessageTool(),\n    ],\n    prompts: [createSimplePrompt(), createArgsPrompt()],\n    resources: [\n      createArchitectureResource(),\n      createTestCwdResource(),\n      createTestEnvResource(),\n      createTestArgvResource(),\n    ],\n  };\n}\n"
  },
  {
    "path": "cli/__tests__/helpers/test-server-http.ts",
    "content": "import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { SSEServerTransport } from \"@modelcontextprotocol/sdk/server/sse.js\";\nimport { SetLevelRequestSchema } from \"@modelcontextprotocol/sdk/types.js\";\nimport type { Request, Response } from \"express\";\nimport express from \"express\";\nimport { createServer as createHttpServer, Server as HttpServer } from \"http\";\nimport { createServer as createNetServer } from \"net\";\nimport { randomUUID } from \"crypto\";\nimport * as z from \"zod/v4\";\nimport type { ServerConfig } from \"./test-fixtures.js\";\n\nexport interface RecordedRequest {\n  method: string;\n  params?: any;\n  headers?: Record<string, string>;\n  metadata?: Record<string, string>;\n  response: any;\n  timestamp: number;\n}\n\n/**\n * Find an available port starting from the given port\n */\nasync function findAvailablePort(startPort: number): Promise<number> {\n  return new Promise((resolve, reject) => {\n    const server = createNetServer();\n    server.listen(startPort, () => {\n      const port = (server.address() as { port: number })?.port;\n      server.close(() => resolve(port || startPort));\n    });\n    server.on(\"error\", (err: NodeJS.ErrnoException) => {\n      if (err.code === \"EADDRINUSE\") {\n        // Try next port\n        findAvailablePort(startPort + 1)\n          .then(resolve)\n          .catch(reject);\n      } else {\n        reject(err);\n      }\n    });\n  });\n}\n\n/**\n * Extract headers from Express request\n */\nfunction extractHeaders(req: Request): Record<string, string> {\n  const headers: Record<string, string> = {};\n  for (const [key, value] of Object.entries(req.headers)) {\n    if (typeof value === \"string\") {\n      headers[key] = value;\n    } else if (Array.isArray(value) && value.length > 0) {\n      headers[key] = value[value.length - 1];\n    }\n  }\n  return headers;\n}\n\n// With this test server, your test can hold an instance and you can get the server's recorded message history at any time.\n//\nexport class TestServerHttp {\n  private mcpServer: McpServer;\n  private config: ServerConfig;\n  private recordedRequests: RecordedRequest[] = [];\n  private httpServer?: HttpServer;\n  private transport?: StreamableHTTPServerTransport | SSEServerTransport;\n  private url?: string;\n  private currentRequestHeaders?: Record<string, string>;\n  private currentLogLevel: string | null = null;\n\n  constructor(config: ServerConfig) {\n    this.config = config;\n    const capabilities: {\n      tools?: {};\n      resources?: {};\n      prompts?: {};\n      logging?: {};\n    } = {};\n\n    // Only include capabilities for features that are present in config\n    if (config.tools !== undefined) {\n      capabilities.tools = {};\n    }\n    if (config.resources !== undefined) {\n      capabilities.resources = {};\n    }\n    if (config.prompts !== undefined) {\n      capabilities.prompts = {};\n    }\n    if (config.logging === true) {\n      capabilities.logging = {};\n    }\n\n    this.mcpServer = new McpServer(config.serverInfo, {\n      capabilities,\n    });\n\n    this.setupHandlers();\n    if (config.logging === true) {\n      this.setupLoggingHandler();\n    }\n  }\n\n  private setupHandlers() {\n    // Set up tools\n    if (this.config.tools && this.config.tools.length > 0) {\n      for (const tool of this.config.tools) {\n        this.mcpServer.registerTool(\n          tool.name,\n          {\n            description: tool.description,\n            inputSchema: tool.inputSchema,\n          },\n          async (args) => {\n            const result = await tool.handler(args as Record<string, any>);\n            return {\n              content: [{ type: \"text\", text: JSON.stringify(result) }],\n            };\n          },\n        );\n      }\n    }\n\n    // Set up resources\n    if (this.config.resources && this.config.resources.length > 0) {\n      for (const resource of this.config.resources) {\n        this.mcpServer.registerResource(\n          resource.name,\n          resource.uri,\n          {\n            description: resource.description,\n            mimeType: resource.mimeType,\n          },\n          async () => {\n            return {\n              contents: [\n                {\n                  uri: resource.uri,\n                  mimeType: resource.mimeType || \"text/plain\",\n                  text: resource.text || \"\",\n                },\n              ],\n            };\n          },\n        );\n      }\n    }\n\n    // Set up prompts\n    if (this.config.prompts && this.config.prompts.length > 0) {\n      for (const prompt of this.config.prompts) {\n        this.mcpServer.registerPrompt(\n          prompt.name,\n          {\n            description: prompt.description,\n            argsSchema: prompt.argsSchema,\n          },\n          async (args) => {\n            // Return a simple prompt response\n            return {\n              messages: [\n                {\n                  role: \"user\",\n                  content: {\n                    type: \"text\",\n                    text: `Prompt: ${prompt.name}${args ? ` with args: ${JSON.stringify(args)}` : \"\"}`,\n                  },\n                },\n              ],\n            };\n          },\n        );\n      }\n    }\n  }\n\n  private setupLoggingHandler() {\n    // Intercept logging/setLevel requests to track the level\n    this.mcpServer.server.setRequestHandler(\n      SetLevelRequestSchema,\n      async (request) => {\n        this.currentLogLevel = request.params.level;\n        // Return empty result as per MCP spec\n        return {};\n      },\n    );\n  }\n\n  /**\n   * Start the server with the specified transport.\n   * When requestedPort is omitted, uses port 0 so the OS assigns a unique port (avoids EADDRINUSE when tests run in parallel).\n   */\n  async start(\n    transport: \"http\" | \"sse\",\n    requestedPort?: number,\n  ): Promise<number> {\n    const port =\n      requestedPort !== undefined ? await findAvailablePort(requestedPort) : 0;\n\n    if (transport === \"http\") {\n      const actualPort = await this.startHttp(port);\n      this.url = `http://localhost:${actualPort}`;\n      return actualPort;\n    } else {\n      const actualPort = await this.startSse(port);\n      this.url = `http://localhost:${actualPort}`;\n      return actualPort;\n    }\n  }\n\n  private async startHttp(port: number): Promise<number> {\n    const app = express();\n    app.use(express.json());\n\n    // Create HTTP server\n    this.httpServer = createHttpServer(app);\n\n    // Create StreamableHTTP transport (stateful so it can handle multiple requests per session)\n    this.transport = new StreamableHTTPServerTransport({\n      sessionIdGenerator: () => randomUUID(),\n    });\n\n    // Set up Express route to handle MCP requests\n    app.post(\"/mcp\", async (req: Request, res: Response) => {\n      // Capture headers for this request\n      this.currentRequestHeaders = extractHeaders(req);\n\n      try {\n        await (this.transport as StreamableHTTPServerTransport).handleRequest(\n          req,\n          res,\n          req.body,\n        );\n      } catch (error) {\n        res.status(500).json({\n          error: error instanceof Error ? error.message : String(error),\n        });\n      }\n    });\n\n    // Intercept messages to record them\n    const originalOnMessage = this.transport.onmessage;\n    this.transport.onmessage = async (message) => {\n      const timestamp = Date.now();\n      const method =\n        \"method\" in message && typeof message.method === \"string\"\n          ? message.method\n          : \"unknown\";\n      const params = \"params\" in message ? message.params : undefined;\n\n      try {\n        // Extract metadata from params if present\n        const metadata =\n          params && typeof params === \"object\" && \"_meta\" in params\n            ? ((params as any)._meta as Record<string, string>)\n            : undefined;\n\n        // Let the server handle the message\n        if (originalOnMessage) {\n          await originalOnMessage.call(this.transport, message);\n        }\n\n        // Record successful request (response will be sent by transport)\n        // Note: We can't easily capture the response here, so we'll record\n        // that the request was processed\n        this.recordedRequests.push({\n          method,\n          params,\n          headers: { ...this.currentRequestHeaders },\n          metadata: metadata ? { ...metadata } : undefined,\n          response: { processed: true },\n          timestamp,\n        });\n      } catch (error) {\n        // Extract metadata from params if present\n        const metadata =\n          params && typeof params === \"object\" && \"_meta\" in params\n            ? ((params as any)._meta as Record<string, string>)\n            : undefined;\n\n        // Record error\n        this.recordedRequests.push({\n          method,\n          params,\n          headers: { ...this.currentRequestHeaders },\n          metadata: metadata ? { ...metadata } : undefined,\n          response: {\n            error: error instanceof Error ? error.message : String(error),\n          },\n          timestamp,\n        });\n        throw error;\n      }\n    };\n\n    // Connect transport to server\n    await this.mcpServer.connect(this.transport);\n\n    // Start listening (port 0 = OS assigns a unique port)\n    return new Promise((resolve, reject) => {\n      this.httpServer!.listen(port, () => {\n        const assignedPort = (this.httpServer!.address() as { port: number })\n          ?.port;\n        resolve(assignedPort ?? port);\n      });\n      this.httpServer!.on(\"error\", reject);\n    });\n  }\n\n  private async startSse(port: number): Promise<number> {\n    const app = express();\n    app.use(express.json());\n\n    // Create HTTP server\n    this.httpServer = createHttpServer(app);\n\n    // For SSE, we need to set up an Express route that creates the transport per request\n    // This is a simplified version - SSE transport is created per connection\n    app.get(\"/mcp\", async (req: Request, res: Response) => {\n      this.currentRequestHeaders = extractHeaders(req);\n      const sseTransport = new SSEServerTransport(\"/mcp\", res);\n\n      // Intercept messages\n      const originalOnMessage = sseTransport.onmessage;\n      sseTransport.onmessage = async (message) => {\n        const timestamp = Date.now();\n        const method =\n          \"method\" in message && typeof message.method === \"string\"\n            ? message.method\n            : \"unknown\";\n        const params = \"params\" in message ? message.params : undefined;\n\n        try {\n          // Extract metadata from params if present\n          const metadata =\n            params && typeof params === \"object\" && \"_meta\" in params\n              ? ((params as any)._meta as Record<string, string>)\n              : undefined;\n\n          if (originalOnMessage) {\n            await originalOnMessage.call(sseTransport, message);\n          }\n\n          this.recordedRequests.push({\n            method,\n            params,\n            headers: { ...this.currentRequestHeaders },\n            metadata: metadata ? { ...metadata } : undefined,\n            response: { processed: true },\n            timestamp,\n          });\n        } catch (error) {\n          // Extract metadata from params if present\n          const metadata =\n            params && typeof params === \"object\" && \"_meta\" in params\n              ? ((params as any)._meta as Record<string, string>)\n              : undefined;\n\n          this.recordedRequests.push({\n            method,\n            params,\n            headers: { ...this.currentRequestHeaders },\n            metadata: metadata ? { ...metadata } : undefined,\n            response: {\n              error: error instanceof Error ? error.message : String(error),\n            },\n            timestamp,\n          });\n          throw error;\n        }\n      };\n\n      await this.mcpServer.connect(sseTransport);\n      await sseTransport.start();\n    });\n\n    // Note: SSE transport is created per request, so we don't store a single instance\n    this.transport = undefined;\n\n    // Start listening (port 0 = OS assigns a unique port)\n    return new Promise((resolve, reject) => {\n      this.httpServer!.listen(port, () => {\n        const assignedPort = (this.httpServer!.address() as { port: number })\n          ?.port;\n        resolve(assignedPort ?? port);\n      });\n      this.httpServer!.on(\"error\", reject);\n    });\n  }\n\n  /**\n   * Stop the server\n   */\n  async stop(): Promise<void> {\n    await this.mcpServer.close();\n\n    if (this.transport) {\n      await this.transport.close();\n      this.transport = undefined;\n    }\n\n    if (this.httpServer) {\n      return new Promise((resolve) => {\n        // Force close all connections\n        this.httpServer!.closeAllConnections?.();\n        this.httpServer!.close(() => {\n          this.httpServer = undefined;\n          resolve();\n        });\n      });\n    }\n  }\n\n  /**\n   * Get all recorded requests\n   */\n  getRecordedRequests(): RecordedRequest[] {\n    return [...this.recordedRequests];\n  }\n\n  /**\n   * Clear recorded requests\n   */\n  clearRecordings(): void {\n    this.recordedRequests = [];\n  }\n\n  /**\n   * Get the server URL\n   */\n  getUrl(): string {\n    if (!this.url) {\n      throw new Error(\"Server not started\");\n    }\n    return this.url;\n  }\n\n  /**\n   * Get the most recent log level that was set\n   */\n  getCurrentLogLevel(): string | null {\n    return this.currentLogLevel;\n  }\n}\n\n/**\n * Create an HTTP/SSE MCP test server\n */\nexport function createTestServerHttp(config: ServerConfig): TestServerHttp {\n  return new TestServerHttp(config);\n}\n"
  },
  {
    "path": "cli/__tests__/helpers/test-server-stdio.ts",
    "content": "#!/usr/bin/env node\n\n/**\n * Test MCP server for stdio transport testing\n * Can be used programmatically or run as a standalone executable\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport * as z from \"zod/v4\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\nimport { dirname } from \"path\";\nimport type {\n  ServerConfig,\n  ToolDefinition,\n  PromptDefinition,\n  ResourceDefinition,\n} from \"./test-fixtures.js\";\nimport { getDefaultServerConfig } from \"./test-fixtures.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nexport class TestServerStdio {\n  private mcpServer: McpServer;\n  private config: ServerConfig;\n  private transport?: StdioServerTransport;\n\n  constructor(config: ServerConfig) {\n    this.config = config;\n    const capabilities: {\n      tools?: {};\n      resources?: {};\n      prompts?: {};\n      logging?: {};\n    } = {};\n\n    // Only include capabilities for features that are present in config\n    if (config.tools !== undefined) {\n      capabilities.tools = {};\n    }\n    if (config.resources !== undefined) {\n      capabilities.resources = {};\n    }\n    if (config.prompts !== undefined) {\n      capabilities.prompts = {};\n    }\n    if (config.logging === true) {\n      capabilities.logging = {};\n    }\n\n    this.mcpServer = new McpServer(config.serverInfo, {\n      capabilities,\n    });\n\n    this.setupHandlers();\n  }\n\n  private setupHandlers() {\n    // Set up tools\n    if (this.config.tools && this.config.tools.length > 0) {\n      for (const tool of this.config.tools) {\n        this.mcpServer.registerTool(\n          tool.name,\n          {\n            description: tool.description,\n            inputSchema: tool.inputSchema,\n          },\n          async (args) => {\n            const result = await tool.handler(args as Record<string, any>);\n            // If handler returns content array directly (like get-annotated-message), use it\n            if (result && Array.isArray(result.content)) {\n              return { content: result.content };\n            }\n            // If handler returns message (like echo), format it\n            if (result && typeof result.message === \"string\") {\n              return {\n                content: [\n                  {\n                    type: \"text\",\n                    text: result.message,\n                  },\n                ],\n              };\n            }\n            // Otherwise, stringify the result\n            return {\n              content: [\n                {\n                  type: \"text\",\n                  text: JSON.stringify(result),\n                },\n              ],\n            };\n          },\n        );\n      }\n    }\n\n    // Set up resources\n    if (this.config.resources && this.config.resources.length > 0) {\n      for (const resource of this.config.resources) {\n        this.mcpServer.registerResource(\n          resource.name,\n          resource.uri,\n          {\n            description: resource.description,\n            mimeType: resource.mimeType,\n          },\n          async () => {\n            // For dynamic resources, get fresh text\n            let text = resource.text;\n            if (resource.name === \"test-cwd\") {\n              text = process.cwd();\n            } else if (resource.name === \"test-env\") {\n              text = JSON.stringify(process.env, null, 2);\n            } else if (resource.name === \"test-argv\") {\n              text = JSON.stringify(process.argv, null, 2);\n            }\n\n            return {\n              contents: [\n                {\n                  uri: resource.uri,\n                  mimeType: resource.mimeType || \"text/plain\",\n                  text: text || \"\",\n                },\n              ],\n            };\n          },\n        );\n      }\n    }\n\n    // Set up prompts\n    if (this.config.prompts && this.config.prompts.length > 0) {\n      for (const prompt of this.config.prompts) {\n        this.mcpServer.registerPrompt(\n          prompt.name,\n          {\n            description: prompt.description,\n            argsSchema: prompt.argsSchema,\n          },\n          async (args) => {\n            if (prompt.name === \"args-prompt\" && args) {\n              const city = (args as any).city as string;\n              const state = (args as any).state as string;\n              return {\n                messages: [\n                  {\n                    role: \"user\",\n                    content: {\n                      type: \"text\",\n                      text: `This is a prompt with arguments: city=${city}, state=${state}`,\n                    },\n                  },\n                ],\n              };\n            } else {\n              return {\n                messages: [\n                  {\n                    role: \"user\",\n                    content: {\n                      type: \"text\",\n                      text: \"This is a simple prompt for testing purposes.\",\n                    },\n                  },\n                ],\n              };\n            }\n          },\n        );\n      }\n    }\n  }\n\n  /**\n   * Start the server with stdio transport\n   */\n  async start(): Promise<void> {\n    this.transport = new StdioServerTransport();\n    await this.mcpServer.connect(this.transport);\n  }\n\n  /**\n   * Stop the server\n   */\n  async stop(): Promise<void> {\n    await this.mcpServer.close();\n    if (this.transport) {\n      await this.transport.close();\n      this.transport = undefined;\n    }\n  }\n}\n\n/**\n * Create a stdio MCP test server\n */\nexport function createTestServerStdio(config: ServerConfig): TestServerStdio {\n  return new TestServerStdio(config);\n}\n\n/**\n * Get the path to the test MCP server script\n */\nexport function getTestMcpServerPath(): string {\n  return path.resolve(__dirname, \"test-server-stdio.ts\");\n}\n\n/**\n * Get the command and args to run the test MCP server\n */\nexport function getTestMcpServerCommand(): { command: string; args: string[] } {\n  return {\n    command: \"tsx\",\n    args: [getTestMcpServerPath()],\n  };\n}\n\n// If run as a standalone script, start with default config\n// Check if this file is being executed directly (not imported)\nconst isMainModule =\n  import.meta.url.endsWith(process.argv[1]) ||\n  process.argv[1]?.endsWith(\"test-server-stdio.ts\") ||\n  process.argv[1]?.endsWith(\"test-server-stdio.js\");\n\nif (isMainModule) {\n  const server = new TestServerStdio(getDefaultServerConfig());\n  server\n    .start()\n    .then(() => {\n      // Server is now running and listening on stdio\n      // Keep the process alive\n    })\n    .catch((error) => {\n      console.error(\"Failed to start test MCP server:\", error);\n      process.exit(1);\n    });\n}\n"
  },
  {
    "path": "cli/__tests__/metadata.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport { runCli } from \"./helpers/cli-runner.js\";\nimport {\n  expectCliSuccess,\n  expectCliFailure,\n  expectValidJson,\n} from \"./helpers/assertions.js\";\nimport { createTestServerHttp } from \"./helpers/test-server-http.js\";\nimport {\n  createEchoTool,\n  createAddTool,\n  createTestServerInfo,\n} from \"./helpers/test-fixtures.js\";\nimport { NO_SERVER_SENTINEL } from \"./helpers/fixtures.js\";\n\ndescribe(\"Metadata Tests\", () => {\n  describe(\"General Metadata\", () => {\n    it(\"should work with tools/list\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createEchoTool()],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n          \"--metadata\",\n          \"client=test-client\",\n          \"--transport\",\n          \"http\",\n        ]);\n\n        expectCliSuccess(result);\n        const json = expectValidJson(result);\n        expect(json).toHaveProperty(\"tools\");\n\n        // Validate metadata was sent\n        const recordedRequests = server.getRecordedRequests();\n        const toolsListRequest = recordedRequests.find(\n          (r) => r.method === \"tools/list\",\n        );\n        expect(toolsListRequest).toBeDefined();\n        expect(toolsListRequest?.metadata).toEqual({ client: \"test-client\" });\n      } finally {\n        await server.stop();\n      }\n    });\n\n    it(\"should work with resources/list\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        resources: [\n          {\n            uri: \"test://resource\",\n            name: \"test-resource\",\n            text: \"test content\",\n          },\n        ],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"resources/list\",\n          \"--metadata\",\n          \"client=test-client\",\n          \"--transport\",\n          \"http\",\n        ]);\n\n        expectCliSuccess(result);\n        const json = expectValidJson(result);\n        expect(json).toHaveProperty(\"resources\");\n\n        // Validate metadata was sent\n        const recordedRequests = server.getRecordedRequests();\n        const resourcesListRequest = recordedRequests.find(\n          (r) => r.method === \"resources/list\",\n        );\n        expect(resourcesListRequest).toBeDefined();\n        expect(resourcesListRequest?.metadata).toEqual({\n          client: \"test-client\",\n        });\n      } finally {\n        await server.stop();\n      }\n    });\n\n    it(\"should work with prompts/list\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        prompts: [\n          {\n            name: \"test-prompt\",\n            description: \"A test prompt\",\n          },\n        ],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"prompts/list\",\n          \"--metadata\",\n          \"client=test-client\",\n          \"--transport\",\n          \"http\",\n        ]);\n\n        expectCliSuccess(result);\n        const json = expectValidJson(result);\n        expect(json).toHaveProperty(\"prompts\");\n\n        // Validate metadata was sent\n        const recordedRequests = server.getRecordedRequests();\n        const promptsListRequest = recordedRequests.find(\n          (r) => r.method === \"prompts/list\",\n        );\n        expect(promptsListRequest).toBeDefined();\n        expect(promptsListRequest?.metadata).toEqual({\n          client: \"test-client\",\n        });\n      } finally {\n        await server.stop();\n      }\n    });\n\n    it(\"should work with resources/read\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        resources: [\n          {\n            uri: \"test://resource\",\n            name: \"test-resource\",\n            text: \"test content\",\n          },\n        ],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"resources/read\",\n          \"--uri\",\n          \"test://resource\",\n          \"--metadata\",\n          \"client=test-client\",\n          \"--transport\",\n          \"http\",\n        ]);\n\n        expectCliSuccess(result);\n        const json = expectValidJson(result);\n        expect(json).toHaveProperty(\"contents\");\n\n        // Validate metadata was sent\n        const recordedRequests = server.getRecordedRequests();\n        const readRequest = recordedRequests.find(\n          (r) => r.method === \"resources/read\",\n        );\n        expect(readRequest).toBeDefined();\n        expect(readRequest?.metadata).toEqual({ client: \"test-client\" });\n      } finally {\n        await server.stop();\n      }\n    });\n\n    it(\"should work with prompts/get\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        prompts: [\n          {\n            name: \"test-prompt\",\n            description: \"A test prompt\",\n          },\n        ],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"prompts/get\",\n          \"--prompt-name\",\n          \"test-prompt\",\n          \"--metadata\",\n          \"client=test-client\",\n          \"--transport\",\n          \"http\",\n        ]);\n\n        expectCliSuccess(result);\n        const json = expectValidJson(result);\n        expect(json).toHaveProperty(\"messages\");\n\n        // Validate metadata was sent\n        const recordedRequests = server.getRecordedRequests();\n        const getPromptRequest = recordedRequests.find(\n          (r) => r.method === \"prompts/get\",\n        );\n        expect(getPromptRequest).toBeDefined();\n        expect(getPromptRequest?.metadata).toEqual({ client: \"test-client\" });\n      } finally {\n        await server.stop();\n      }\n    });\n  });\n\n  describe(\"Tool-Specific Metadata\", () => {\n    it(\"should work with tools/call\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createEchoTool()],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"tools/call\",\n          \"--tool-name\",\n          \"echo\",\n          \"--tool-arg\",\n          \"message=hello world\",\n          \"--tool-metadata\",\n          \"client=test-client\",\n          \"--transport\",\n          \"http\",\n        ]);\n\n        expectCliSuccess(result);\n        const json = expectValidJson(result);\n        expect(json).toHaveProperty(\"content\");\n\n        // Validate metadata was sent\n        const recordedRequests = server.getRecordedRequests();\n        const toolCallRequest = recordedRequests.find(\n          (r) => r.method === \"tools/call\",\n        );\n        expect(toolCallRequest).toBeDefined();\n        expect(toolCallRequest?.metadata).toEqual({ client: \"test-client\" });\n      } finally {\n        await server.stop();\n      }\n    });\n\n    it(\"should work with complex tool\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createAddTool()],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"tools/call\",\n          \"--tool-name\",\n          \"add\",\n          \"--tool-arg\",\n          \"a=10\",\n          \"b=20\",\n          \"--tool-metadata\",\n          \"client=test-client\",\n          \"--transport\",\n          \"http\",\n        ]);\n\n        expectCliSuccess(result);\n        const json = expectValidJson(result);\n        expect(json).toHaveProperty(\"content\");\n\n        // Validate metadata was sent\n        const recordedRequests = server.getRecordedRequests();\n        const toolCallRequest = recordedRequests.find(\n          (r) => r.method === \"tools/call\",\n        );\n        expect(toolCallRequest).toBeDefined();\n        expect(toolCallRequest?.metadata).toEqual({ client: \"test-client\" });\n      } finally {\n        await server.stop();\n      }\n    });\n  });\n\n  describe(\"Metadata Merging\", () => {\n    it(\"should merge general and tool-specific metadata (tool-specific overrides)\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createEchoTool()],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"tools/call\",\n          \"--tool-name\",\n          \"echo\",\n          \"--tool-arg\",\n          \"message=hello world\",\n          \"--metadata\",\n          \"client=general-client\",\n          \"shared_key=shared_value\",\n          \"--tool-metadata\",\n          \"client=tool-specific-client\",\n          \"--transport\",\n          \"http\",\n        ]);\n\n        expectCliSuccess(result);\n\n        // Validate metadata was merged correctly (tool-specific overrides general)\n        const recordedRequests = server.getRecordedRequests();\n        const toolCallRequest = recordedRequests.find(\n          (r) => r.method === \"tools/call\",\n        );\n        expect(toolCallRequest).toBeDefined();\n        expect(toolCallRequest?.metadata).toEqual({\n          client: \"tool-specific-client\", // Tool-specific overrides general\n          shared_key: \"shared_value\", // General metadata is preserved\n        });\n      } finally {\n        await server.stop();\n      }\n    });\n  });\n\n  describe(\"Metadata Parsing\", () => {\n    it(\"should handle numeric values\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createEchoTool()],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n          \"--metadata\",\n          \"integer_value=42\",\n          \"decimal_value=3.14159\",\n          \"negative_value=-10\",\n          \"--transport\",\n          \"http\",\n        ]);\n\n        expectCliSuccess(result);\n\n        // Validate metadata values are sent as strings\n        const recordedRequests = server.getRecordedRequests();\n        const toolsListRequest = recordedRequests.find(\n          (r) => r.method === \"tools/list\",\n        );\n        expect(toolsListRequest).toBeDefined();\n        expect(toolsListRequest?.metadata).toEqual({\n          integer_value: \"42\",\n          decimal_value: \"3.14159\",\n          negative_value: \"-10\",\n        });\n      } finally {\n        await server.stop();\n      }\n    });\n\n    it(\"should handle JSON values\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createEchoTool()],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n          \"--metadata\",\n          'json_object=\"{\\\\\"key\\\\\":\\\\\"value\\\\\"}\"',\n          'json_array=\"[1,2,3]\"',\n          'json_string=\"\\\\\"quoted\\\\\"\"',\n          \"--transport\",\n          \"http\",\n        ]);\n\n        expectCliSuccess(result);\n\n        // Validate JSON values are sent as strings\n        const recordedRequests = server.getRecordedRequests();\n        const toolsListRequest = recordedRequests.find(\n          (r) => r.method === \"tools/list\",\n        );\n        expect(toolsListRequest).toBeDefined();\n        expect(toolsListRequest?.metadata).toEqual({\n          json_object: '{\"key\":\"value\"}',\n          json_array: \"[1,2,3]\",\n          json_string: '\"quoted\"',\n        });\n      } finally {\n        await server.stop();\n      }\n    });\n\n    it(\"should handle special characters\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createEchoTool()],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n          \"--metadata\",\n          \"unicode=🚀🎉✨\",\n          \"special_chars=!@#$%^&*()\",\n          \"spaces=hello world with spaces\",\n          \"--transport\",\n          \"http\",\n        ]);\n\n        expectCliSuccess(result);\n\n        // Validate special characters are preserved\n        const recordedRequests = server.getRecordedRequests();\n        const toolsListRequest = recordedRequests.find(\n          (r) => r.method === \"tools/list\",\n        );\n        expect(toolsListRequest).toBeDefined();\n        expect(toolsListRequest?.metadata).toEqual({\n          unicode: \"🚀🎉✨\",\n          special_chars: \"!@#$%^&*()\",\n          spaces: \"hello world with spaces\",\n        });\n      } finally {\n        await server.stop();\n      }\n    });\n  });\n\n  describe(\"Metadata Edge Cases\", () => {\n    it(\"should handle single metadata entry\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createEchoTool()],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n          \"--metadata\",\n          \"single_key=single_value\",\n          \"--transport\",\n          \"http\",\n        ]);\n\n        expectCliSuccess(result);\n\n        // Validate single metadata entry\n        const recordedRequests = server.getRecordedRequests();\n        const toolsListRequest = recordedRequests.find(\n          (r) => r.method === \"tools/list\",\n        );\n        expect(toolsListRequest).toBeDefined();\n        expect(toolsListRequest?.metadata).toEqual({\n          single_key: \"single_value\",\n        });\n      } finally {\n        await server.stop();\n      }\n    });\n\n    it(\"should handle many metadata entries\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createEchoTool()],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n          \"--metadata\",\n          \"key1=value1\",\n          \"key2=value2\",\n          \"key3=value3\",\n          \"key4=value4\",\n          \"key5=value5\",\n          \"--transport\",\n          \"http\",\n        ]);\n\n        expectCliSuccess(result);\n\n        // Validate all metadata entries\n        const recordedRequests = server.getRecordedRequests();\n        const toolsListRequest = recordedRequests.find(\n          (r) => r.method === \"tools/list\",\n        );\n        expect(toolsListRequest).toBeDefined();\n        expect(toolsListRequest?.metadata).toEqual({\n          key1: \"value1\",\n          key2: \"value2\",\n          key3: \"value3\",\n          key4: \"value4\",\n          key5: \"value5\",\n        });\n      } finally {\n        await server.stop();\n      }\n    });\n  });\n\n  describe(\"Metadata Error Cases\", () => {\n    it(\"should fail with invalid metadata format (missing equals)\", async () => {\n      const result = await runCli([\n        NO_SERVER_SENTINEL,\n        \"--cli\",\n        \"--method\",\n        \"tools/list\",\n        \"--metadata\",\n        \"invalid_format_no_equals\",\n      ]);\n\n      expectCliFailure(result);\n    });\n\n    it(\"should fail with invalid tool-metadata format (missing equals)\", async () => {\n      const result = await runCli([\n        NO_SERVER_SENTINEL,\n        \"--cli\",\n        \"--method\",\n        \"tools/call\",\n        \"--tool-name\",\n        \"echo\",\n        \"--tool-arg\",\n        \"message=test\",\n        \"--tool-metadata\",\n        \"invalid_format_no_equals\",\n      ]);\n\n      expectCliFailure(result);\n    });\n  });\n\n  describe(\"Metadata Impact\", () => {\n    it(\"should handle tool-specific metadata precedence over general\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createEchoTool()],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"tools/call\",\n          \"--tool-name\",\n          \"echo\",\n          \"--tool-arg\",\n          \"message=precedence test\",\n          \"--metadata\",\n          \"client=general-client\",\n          \"--tool-metadata\",\n          \"client=tool-specific-client\",\n          \"--transport\",\n          \"http\",\n        ]);\n\n        expectCliSuccess(result);\n\n        // Validate tool-specific metadata overrides general\n        const recordedRequests = server.getRecordedRequests();\n        const toolCallRequest = recordedRequests.find(\n          (r) => r.method === \"tools/call\",\n        );\n        expect(toolCallRequest).toBeDefined();\n        expect(toolCallRequest?.metadata).toEqual({\n          client: \"tool-specific-client\",\n        });\n      } finally {\n        await server.stop();\n      }\n    });\n\n    it(\"should work with resources methods\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        resources: [\n          {\n            uri: \"test://resource\",\n            name: \"test-resource\",\n            text: \"test content\",\n          },\n        ],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"resources/list\",\n          \"--metadata\",\n          \"resource_client=test-resource-client\",\n          \"--transport\",\n          \"http\",\n        ]);\n\n        expectCliSuccess(result);\n\n        // Validate metadata was sent\n        const recordedRequests = server.getRecordedRequests();\n        const resourcesListRequest = recordedRequests.find(\n          (r) => r.method === \"resources/list\",\n        );\n        expect(resourcesListRequest).toBeDefined();\n        expect(resourcesListRequest?.metadata).toEqual({\n          resource_client: \"test-resource-client\",\n        });\n      } finally {\n        await server.stop();\n      }\n    });\n\n    it(\"should work with prompts methods\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        prompts: [\n          {\n            name: \"test-prompt\",\n            description: \"A test prompt\",\n          },\n        ],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"prompts/get\",\n          \"--prompt-name\",\n          \"test-prompt\",\n          \"--metadata\",\n          \"prompt_client=test-prompt-client\",\n          \"--transport\",\n          \"http\",\n        ]);\n\n        expectCliSuccess(result);\n\n        // Validate metadata was sent\n        const recordedRequests = server.getRecordedRequests();\n        const getPromptRequest = recordedRequests.find(\n          (r) => r.method === \"prompts/get\",\n        );\n        expect(getPromptRequest).toBeDefined();\n        expect(getPromptRequest?.metadata).toEqual({\n          prompt_client: \"test-prompt-client\",\n        });\n      } finally {\n        await server.stop();\n      }\n    });\n  });\n\n  describe(\"Metadata Validation\", () => {\n    it(\"should handle special characters in keys\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createEchoTool()],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"tools/call\",\n          \"--tool-name\",\n          \"echo\",\n          \"--tool-arg\",\n          \"message=special keys test\",\n          \"--metadata\",\n          \"key-with-dashes=value1\",\n          \"key_with_underscores=value2\",\n          \"key.with.dots=value3\",\n          \"--transport\",\n          \"http\",\n        ]);\n\n        expectCliSuccess(result);\n\n        // Validate special characters in keys are preserved\n        const recordedRequests = server.getRecordedRequests();\n        const toolCallRequest = recordedRequests.find(\n          (r) => r.method === \"tools/call\",\n        );\n        expect(toolCallRequest).toBeDefined();\n        expect(toolCallRequest?.metadata).toEqual({\n          \"key-with-dashes\": \"value1\",\n          key_with_underscores: \"value2\",\n          \"key.with.dots\": \"value3\",\n        });\n      } finally {\n        await server.stop();\n      }\n    });\n  });\n\n  describe(\"Metadata Integration\", () => {\n    it(\"should work with all MCP methods\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createEchoTool()],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"tools/list\",\n          \"--metadata\",\n          \"integration_test=true\",\n          \"test_phase=all_methods\",\n          \"--transport\",\n          \"http\",\n        ]);\n\n        expectCliSuccess(result);\n\n        // Validate metadata was sent\n        const recordedRequests = server.getRecordedRequests();\n        const toolsListRequest = recordedRequests.find(\n          (r) => r.method === \"tools/list\",\n        );\n        expect(toolsListRequest).toBeDefined();\n        expect(toolsListRequest?.metadata).toEqual({\n          integration_test: \"true\",\n          test_phase: \"all_methods\",\n        });\n      } finally {\n        await server.stop();\n      }\n    });\n\n    it(\"should handle complex metadata scenario\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createEchoTool()],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"tools/call\",\n          \"--tool-name\",\n          \"echo\",\n          \"--tool-arg\",\n          \"message=complex test\",\n          \"--metadata\",\n          \"session_id=12345\",\n          \"user_id=67890\",\n          \"timestamp=2024-01-01T00:00:00Z\",\n          \"request_id=req-abc-123\",\n          \"--tool-metadata\",\n          \"tool_session=session-xyz-789\",\n          \"execution_context=test\",\n          \"priority=high\",\n          \"--transport\",\n          \"http\",\n        ]);\n\n        expectCliSuccess(result);\n\n        // Validate complex metadata merging\n        const recordedRequests = server.getRecordedRequests();\n        const toolCallRequest = recordedRequests.find(\n          (r) => r.method === \"tools/call\",\n        );\n        expect(toolCallRequest).toBeDefined();\n        expect(toolCallRequest?.metadata).toEqual({\n          session_id: \"12345\",\n          user_id: \"67890\",\n          timestamp: \"2024-01-01T00:00:00Z\",\n          request_id: \"req-abc-123\",\n          tool_session: \"session-xyz-789\",\n          execution_context: \"test\",\n          priority: \"high\",\n        });\n      } finally {\n        await server.stop();\n      }\n    });\n\n    it(\"should handle metadata parsing validation\", async () => {\n      const server = createTestServerHttp({\n        serverInfo: createTestServerInfo(),\n        tools: [createEchoTool()],\n      });\n\n      try {\n        await server.start(\"http\");\n        const serverUrl = `${server.getUrl()}/mcp`;\n\n        const result = await runCli([\n          serverUrl,\n          \"--cli\",\n          \"--method\",\n          \"tools/call\",\n          \"--tool-name\",\n          \"echo\",\n          \"--tool-arg\",\n          \"message=parsing validation test\",\n          \"--metadata\",\n          \"valid_key=valid_value\",\n          \"numeric_key=123\",\n          \"boolean_key=true\",\n          'json_key=\\'{\"test\":\"value\"}\\'',\n          \"special_key=!@#$%^&*()\",\n          \"unicode_key=🚀🎉✨\",\n          \"--transport\",\n          \"http\",\n        ]);\n\n        expectCliSuccess(result);\n\n        // Validate all value types are sent as strings\n        // Note: The CLI parses metadata values, so single-quoted JSON strings\n        // are preserved with their quotes\n        const recordedRequests = server.getRecordedRequests();\n        const toolCallRequest = recordedRequests.find(\n          (r) => r.method === \"tools/call\",\n        );\n        expect(toolCallRequest).toBeDefined();\n        expect(toolCallRequest?.metadata).toEqual({\n          valid_key: \"valid_value\",\n          numeric_key: \"123\",\n          boolean_key: \"true\",\n          json_key: '\\'{\"test\":\"value\"}\\'', // Single quotes are preserved\n          special_key: \"!@#$%^&*()\",\n          unicode_key: \"🚀🎉✨\",\n        });\n      } finally {\n        await server.stop();\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "cli/__tests__/tools.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport { runCli } from \"./helpers/cli-runner.js\";\nimport {\n  expectCliSuccess,\n  expectCliFailure,\n  expectValidJson,\n  expectJsonError,\n} from \"./helpers/assertions.js\";\nimport { getTestMcpServerCommand } from \"./helpers/test-server-stdio.js\";\n\ndescribe(\"Tool Tests\", () => {\n  describe(\"Tool Discovery\", () => {\n    it(\"should list available tools\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"tools/list\",\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"tools\");\n      expect(Array.isArray(json.tools)).toBe(true);\n      expect(json.tools.length).toBeGreaterThan(0);\n      // Validate that tools have required properties\n      expect(json.tools[0]).toHaveProperty(\"name\");\n      expect(json.tools[0]).toHaveProperty(\"description\");\n      // Validate expected tools from test-mcp-server\n      const toolNames = json.tools.map((tool: any) => tool.name);\n      expect(toolNames).toContain(\"echo\");\n      expect(toolNames).toContain(\"get-sum\");\n      expect(toolNames).toContain(\"get-annotated-message\");\n    });\n  });\n\n  describe(\"JSON Argument Parsing\", () => {\n    it(\"should handle string arguments (backward compatibility)\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"tools/call\",\n        \"--tool-name\",\n        \"echo\",\n        \"--tool-arg\",\n        \"message=hello world\",\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"content\");\n      expect(Array.isArray(json.content)).toBe(true);\n      expect(json.content.length).toBeGreaterThan(0);\n      expect(json.content[0]).toHaveProperty(\"type\", \"text\");\n      expect(json.content[0].text).toBe(\"Echo: hello world\");\n    });\n\n    it(\"should handle integer number arguments\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"tools/call\",\n        \"--tool-name\",\n        \"get-sum\",\n        \"--tool-arg\",\n        \"a=42\",\n        \"b=58\",\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"content\");\n      expect(Array.isArray(json.content)).toBe(true);\n      expect(json.content.length).toBeGreaterThan(0);\n      expect(json.content[0]).toHaveProperty(\"type\", \"text\");\n      // test-mcp-server returns JSON with {result: a+b}\n      const resultData = JSON.parse(json.content[0].text);\n      expect(resultData.result).toBe(100);\n    });\n\n    it(\"should handle decimal number arguments\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"tools/call\",\n        \"--tool-name\",\n        \"get-sum\",\n        \"--tool-arg\",\n        \"a=19.99\",\n        \"b=20.01\",\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"content\");\n      expect(Array.isArray(json.content)).toBe(true);\n      expect(json.content.length).toBeGreaterThan(0);\n      expect(json.content[0]).toHaveProperty(\"type\", \"text\");\n      // test-mcp-server returns JSON with {result: a+b}\n      const resultData = JSON.parse(json.content[0].text);\n      expect(resultData.result).toBeCloseTo(40.0, 2);\n    });\n\n    it(\"should handle boolean arguments - true\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"tools/call\",\n        \"--tool-name\",\n        \"get-annotated-message\",\n        \"--tool-arg\",\n        \"messageType=success\",\n        \"includeImage=true\",\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"content\");\n      expect(Array.isArray(json.content)).toBe(true);\n      // Should have both text and image content\n      expect(json.content.length).toBeGreaterThan(1);\n      const hasImage = json.content.some((item: any) => item.type === \"image\");\n      expect(hasImage).toBe(true);\n    });\n\n    it(\"should handle boolean arguments - false\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"tools/call\",\n        \"--tool-name\",\n        \"get-annotated-message\",\n        \"--tool-arg\",\n        \"messageType=error\",\n        \"includeImage=false\",\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"content\");\n      expect(Array.isArray(json.content)).toBe(true);\n      // Should only have text content, no image\n      const hasImage = json.content.some((item: any) => item.type === \"image\");\n      expect(hasImage).toBe(false);\n      // test-mcp-server returns \"This is a {messageType} message\"\n      expect(json.content[0].text.toLowerCase()).toContain(\"error\");\n    });\n\n    it(\"should handle null arguments\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"tools/call\",\n        \"--tool-name\",\n        \"echo\",\n        \"--tool-arg\",\n        'message=\"null\"',\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"content\");\n      expect(Array.isArray(json.content)).toBe(true);\n      expect(json.content[0]).toHaveProperty(\"type\", \"text\");\n      // The string \"null\" should be passed through\n      expect(json.content[0].text).toBe(\"Echo: null\");\n    });\n\n    it(\"should handle multiple arguments with mixed types\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"tools/call\",\n        \"--tool-name\",\n        \"get-sum\",\n        \"--tool-arg\",\n        \"a=42.5\",\n        \"b=57.5\",\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"content\");\n      expect(Array.isArray(json.content)).toBe(true);\n      expect(json.content.length).toBeGreaterThan(0);\n      expect(json.content[0]).toHaveProperty(\"type\", \"text\");\n      // test-mcp-server returns JSON with {result: a+b}\n      const resultData = JSON.parse(json.content[0].text);\n      expect(resultData.result).toBeCloseTo(100.0, 1);\n    });\n  });\n\n  describe(\"JSON Parsing Edge Cases\", () => {\n    it(\"should fall back to string for invalid JSON\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"tools/call\",\n        \"--tool-name\",\n        \"echo\",\n        \"--tool-arg\",\n        \"message={invalid json}\",\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"content\");\n      expect(Array.isArray(json.content)).toBe(true);\n      expect(json.content[0]).toHaveProperty(\"type\", \"text\");\n      // Should treat invalid JSON as a string\n      expect(json.content[0].text).toBe(\"Echo: {invalid json}\");\n    });\n\n    it(\"should handle empty string value\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"tools/call\",\n        \"--tool-name\",\n        \"echo\",\n        \"--tool-arg\",\n        'message=\"\"',\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"content\");\n      expect(Array.isArray(json.content)).toBe(true);\n      expect(json.content[0]).toHaveProperty(\"type\", \"text\");\n      // Empty string should be preserved\n      expect(json.content[0].text).toBe(\"Echo: \");\n    });\n\n    it(\"should handle special characters in strings\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"tools/call\",\n        \"--tool-name\",\n        \"echo\",\n        \"--tool-arg\",\n        'message=\"C:\\\\\\\\Users\\\\\\\\test\"',\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"content\");\n      expect(Array.isArray(json.content)).toBe(true);\n      expect(json.content[0]).toHaveProperty(\"type\", \"text\");\n      // Special characters should be preserved\n      expect(json.content[0].text).toContain(\"C:\");\n      expect(json.content[0].text).toContain(\"Users\");\n      expect(json.content[0].text).toContain(\"test\");\n    });\n\n    it(\"should handle unicode characters\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"tools/call\",\n        \"--tool-name\",\n        \"echo\",\n        \"--tool-arg\",\n        'message=\"🚀🎉✨\"',\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"content\");\n      expect(Array.isArray(json.content)).toBe(true);\n      expect(json.content[0]).toHaveProperty(\"type\", \"text\");\n      // Unicode characters should be preserved\n      expect(json.content[0].text).toContain(\"🚀\");\n      expect(json.content[0].text).toContain(\"🎉\");\n      expect(json.content[0].text).toContain(\"✨\");\n    });\n\n    it(\"should handle arguments with equals signs in values\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"tools/call\",\n        \"--tool-name\",\n        \"echo\",\n        \"--tool-arg\",\n        \"message=2+2=4\",\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"content\");\n      expect(Array.isArray(json.content)).toBe(true);\n      expect(json.content[0]).toHaveProperty(\"type\", \"text\");\n      // Equals signs in values should be preserved\n      expect(json.content[0].text).toBe(\"Echo: 2+2=4\");\n    });\n\n    it(\"should handle base64-like strings\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const base64String =\n        \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=\";\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"tools/call\",\n        \"--tool-name\",\n        \"echo\",\n        \"--tool-arg\",\n        `message=${base64String}`,\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"content\");\n      expect(Array.isArray(json.content)).toBe(true);\n      expect(json.content[0]).toHaveProperty(\"type\", \"text\");\n      // Base64-like strings should be preserved\n      expect(json.content[0].text).toBe(`Echo: ${base64String}`);\n    });\n  });\n\n  describe(\"Tool Error Handling\", () => {\n    it(\"should fail with nonexistent tool\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"tools/call\",\n        \"--tool-name\",\n        \"nonexistent_tool\",\n        \"--tool-arg\",\n        \"message=test\",\n      ]);\n\n      // CLI returns exit code 0 but includes isError: true in JSON\n      expectJsonError(result);\n    });\n\n    it(\"should fail when tool name is missing\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"tools/call\",\n        \"--tool-arg\",\n        \"message=test\",\n      ]);\n\n      expectCliFailure(result);\n    });\n\n    it(\"should fail with invalid tool argument format\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"tools/call\",\n        \"--tool-name\",\n        \"echo\",\n        \"--tool-arg\",\n        \"invalid_format_no_equals\",\n      ]);\n\n      expectCliFailure(result);\n    });\n  });\n\n  describe(\"Prompt JSON Arguments\", () => {\n    it(\"should handle prompt with JSON arguments\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"prompts/get\",\n        \"--prompt-name\",\n        \"args-prompt\",\n        \"--prompt-args\",\n        \"city=New York\",\n        \"state=NY\",\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"messages\");\n      expect(Array.isArray(json.messages)).toBe(true);\n      expect(json.messages.length).toBeGreaterThan(0);\n      expect(json.messages[0]).toHaveProperty(\"content\");\n      expect(json.messages[0].content).toHaveProperty(\"type\", \"text\");\n      // Validate that the arguments were actually used in the response\n      // test-mcp-server formats it as \"This is a prompt with arguments: city={city}, state={state}\"\n      expect(json.messages[0].content.text).toContain(\"city=New York\");\n      expect(json.messages[0].content.text).toContain(\"state=NY\");\n    });\n\n    it(\"should handle prompt with simple arguments\", async () => {\n      // Note: simple-prompt doesn't accept arguments, but the CLI should still\n      // accept the command and the server should ignore the arguments\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"prompts/get\",\n        \"--prompt-name\",\n        \"simple-prompt\",\n        \"--prompt-args\",\n        \"name=test\",\n        \"count=5\",\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"messages\");\n      expect(Array.isArray(json.messages)).toBe(true);\n      expect(json.messages.length).toBeGreaterThan(0);\n      expect(json.messages[0]).toHaveProperty(\"content\");\n      expect(json.messages[0].content).toHaveProperty(\"type\", \"text\");\n      // test-mcp-server's simple-prompt returns standard message (ignoring args)\n      expect(json.messages[0].content.text).toBe(\n        \"This is a simple prompt for testing purposes.\",\n      );\n    });\n  });\n\n  describe(\"Backward Compatibility\", () => {\n    it(\"should support existing string-only usage\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"tools/call\",\n        \"--tool-name\",\n        \"echo\",\n        \"--tool-arg\",\n        \"message=hello\",\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"content\");\n      expect(Array.isArray(json.content)).toBe(true);\n      expect(json.content[0]).toHaveProperty(\"type\", \"text\");\n      expect(json.content[0].text).toBe(\"Echo: hello\");\n    });\n\n    it(\"should support multiple string arguments\", async () => {\n      const { command, args } = getTestMcpServerCommand();\n      const result = await runCli([\n        command,\n        ...args,\n        \"--cli\",\n        \"--method\",\n        \"tools/call\",\n        \"--tool-name\",\n        \"get-sum\",\n        \"--tool-arg\",\n        \"a=10\",\n        \"b=20\",\n      ]);\n\n      expectCliSuccess(result);\n      const json = expectValidJson(result);\n      expect(json).toHaveProperty(\"content\");\n      expect(Array.isArray(json.content)).toBe(true);\n      expect(json.content.length).toBeGreaterThan(0);\n      expect(json.content[0]).toHaveProperty(\"type\", \"text\");\n      // test-mcp-server returns JSON with {result: a+b}\n      const resultData = JSON.parse(json.content[0].text);\n      expect(resultData.result).toBe(30);\n    });\n  });\n});\n"
  },
  {
    "path": "cli/package.json",
    "content": "{\n  \"name\": \"@modelcontextprotocol/inspector-cli\",\n  \"version\": \"0.21.1\",\n  \"description\": \"CLI for the Model Context Protocol inspector\",\n  \"license\": \"SEE LICENSE IN LICENSE\",\n  \"author\": \"Model Context Protocol a Series of LF Projects, LLC.\",\n  \"homepage\": \"https://modelcontextprotocol.io\",\n  \"bugs\": \"https://github.com/modelcontextprotocol/inspector/issues\",\n  \"main\": \"build/cli.js\",\n  \"type\": \"module\",\n  \"bin\": {\n    \"mcp-inspector-cli\": \"build/cli.js\"\n  },\n  \"files\": [\n    \"build\",\n    \"LICENSE\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsc\",\n    \"postbuild\": \"node scripts/make-executable.js\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest\",\n    \"test:cli\": \"vitest run cli.test.ts\",\n    \"test:cli-tools\": \"vitest run tools.test.ts\",\n    \"test:cli-headers\": \"vitest run headers.test.ts\",\n    \"test:cli-metadata\": \"vitest run metadata.test.ts\"\n  },\n  \"devDependencies\": {\n    \"@types/express\": \"^5.0.0\",\n    \"tsx\": \"^4.7.0\",\n    \"vitest\": \"^4.0.17\"\n  },\n  \"dependencies\": {\n    \"@modelcontextprotocol/sdk\": \"^1.25.2\",\n    \"commander\": \"^13.1.0\",\n    \"express\": \"^5.2.1\",\n    \"spawn-rx\": \"^5.1.2\"\n  }\n}\n"
  },
  {
    "path": "cli/scripts/make-executable.js",
    "content": "/**\n * Cross-platform script to make a file executable\n */\nimport { promises as fs } from \"fs\";\nimport { platform } from \"os\";\nimport { execSync } from \"child_process\";\nimport path from \"path\";\n\nconst TARGET_FILE = path.resolve(\"build/cli.js\");\n\nasync function makeExecutable() {\n  try {\n    // On Unix-like systems (Linux, macOS), use chmod\n    if (platform() !== \"win32\") {\n      execSync(`chmod +x \"${TARGET_FILE}\"`);\n      console.log(\"Made file executable with chmod\");\n    } else {\n      // On Windows, no need to make files \"executable\" in the Unix sense\n      // Just ensure the file exists\n      await fs.access(TARGET_FILE);\n      console.log(\"File exists and is accessible on Windows\");\n    }\n  } catch (error) {\n    console.error(\"Error making file executable:\", error);\n    process.exit(1);\n  }\n}\n\nmakeExecutable();\n"
  },
  {
    "path": "cli/src/cli.ts",
    "content": "#!/usr/bin/env node\n\nimport { Command } from \"commander\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { dirname, resolve } from \"path\";\nimport { spawnPromise } from \"spawn-rx\";\nimport { fileURLToPath } from \"url\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\ntype Args = {\n  command: string;\n  args: string[];\n  envArgs: Record<string, string>;\n  cli: boolean;\n  transport?: \"stdio\" | \"sse\" | \"streamable-http\";\n  serverUrl?: string;\n  headers?: Record<string, string>;\n};\n\ntype CliOptions = {\n  e?: Record<string, string>;\n  config?: string;\n  server?: string;\n  cli?: boolean;\n  transport?: string;\n  serverUrl?: string;\n  header?: Record<string, string>;\n};\n\ntype ServerConfig =\n  | {\n      type: \"stdio\";\n      command: string;\n      args?: string[];\n      env?: Record<string, string>;\n    }\n  | {\n      type: \"sse\" | \"streamable-http\";\n      url: string;\n      note?: string;\n    };\n\nfunction handleError(error: unknown): never {\n  let message: string;\n\n  if (error instanceof Error) {\n    message = error.message;\n  } else if (typeof error === \"string\") {\n    message = error;\n  } else {\n    message = \"Unknown error\";\n  }\n\n  console.error(message);\n\n  process.exit(1);\n}\n\nfunction delay(ms: number): Promise<void> {\n  return new Promise((resolve) => setTimeout(resolve, ms, true));\n}\n\nasync function runWebClient(args: Args): Promise<void> {\n  // Path to the client entry point\n  const inspectorClientPath = resolve(\n    __dirname,\n    \"../../\",\n    \"client\",\n    \"bin\",\n    \"start.js\",\n  );\n\n  const abort = new AbortController();\n  let cancelled: boolean = false;\n  process.on(\"SIGINT\", () => {\n    cancelled = true;\n    abort.abort();\n  });\n\n  // Build arguments to pass to start.js\n  const startArgs: string[] = [];\n\n  // Pass environment variables\n  for (const [key, value] of Object.entries(args.envArgs)) {\n    startArgs.push(\"-e\", `${key}=${value}`);\n  }\n\n  // Pass transport type if specified\n  if (args.transport) {\n    startArgs.push(\"--transport\", args.transport);\n  }\n\n  // Pass server URL if specified\n  if (args.serverUrl) {\n    startArgs.push(\"--server-url\", args.serverUrl);\n  }\n\n  // Pass command and args (using -- to separate them)\n  if (args.command) {\n    startArgs.push(\"--\", args.command, ...args.args);\n  }\n\n  try {\n    await spawnPromise(\"node\", [inspectorClientPath, ...startArgs], {\n      signal: abort.signal,\n      echoOutput: true,\n      // pipe the stdout through here, prevents issues with buffering and\n      // dropping the end of console.out after 8192 chars due to node\n      // closing the stdout pipe before the output has finished flushing\n      stdio: \"inherit\",\n    });\n  } catch (e) {\n    if (!cancelled || process.env.DEBUG) throw e;\n  }\n}\n\nasync function runCli(args: Args): Promise<void> {\n  const projectRoot = resolve(__dirname, \"..\");\n  const cliPath = resolve(projectRoot, \"build\", \"index.js\");\n\n  const abort = new AbortController();\n\n  let cancelled = false;\n\n  process.on(\"SIGINT\", () => {\n    cancelled = true;\n    abort.abort();\n  });\n\n  try {\n    // Build CLI arguments\n    const cliArgs = [cliPath];\n\n    // Add target URL/command first\n    cliArgs.push(args.command, ...args.args);\n\n    // Add transport flag if specified\n    if (args.transport && args.transport !== \"stdio\") {\n      // Convert streamable-http back to http for CLI mode\n      const cliTransport =\n        args.transport === \"streamable-http\" ? \"http\" : args.transport;\n      cliArgs.push(\"--transport\", cliTransport);\n    }\n\n    // Add headers if specified\n    if (args.headers) {\n      for (const [key, value] of Object.entries(args.headers)) {\n        cliArgs.push(\"--header\", `${key}: ${value}`);\n      }\n    }\n\n    await spawnPromise(\"node\", cliArgs, {\n      env: { ...process.env, ...args.envArgs },\n      signal: abort.signal,\n      echoOutput: true,\n      // pipe the stdout through here, prevents issues with buffering and\n      // dropping the end of console.out after 8192 chars due to node\n      // closing the stdout pipe before the output has finished flushing\n      stdio: \"inherit\",\n    });\n  } catch (e) {\n    if (!cancelled || process.env.DEBUG) {\n      throw e;\n    }\n  }\n}\n\nfunction loadConfigFile(configPath: string, serverName: string): ServerConfig {\n  try {\n    const resolvedConfigPath = path.isAbsolute(configPath)\n      ? configPath\n      : path.resolve(process.cwd(), configPath);\n\n    if (!fs.existsSync(resolvedConfigPath)) {\n      throw new Error(`Config file not found: ${resolvedConfigPath}`);\n    }\n\n    const configContent = fs.readFileSync(resolvedConfigPath, \"utf8\");\n    const parsedConfig = JSON.parse(configContent);\n\n    if (!parsedConfig.mcpServers || !parsedConfig.mcpServers[serverName]) {\n      const availableServers = Object.keys(parsedConfig.mcpServers || {}).join(\n        \", \",\n      );\n      throw new Error(\n        `Server '${serverName}' not found in config file. Available servers: ${availableServers}`,\n      );\n    }\n\n    const serverConfig = parsedConfig.mcpServers[serverName];\n\n    return serverConfig;\n  } catch (err: unknown) {\n    if (err instanceof SyntaxError) {\n      throw new Error(`Invalid JSON in config file: ${err.message}`);\n    }\n\n    throw err;\n  }\n}\n\nfunction parseKeyValuePair(\n  value: string,\n  previous: Record<string, string> = {},\n): Record<string, string> {\n  const parts = value.split(\"=\");\n  const key = parts[0];\n  const val = parts.slice(1).join(\"=\");\n\n  if (val === undefined || val === \"\") {\n    throw new Error(\n      `Invalid parameter format: ${value}. Use key=value format.`,\n    );\n  }\n\n  return { ...previous, [key as string]: val };\n}\n\nfunction parseHeaderPair(\n  value: string,\n  previous: Record<string, string> = {},\n): Record<string, string> {\n  const colonIndex = value.indexOf(\":\");\n\n  if (colonIndex === -1) {\n    throw new Error(\n      `Invalid header format: ${value}. Use \"HeaderName: Value\" format.`,\n    );\n  }\n\n  const key = value.slice(0, colonIndex).trim();\n  const val = value.slice(colonIndex + 1).trim();\n\n  if (key === \"\" || val === \"\") {\n    throw new Error(\n      `Invalid header format: ${value}. Use \"HeaderName: Value\" format.`,\n    );\n  }\n\n  return { ...previous, [key]: val };\n}\n\nfunction parseArgs(): Args {\n  const program = new Command();\n\n  const argSeparatorIndex = process.argv.indexOf(\"--\");\n  let preArgs = process.argv;\n  let postArgs: string[] = [];\n\n  if (argSeparatorIndex !== -1) {\n    preArgs = process.argv.slice(0, argSeparatorIndex);\n    postArgs = process.argv.slice(argSeparatorIndex + 1);\n  }\n\n  program\n    .name(\"inspector-bin\")\n    .allowExcessArguments()\n    .allowUnknownOption()\n    .option(\n      \"-e <env>\",\n      \"environment variables in KEY=VALUE format\",\n      parseKeyValuePair,\n      {},\n    )\n    .option(\"--config <path>\", \"config file path\")\n    .option(\"--server <n>\", \"server name from config file\")\n    .option(\"--cli\", \"enable CLI mode\")\n    .option(\"--transport <type>\", \"transport type (stdio, sse, http)\")\n    .option(\"--server-url <url>\", \"server URL for SSE/HTTP transport\")\n    .option(\n      \"--header <headers...>\",\n      'HTTP headers as \"HeaderName: Value\" pairs (for HTTP/SSE transports)',\n      parseHeaderPair,\n      {},\n    );\n\n  // Parse only the arguments before --\n  program.parse(preArgs);\n\n  const options = program.opts() as CliOptions;\n  const remainingArgs = program.args;\n\n  // Add back any arguments that came after --\n  const finalArgs = [...remainingArgs, ...postArgs];\n\n  // Validate config and server options\n  if (!options.config && options.server) {\n    throw new Error(\"--server requires --config to be specified\");\n  }\n\n  // If config is provided without server, try to auto-select\n  if (options.config && !options.server) {\n    const configContent = fs.readFileSync(\n      path.isAbsolute(options.config)\n        ? options.config\n        : path.resolve(process.cwd(), options.config),\n      \"utf8\",\n    );\n    const parsedConfig = JSON.parse(configContent);\n    const servers = Object.keys(parsedConfig.mcpServers || {});\n\n    if (servers.length === 1) {\n      // Use the only server if there's just one\n      options.server = servers[0];\n    } else if (servers.length === 0) {\n      throw new Error(\"No servers found in config file\");\n    } else {\n      // Multiple servers, require explicit selection\n      throw new Error(\n        `Multiple servers found in config file. Please specify one with --server.\\nAvailable servers: ${servers.join(\", \")}`,\n      );\n    }\n  }\n\n  // If config file is specified, load and use the options from the file. We must merge the args\n  // from the command line and the file together, or we will miss the method options (--method,\n  // etc.)\n  if (options.config && options.server) {\n    const config = loadConfigFile(options.config, options.server);\n\n    if (config.type === \"stdio\") {\n      return {\n        command: config.command,\n        args: [...(config.args || []), ...finalArgs],\n        envArgs: { ...(config.env || {}), ...(options.e || {}) },\n        cli: options.cli || false,\n        transport: \"stdio\",\n        headers: options.header,\n      };\n    } else if (config.type === \"sse\" || config.type === \"streamable-http\") {\n      return {\n        command: config.url,\n        args: finalArgs,\n        envArgs: options.e || {},\n        cli: options.cli || false,\n        transport: config.type,\n        serverUrl: config.url,\n        headers: options.header,\n      };\n    } else {\n      // Backwards compatibility: if no type field, assume stdio\n      return {\n        command: (config as any).command || \"\",\n        args: [...((config as any).args || []), ...finalArgs],\n        envArgs: { ...((config as any).env || {}), ...(options.e || {}) },\n        cli: options.cli || false,\n        transport: \"stdio\",\n        headers: options.header,\n      };\n    }\n  }\n\n  // Otherwise use command line arguments\n  const command = finalArgs[0] || \"\";\n  const args = finalArgs.slice(1);\n\n  // Map \"http\" shorthand to \"streamable-http\"\n  let transport = options.transport;\n  if (transport === \"http\") {\n    transport = \"streamable-http\";\n  }\n\n  return {\n    command,\n    args,\n    envArgs: options.e || {},\n    cli: options.cli || false,\n    transport: transport as \"stdio\" | \"sse\" | \"streamable-http\" | undefined,\n    serverUrl: options.serverUrl,\n    headers: options.header,\n  };\n}\n\nasync function main(): Promise<void> {\n  process.on(\"uncaughtException\", (error) => {\n    handleError(error);\n  });\n\n  try {\n    const args = parseArgs();\n\n    if (args.cli) {\n      await runCli(args);\n    } else {\n      await runWebClient(args);\n    }\n  } catch (error) {\n    handleError(error);\n  }\n}\n\nmain();\n"
  },
  {
    "path": "cli/src/client/connection.ts",
    "content": "import { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport type { Transport } from \"@modelcontextprotocol/sdk/shared/transport.js\";\nimport { McpResponse } from \"./types.js\";\n\nexport const validLogLevels = [\n  \"trace\",\n  \"debug\",\n  \"info\",\n  \"warn\",\n  \"error\",\n] as const;\n\nexport type LogLevel = (typeof validLogLevels)[number];\n\nexport async function connect(\n  client: Client,\n  transport: Transport,\n): Promise<void> {\n  try {\n    await client.connect(transport);\n\n    if (client.getServerCapabilities()?.logging) {\n      // default logging level is undefined in the spec, but the user of the\n      // inspector most likely wants debug.\n      await client.setLoggingLevel(\"debug\");\n    }\n  } catch (error) {\n    throw new Error(\n      `Failed to connect to MCP server: ${error instanceof Error ? error.message : String(error)}`,\n    );\n  }\n}\n\nexport async function disconnect(transport: Transport): Promise<void> {\n  try {\n    await transport.close();\n  } catch (error) {\n    throw new Error(\n      `Failed to disconnect from MCP server: ${error instanceof Error ? error.message : String(error)}`,\n    );\n  }\n}\n\n// Set logging level\nexport async function setLoggingLevel(\n  client: Client,\n  level: LogLevel,\n): Promise<McpResponse> {\n  try {\n    const response = await client.setLoggingLevel(level as any);\n    return response;\n  } catch (error) {\n    throw new Error(\n      `Failed to set logging level: ${error instanceof Error ? error.message : String(error)}`,\n    );\n  }\n}\n"
  },
  {
    "path": "cli/src/client/index.ts",
    "content": "// Re-export everything from the client modules\nexport * from \"./connection.js\";\nexport * from \"./prompts.js\";\nexport * from \"./resources.js\";\nexport * from \"./tools.js\";\nexport * from \"./types.js\";\n"
  },
  {
    "path": "cli/src/client/prompts.ts",
    "content": "import { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { McpResponse } from \"./types.js\";\n\n// JSON value type matching the client utils\ntype JsonValue =\n  | string\n  | number\n  | boolean\n  | null\n  | undefined\n  | JsonValue[]\n  | { [key: string]: JsonValue };\n\n// List available prompts\nexport async function listPrompts(\n  client: Client,\n  metadata?: Record<string, string>,\n): Promise<McpResponse> {\n  try {\n    const params =\n      metadata && Object.keys(metadata).length > 0 ? { _meta: metadata } : {};\n    const response = await client.listPrompts(params);\n    return response;\n  } catch (error) {\n    throw new Error(\n      `Failed to list prompts: ${error instanceof Error ? error.message : String(error)}`,\n    );\n  }\n}\n\n// Get a prompt\nexport async function getPrompt(\n  client: Client,\n  name: string,\n  args?: Record<string, JsonValue>,\n  metadata?: Record<string, string>,\n): Promise<McpResponse> {\n  try {\n    // Convert all arguments to strings for prompt arguments\n    const stringArgs: Record<string, string> = {};\n    if (args) {\n      for (const [key, value] of Object.entries(args)) {\n        if (typeof value === \"string\") {\n          stringArgs[key] = value;\n        } else if (value === null || value === undefined) {\n          stringArgs[key] = String(value);\n        } else {\n          stringArgs[key] = JSON.stringify(value);\n        }\n      }\n    }\n\n    const params: any = {\n      name,\n      arguments: stringArgs,\n    };\n\n    if (metadata && Object.keys(metadata).length > 0) {\n      params._meta = metadata;\n    }\n\n    const response = await client.getPrompt(params);\n\n    return response;\n  } catch (error) {\n    throw new Error(\n      `Failed to get prompt: ${error instanceof Error ? error.message : String(error)}`,\n    );\n  }\n}\n"
  },
  {
    "path": "cli/src/client/resources.ts",
    "content": "import { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { McpResponse } from \"./types.js\";\n\n// List available resources\nexport async function listResources(\n  client: Client,\n  metadata?: Record<string, string>,\n): Promise<McpResponse> {\n  try {\n    const params =\n      metadata && Object.keys(metadata).length > 0 ? { _meta: metadata } : {};\n    const response = await client.listResources(params);\n    return response;\n  } catch (error) {\n    throw new Error(\n      `Failed to list resources: ${error instanceof Error ? error.message : String(error)}`,\n    );\n  }\n}\n\n// Read a resource\nexport async function readResource(\n  client: Client,\n  uri: string,\n  metadata?: Record<string, string>,\n): Promise<McpResponse> {\n  try {\n    const params: any = { uri };\n    if (metadata && Object.keys(metadata).length > 0) {\n      params._meta = metadata;\n    }\n    const response = await client.readResource(params);\n    return response;\n  } catch (error) {\n    throw new Error(\n      `Failed to read resource ${uri}: ${error instanceof Error ? error.message : String(error)}`,\n    );\n  }\n}\n\n// List resource templates\nexport async function listResourceTemplates(\n  client: Client,\n  metadata?: Record<string, string>,\n): Promise<McpResponse> {\n  try {\n    const params =\n      metadata && Object.keys(metadata).length > 0 ? { _meta: metadata } : {};\n    const response = await client.listResourceTemplates(params);\n    return response;\n  } catch (error) {\n    throw new Error(\n      `Failed to list resource templates: ${error instanceof Error ? error.message : String(error)}`,\n    );\n  }\n}\n"
  },
  {
    "path": "cli/src/client/tools.ts",
    "content": "import { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { Tool } from \"@modelcontextprotocol/sdk/types.js\";\nimport { McpResponse } from \"./types.js\";\n\n// JSON value type matching the client utils\ntype JsonValue =\n  | string\n  | number\n  | boolean\n  | null\n  | undefined\n  | JsonValue[]\n  | { [key: string]: JsonValue };\n\ntype JsonSchemaType = {\n  type: \"string\" | \"number\" | \"integer\" | \"boolean\" | \"array\" | \"object\";\n  description?: string;\n  properties?: Record<string, JsonSchemaType>;\n  items?: JsonSchemaType;\n};\n\nexport async function listTools(\n  client: Client,\n  metadata?: Record<string, string>,\n): Promise<McpResponse> {\n  try {\n    const params =\n      metadata && Object.keys(metadata).length > 0 ? { _meta: metadata } : {};\n    const response = await client.listTools(params);\n    return response;\n  } catch (error) {\n    throw new Error(\n      `Failed to list tools: ${error instanceof Error ? error.message : String(error)}`,\n    );\n  }\n}\n\nfunction convertParameterValue(\n  value: string,\n  schema: JsonSchemaType,\n): JsonValue {\n  if (!value) {\n    return value;\n  }\n\n  if (schema.type === \"number\" || schema.type === \"integer\") {\n    return Number(value);\n  }\n\n  if (schema.type === \"boolean\") {\n    return value.toLowerCase() === \"true\";\n  }\n\n  if (schema.type === \"object\" || schema.type === \"array\") {\n    try {\n      return JSON.parse(value) as JsonValue;\n    } catch (error) {\n      return value;\n    }\n  }\n\n  return value;\n}\n\nfunction convertParameters(\n  tool: Tool,\n  params: Record<string, string>,\n): Record<string, JsonValue> {\n  const result: Record<string, JsonValue> = {};\n  const properties = tool.inputSchema.properties || {};\n\n  for (const [key, value] of Object.entries(params)) {\n    const paramSchema = properties[key] as JsonSchemaType | undefined;\n\n    if (paramSchema) {\n      result[key] = convertParameterValue(value, paramSchema);\n    } else {\n      // If no schema is found for this parameter, keep it as string\n      result[key] = value;\n    }\n  }\n\n  return result;\n}\n\nexport async function callTool(\n  client: Client,\n  name: string,\n  args: Record<string, JsonValue>,\n  generalMetadata?: Record<string, string>,\n  toolSpecificMetadata?: Record<string, string>,\n): Promise<McpResponse> {\n  try {\n    const toolsResponse = await listTools(client, generalMetadata);\n    const tools = toolsResponse.tools as Tool[];\n    const tool = tools.find((t) => t.name === name);\n\n    let convertedArgs: Record<string, JsonValue> = args;\n\n    if (tool) {\n      // Convert parameters based on the tool's schema, but only for string values\n      // since we now accept pre-parsed values from the CLI\n      const stringArgs: Record<string, string> = {};\n      for (const [key, value] of Object.entries(args)) {\n        if (typeof value === \"string\") {\n          stringArgs[key] = value;\n        }\n      }\n\n      if (Object.keys(stringArgs).length > 0) {\n        const convertedStringArgs = convertParameters(tool, stringArgs);\n        convertedArgs = { ...args, ...convertedStringArgs };\n      }\n    }\n\n    // Merge general metadata with tool-specific metadata\n    // Tool-specific metadata takes precedence over general metadata\n    let mergedMetadata: Record<string, string> | undefined;\n    if (generalMetadata || toolSpecificMetadata) {\n      mergedMetadata = {\n        ...(generalMetadata || {}),\n        ...(toolSpecificMetadata || {}),\n      };\n    }\n\n    const response = await client.callTool({\n      name: name,\n      arguments: convertedArgs,\n      _meta:\n        mergedMetadata && Object.keys(mergedMetadata).length > 0\n          ? mergedMetadata\n          : undefined,\n    });\n    return response;\n  } catch (error) {\n    throw new Error(\n      `Failed to call tool ${name}: ${error instanceof Error ? error.message : String(error)}`,\n    );\n  }\n}\n"
  },
  {
    "path": "cli/src/client/types.ts",
    "content": "export type McpResponse = Record<string, unknown>;\n"
  },
  {
    "path": "cli/src/error-handler.ts",
    "content": "function formatError(error: unknown): string {\n  let message: string;\n\n  if (error instanceof Error) {\n    message = error.message;\n  } else if (typeof error === \"string\") {\n    message = error;\n  } else {\n    message = \"Unknown error\";\n  }\n\n  return message;\n}\n\nexport function handleError(error: unknown): never {\n  const errorMessage = formatError(error);\n  console.error(errorMessage);\n\n  process.exit(1);\n}\n"
  },
  {
    "path": "cli/src/index.ts",
    "content": "#!/usr/bin/env node\n\nimport * as fs from \"fs\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { Command } from \"commander\";\nimport {\n  callTool,\n  connect,\n  disconnect,\n  getPrompt,\n  listPrompts,\n  listResources,\n  listResourceTemplates,\n  listTools,\n  LogLevel,\n  McpResponse,\n  readResource,\n  setLoggingLevel,\n  validLogLevels,\n} from \"./client/index.js\";\nimport { handleError } from \"./error-handler.js\";\nimport { createTransport, TransportOptions } from \"./transport.js\";\nimport { awaitableLog } from \"./utils/awaitable-log.js\";\n\n// JSON value type for CLI arguments\ntype JsonValue =\n  | string\n  | number\n  | boolean\n  | null\n  | undefined\n  | JsonValue[]\n  | { [key: string]: JsonValue };\n\ntype Args = {\n  target: string[];\n  method?: string;\n  promptName?: string;\n  promptArgs?: Record<string, JsonValue>;\n  uri?: string;\n  logLevel?: LogLevel;\n  toolName?: string;\n  toolArg?: Record<string, JsonValue>;\n  toolMeta?: Record<string, string>;\n  transport?: \"sse\" | \"stdio\" | \"http\";\n  headers?: Record<string, string>;\n  metadata?: Record<string, string>;\n};\n\nfunction createTransportOptions(\n  target: string[],\n  transport?: \"sse\" | \"stdio\" | \"http\",\n  headers?: Record<string, string>,\n): TransportOptions {\n  if (target.length === 0) {\n    throw new Error(\n      \"Target is required. Specify a URL or a command to execute.\",\n    );\n  }\n\n  const [command, ...commandArgs] = target;\n\n  if (!command) {\n    throw new Error(\"Command is required.\");\n  }\n\n  const isUrl = command.startsWith(\"http://\") || command.startsWith(\"https://\");\n\n  if (isUrl && commandArgs.length > 0) {\n    throw new Error(\"Arguments cannot be passed to a URL-based MCP server.\");\n  }\n\n  let transportType: \"sse\" | \"stdio\" | \"http\";\n  if (transport) {\n    if (!isUrl && transport !== \"stdio\") {\n      throw new Error(\"Only stdio transport can be used with local commands.\");\n    }\n    if (isUrl && transport === \"stdio\") {\n      throw new Error(\"stdio transport cannot be used with URLs.\");\n    }\n    transportType = transport;\n  } else if (isUrl) {\n    const url = new URL(command);\n    if (url.pathname.endsWith(\"/mcp\")) {\n      transportType = \"http\";\n    } else if (url.pathname.endsWith(\"/sse\")) {\n      transportType = \"sse\";\n    } else {\n      transportType = \"sse\";\n    }\n  } else {\n    transportType = \"stdio\";\n  }\n\n  return {\n    transportType,\n    command: isUrl ? undefined : command,\n    args: isUrl ? undefined : commandArgs,\n    url: isUrl ? command : undefined,\n    headers,\n  };\n}\n\nasync function callMethod(args: Args): Promise<void> {\n  // Read package.json to get name and version for client identity\n  const pathA = \"../package.json\"; // We're in package @modelcontextprotocol/inspector-cli\n  const pathB = \"../../package.json\"; // We're in package @modelcontextprotocol/inspector\n  let packageJson: { name: string; version: string };\n  let packageJsonData = await import(fs.existsSync(pathA) ? pathA : pathB, {\n    with: { type: \"json\" },\n  });\n  packageJson = packageJsonData.default;\n\n  const transportOptions = createTransportOptions(\n    args.target,\n    args.transport,\n    args.headers,\n  );\n  const transport = createTransport(transportOptions);\n\n  const [, name = packageJson.name] = packageJson.name.split(\"/\");\n  const version = packageJson.version;\n  const clientIdentity = { name, version };\n\n  const client = new Client(clientIdentity);\n\n  try {\n    await connect(client, transport);\n\n    let result: McpResponse;\n\n    // Tools methods\n    if (args.method === \"tools/list\") {\n      result = await listTools(client, args.metadata);\n    } else if (args.method === \"tools/call\") {\n      if (!args.toolName) {\n        throw new Error(\n          \"Tool name is required for tools/call method. Use --tool-name to specify the tool name.\",\n        );\n      }\n\n      result = await callTool(\n        client,\n        args.toolName,\n        args.toolArg || {},\n        args.metadata,\n        args.toolMeta,\n      );\n    }\n    // Resources methods\n    else if (args.method === \"resources/list\") {\n      result = await listResources(client, args.metadata);\n    } else if (args.method === \"resources/read\") {\n      if (!args.uri) {\n        throw new Error(\n          \"URI is required for resources/read method. Use --uri to specify the resource URI.\",\n        );\n      }\n\n      result = await readResource(client, args.uri, args.metadata);\n    } else if (args.method === \"resources/templates/list\") {\n      result = await listResourceTemplates(client, args.metadata);\n    }\n    // Prompts methods\n    else if (args.method === \"prompts/list\") {\n      result = await listPrompts(client, args.metadata);\n    } else if (args.method === \"prompts/get\") {\n      if (!args.promptName) {\n        throw new Error(\n          \"Prompt name is required for prompts/get method. Use --prompt-name to specify the prompt name.\",\n        );\n      }\n\n      result = await getPrompt(\n        client,\n        args.promptName,\n        args.promptArgs || {},\n        args.metadata,\n      );\n    }\n    // Logging methods\n    else if (args.method === \"logging/setLevel\") {\n      if (!args.logLevel) {\n        throw new Error(\n          \"Log level is required for logging/setLevel method. Use --log-level to specify the log level.\",\n        );\n      }\n\n      result = await setLoggingLevel(client, args.logLevel);\n    } else {\n      throw new Error(\n        `Unsupported method: ${args.method}. Supported methods include: tools/list, tools/call, resources/list, resources/read, resources/templates/list, prompts/list, prompts/get, logging/setLevel`,\n      );\n    }\n\n    await awaitableLog(JSON.stringify(result, null, 2));\n  } finally {\n    try {\n      await disconnect(transport);\n    } catch (disconnectError) {\n      throw disconnectError;\n    }\n  }\n}\n\nfunction parseKeyValuePair(\n  value: string,\n  previous: Record<string, JsonValue> = {},\n): Record<string, JsonValue> {\n  const parts = value.split(\"=\");\n  const key = parts[0];\n  const val = parts.slice(1).join(\"=\");\n\n  if (val === undefined || val === \"\") {\n    throw new Error(\n      `Invalid parameter format: ${value}. Use key=value format.`,\n    );\n  }\n\n  // Try to parse as JSON first\n  let parsedValue: JsonValue;\n  try {\n    parsedValue = JSON.parse(val) as JsonValue;\n  } catch {\n    // If JSON parsing fails, keep as string\n    parsedValue = val;\n  }\n\n  return { ...previous, [key as string]: parsedValue };\n}\n\nfunction parseHeaderPair(\n  value: string,\n  previous: Record<string, string> = {},\n): Record<string, string> {\n  const colonIndex = value.indexOf(\":\");\n\n  if (colonIndex === -1) {\n    throw new Error(\n      `Invalid header format: ${value}. Use \"HeaderName: Value\" format.`,\n    );\n  }\n\n  const key = value.slice(0, colonIndex).trim();\n  const val = value.slice(colonIndex + 1).trim();\n\n  if (key === \"\" || val === \"\") {\n    throw new Error(\n      `Invalid header format: ${value}. Use \"HeaderName: Value\" format.`,\n    );\n  }\n\n  return { ...previous, [key]: val };\n}\n\nfunction parseArgs(): Args {\n  const program = new Command();\n\n  // Find if there's a -- in the arguments and split them\n  const argSeparatorIndex = process.argv.indexOf(\"--\");\n  let preArgs = process.argv;\n  let postArgs: string[] = [];\n\n  if (argSeparatorIndex !== -1) {\n    preArgs = process.argv.slice(0, argSeparatorIndex);\n    postArgs = process.argv.slice(argSeparatorIndex + 1);\n  }\n\n  program\n    .name(\"inspector-cli\")\n    .allowUnknownOption()\n    .argument(\"<target...>\", \"Command and arguments or URL of the MCP server\")\n    //\n    // Method selection\n    //\n    .option(\"--method <method>\", \"Method to invoke\")\n    //\n    // Tool-related options\n    //\n    .option(\"--tool-name <toolName>\", \"Tool name (for tools/call method)\")\n    .option(\n      \"--tool-arg <pairs...>\",\n      \"Tool argument as key=value pair\",\n      parseKeyValuePair,\n      {},\n    )\n    //\n    // Resource-related options\n    //\n    .option(\"--uri <uri>\", \"URI of the resource (for resources/read method)\")\n    //\n    // Prompt-related options\n    //\n    .option(\n      \"--prompt-name <promptName>\",\n      \"Name of the prompt (for prompts/get method)\",\n    )\n    .option(\n      \"--prompt-args <pairs...>\",\n      \"Prompt arguments as key=value pairs\",\n      parseKeyValuePair,\n      {},\n    )\n    //\n    // Logging options\n    //\n    .option(\n      \"--log-level <level>\",\n      \"Logging level (for logging/setLevel method)\",\n      (value: string) => {\n        if (!validLogLevels.includes(value as any)) {\n          throw new Error(\n            `Invalid log level: ${value}. Valid levels are: ${validLogLevels.join(\", \")}`,\n          );\n        }\n\n        return value as LogLevel;\n      },\n    )\n    //\n    // Transport options\n    //\n    .option(\n      \"--transport <type>\",\n      \"Transport type (sse, http, or stdio). Auto-detected from URL: /mcp → http, /sse → sse, commands → stdio\",\n      (value: string) => {\n        const validTransports = [\"sse\", \"http\", \"stdio\"];\n        if (!validTransports.includes(value)) {\n          throw new Error(\n            `Invalid transport type: ${value}. Valid types are: ${validTransports.join(\", \")}`,\n          );\n        }\n        return value as \"sse\" | \"http\" | \"stdio\";\n      },\n    )\n    //\n    // HTTP headers\n    //\n    .option(\n      \"--header <headers...>\",\n      'HTTP headers as \"HeaderName: Value\" pairs (for HTTP/SSE transports)',\n      parseHeaderPair,\n      {},\n    )\n    //\n    // Metadata options\n    //\n    .option(\n      \"--metadata <pairs...>\",\n      \"General metadata as key=value pairs (applied to all methods)\",\n      parseKeyValuePair,\n      {},\n    )\n    .option(\n      \"--tool-metadata <pairs...>\",\n      \"Tool-specific metadata as key=value pairs (for tools/call method only)\",\n      parseKeyValuePair,\n      {},\n    );\n\n  // Parse only the arguments before --\n  program.parse(preArgs);\n\n  const options = program.opts() as Omit<Args, \"target\"> & {\n    header?: Record<string, string>;\n    metadata?: Record<string, JsonValue>;\n    toolMetadata?: Record<string, JsonValue>;\n  };\n\n  let remainingArgs = program.args;\n\n  // Add back any arguments that came after --\n  const finalArgs = [...remainingArgs, ...postArgs];\n\n  if (!options.method) {\n    throw new Error(\n      \"Method is required. Use --method to specify the method to invoke.\",\n    );\n  }\n\n  return {\n    target: finalArgs,\n    ...options,\n    headers: options.header, // commander.js uses 'header' field, map to 'headers'\n    metadata: options.metadata\n      ? Object.fromEntries(\n          Object.entries(options.metadata).map(([key, value]) => [\n            key,\n            String(value),\n          ]),\n        )\n      : undefined,\n    toolMeta: options.toolMetadata\n      ? Object.fromEntries(\n          Object.entries(options.toolMetadata).map(([key, value]) => [\n            key,\n            String(value),\n          ]),\n        )\n      : undefined,\n  };\n}\n\nasync function main(): Promise<void> {\n  process.on(\"uncaughtException\", (error) => {\n    handleError(error);\n  });\n\n  try {\n    const args = parseArgs();\n    await callMethod(args);\n\n    // Explicitly exit to ensure process terminates in CI\n    process.exit(0);\n  } catch (error) {\n    handleError(error);\n  }\n}\n\nmain();\n"
  },
  {
    "path": "cli/src/transport.ts",
    "content": "import { SSEClientTransport } from \"@modelcontextprotocol/sdk/client/sse.js\";\nimport {\n  getDefaultEnvironment,\n  StdioClientTransport,\n} from \"@modelcontextprotocol/sdk/client/stdio.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport type { Transport } from \"@modelcontextprotocol/sdk/shared/transport.js\";\nimport { findActualExecutable } from \"spawn-rx\";\n\nexport type TransportOptions = {\n  transportType: \"sse\" | \"stdio\" | \"http\";\n  command?: string;\n  args?: string[];\n  url?: string;\n  headers?: Record<string, string>;\n};\n\nfunction createStdioTransport(options: TransportOptions): Transport {\n  let args: string[] = [];\n\n  if (options.args !== undefined) {\n    args = options.args;\n  }\n\n  const processEnv: Record<string, string> = {};\n\n  for (const [key, value] of Object.entries(process.env)) {\n    if (value !== undefined) {\n      processEnv[key] = value;\n    }\n  }\n\n  const defaultEnv = getDefaultEnvironment();\n\n  const env: Record<string, string> = {\n    ...defaultEnv,\n    ...processEnv,\n  };\n\n  const { cmd: actualCommand, args: actualArgs } = findActualExecutable(\n    options.command ?? \"\",\n    args,\n  );\n\n  return new StdioClientTransport({\n    command: actualCommand,\n    args: actualArgs,\n    env,\n    stderr: \"pipe\",\n  });\n}\n\nexport function createTransport(options: TransportOptions): Transport {\n  const { transportType } = options;\n\n  try {\n    if (transportType === \"stdio\") {\n      return createStdioTransport(options);\n    }\n\n    // If not STDIO, then it must be either SSE or HTTP.\n    if (!options.url) {\n      throw new Error(\"URL must be provided for SSE or HTTP transport types.\");\n    }\n    const url = new URL(options.url);\n\n    if (transportType === \"sse\") {\n      const transportOptions = options.headers\n        ? {\n            requestInit: {\n              headers: options.headers,\n            },\n          }\n        : undefined;\n      return new SSEClientTransport(url, transportOptions);\n    }\n\n    if (transportType === \"http\") {\n      const transportOptions = options.headers\n        ? {\n            requestInit: {\n              headers: options.headers,\n            },\n          }\n        : undefined;\n      return new StreamableHTTPClientTransport(url, transportOptions);\n    }\n\n    throw new Error(`Unsupported transport type: ${transportType}`);\n  } catch (error) {\n    throw new Error(\n      `Failed to create transport: ${error instanceof Error ? error.message : String(error)}`,\n    );\n  }\n}\n"
  },
  {
    "path": "cli/src/utils/awaitable-log.ts",
    "content": "export function awaitableLog(logValue: string): Promise<void> {\n  return new Promise<void>((resolve) => {\n    process.stdout.write(logValue, () => {\n      resolve();\n    });\n  });\n}\n"
  },
  {
    "path": "cli/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"outDir\": \"./build\",\n    \"rootDir\": \"./src\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"resolveJsonModule\": true,\n    \"noUncheckedIndexedAccess\": true\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"packages\", \"**/*.spec.ts\", \"build\"]\n}\n"
  },
  {
    "path": "cli/vitest.config.ts",
    "content": "import { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n  test: {\n    globals: true,\n    environment: \"node\",\n    include: [\"**/__tests__/**/*.test.ts\"],\n    testTimeout: 15000, // 15 seconds - CLI tests spawn subprocesses that need time\n  },\n});\n"
  },
  {
    "path": "client/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "client/LICENSE",
    "content": "The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 (\"Apache-2.0\"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.\n\nContributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License.\n\nNo rights beyond those granted by the applicable original license are conveyed for such contributions.\n\n---\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to the Licensor for inclusion in the Work by the copyright\n      owner or by an individual or Legal Entity authorized to submit on behalf\n      of the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n---\n\nMIT License\n\nCopyright (c) 2024-2025 Model Context Protocol a Series of LF Projects, LLC.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n---\n\nCreative Commons Attribution 4.0 International (CC-BY-4.0)\n\nDocumentation in this project (excluding specifications) is licensed under\nCC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for\nthe full license text.\n"
  },
  {
    "path": "client/README.md",
    "content": "# React + TypeScript + Vite\n\nThis template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.\n\nCurrently, two official plugins are available:\n\n- [@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\n- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh\n\n## Expanding the ESLint configuration\n\nIf you are developing a production application, we recommend updating the configuration to enable type aware lint rules:\n\n- Configure the top-level `parserOptions` property like this:\n\n```js\nexport default tseslint.config({\n  languageOptions: {\n    // other options...\n    parserOptions: {\n      project: [\"./tsconfig.node.json\", \"./tsconfig.app.json\"],\n      tsconfigRootDir: import.meta.dirname,\n    },\n  },\n});\n```\n\n- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`\n- Optionally add `...tseslint.configs.stylisticTypeChecked`\n- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:\n\n```js\n// eslint.config.js\nimport react from \"eslint-plugin-react\";\n\nexport default tseslint.config({\n  // Set the react version\n  settings: { react: { version: \"18.3\" } },\n  plugins: {\n    // Add the react plugin\n    react,\n  },\n  rules: {\n    // other rules...\n    // Enable its recommended rules\n    ...react.configs.recommended.rules,\n    ...react.configs[\"jsx-runtime\"].rules,\n  },\n});\n```\n"
  },
  {
    "path": "client/bin/client.js",
    "content": "#!/usr/bin/env node\n\nimport open from \"open\";\nimport { join, dirname } from \"path\";\nimport { fileURLToPath } from \"url\";\nimport handler from \"serve-handler\";\nimport http from \"http\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst distPath = join(__dirname, \"../dist\");\n\nconst server = http.createServer((request, response) => {\n  const handlerOptions = {\n    public: distPath,\n    rewrites: [{ source: \"/**\", destination: \"/index.html\" }],\n    headers: [\n      {\n        // Ensure index.html is never cached\n        source: \"index.html\",\n        headers: [\n          {\n            key: \"Cache-Control\",\n            value: \"no-cache, no-store, max-age=0\",\n          },\n        ],\n      },\n      {\n        // Allow long-term caching for hashed assets\n        source: \"assets/**\",\n        headers: [\n          {\n            key: \"Cache-Control\",\n            value: \"public, max-age=31536000, immutable\",\n          },\n        ],\n      },\n    ],\n  };\n\n  return handler(request, response, handlerOptions);\n});\n\nconst port = parseInt(process.env.CLIENT_PORT || \"6274\", 10);\nconst host = process.env.HOST || \"localhost\";\nserver.on(\"listening\", () => {\n  const url = process.env.INSPECTOR_URL || `http://${host}:${port}`;\n  console.log(`\\n🚀 MCP Inspector is up and running at:\\n   ${url}\\n`);\n  if (process.env.MCP_AUTO_OPEN_ENABLED !== \"false\") {\n    console.log(`🌐 Opening browser...`);\n    open(url);\n  }\n});\nserver.on(\"error\", (err) => {\n  if (err.message.includes(`EADDRINUSE`)) {\n    console.error(\n      `❌  MCP Inspector PORT IS IN USE at http://${host}:${port} ❌ `,\n    );\n  } else {\n    throw err;\n  }\n});\nserver.listen(port, host);\n"
  },
  {
    "path": "client/bin/start.js",
    "content": "#!/usr/bin/env node\n\nimport open from \"open\";\nimport { resolve, dirname } from \"path\";\nimport { spawnPromise, spawn } from \"spawn-rx\";\nimport { fileURLToPath } from \"url\";\nimport { randomBytes } from \"crypto\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst DEFAULT_MCP_PROXY_LISTEN_PORT = \"6277\";\n\nfunction delay(ms) {\n  return new Promise((resolve) => setTimeout(resolve, ms, true));\n}\n\nfunction getClientUrl(port, authDisabled, sessionToken, serverPort) {\n  const host = process.env.HOST || \"localhost\";\n  const baseUrl = `http://${host}:${port}`;\n\n  const params = new URLSearchParams();\n  if (serverPort && serverPort !== DEFAULT_MCP_PROXY_LISTEN_PORT) {\n    params.set(\"MCP_PROXY_PORT\", serverPort);\n  }\n  if (!authDisabled) {\n    params.set(\"MCP_PROXY_AUTH_TOKEN\", sessionToken);\n  }\n  return params.size > 0 ? `${baseUrl}/?${params.toString()}` : baseUrl;\n}\n\nasync function startDevServer(serverOptions) {\n  const {\n    SERVER_PORT,\n    CLIENT_PORT,\n    sessionToken,\n    envVars,\n    abort,\n    transport,\n    serverUrl,\n  } = serverOptions;\n  const serverCommand = \"npx\";\n  const serverArgs = [\"tsx\", \"watch\", \"--clear-screen=false\", \"src/index.ts\"];\n  const isWindows = process.platform === \"win32\";\n\n  const spawnOptions = {\n    cwd: resolve(__dirname, \"../..\", \"server\"),\n    env: {\n      ...process.env,\n      SERVER_PORT,\n      CLIENT_PORT,\n      MCP_PROXY_AUTH_TOKEN: sessionToken,\n      MCP_ENV_VARS: JSON.stringify(envVars),\n      ...(transport ? { MCP_TRANSPORT: transport } : {}),\n      ...(serverUrl ? { MCP_SERVER_URL: serverUrl } : {}),\n    },\n    signal: abort.signal,\n    echoOutput: true,\n  };\n\n  // For Windows, we need to ignore stdin to simulate < NUL\n  // spawn-rx's 'stdin' option expects an Observable, not 'ignore'\n  // Use Node's stdio option instead\n  if (isWindows) {\n    spawnOptions.stdio = [\"ignore\", \"pipe\", \"pipe\"];\n  }\n\n  const server = spawn(serverCommand, serverArgs, spawnOptions);\n\n  // Give server time to start\n  const serverOk = await Promise.race([\n    new Promise((resolve) => {\n      server.subscribe({\n        complete: () => resolve(false),\n        error: () => resolve(false),\n        next: () => {}, // We're using echoOutput\n      });\n    }),\n    delay(3000).then(() => true),\n  ]);\n\n  return { server, serverOk };\n}\n\nasync function startProdServer(serverOptions) {\n  const {\n    SERVER_PORT,\n    CLIENT_PORT,\n    sessionToken,\n    envVars,\n    abort,\n    command,\n    mcpServerArgs,\n    transport,\n    serverUrl,\n  } = serverOptions;\n  const inspectorServerPath = resolve(\n    __dirname,\n    \"../..\",\n    \"server\",\n    \"build\",\n    \"index.js\",\n  );\n\n  const server = spawnPromise(\n    \"node\",\n    [\n      inspectorServerPath,\n      ...(command ? [`--command=${command}`] : []),\n      ...(mcpServerArgs && mcpServerArgs.length > 0\n        ? [`--args=${mcpServerArgs.join(\" \")}`]\n        : []),\n      ...(transport ? [`--transport=${transport}`] : []),\n      ...(serverUrl ? [`--server-url=${serverUrl}`] : []),\n    ],\n    {\n      env: {\n        ...process.env,\n        SERVER_PORT,\n        CLIENT_PORT,\n        MCP_PROXY_AUTH_TOKEN: sessionToken,\n        MCP_ENV_VARS: JSON.stringify(envVars),\n      },\n      signal: abort.signal,\n      echoOutput: true,\n    },\n  );\n\n  // Make sure server started before starting client\n  const serverOk = await Promise.race([server, delay(2 * 1000)]);\n\n  return { server, serverOk };\n}\n\nasync function startDevClient(clientOptions) {\n  const {\n    CLIENT_PORT,\n    SERVER_PORT,\n    authDisabled,\n    sessionToken,\n    abort,\n    cancelled,\n  } = clientOptions;\n  const clientCommand = \"npx\";\n  const host = process.env.HOST || \"localhost\";\n  const clientArgs = [\"vite\", \"--port\", CLIENT_PORT, \"--host\", host];\n  const isWindows = process.platform === \"win32\";\n\n  const spawnOptions = {\n    cwd: resolve(__dirname, \"..\"),\n    env: { ...process.env, CLIENT_PORT },\n    signal: abort.signal,\n    echoOutput: true,\n  };\n\n  // For Windows, we need to ignore stdin to prevent hanging\n  if (isWindows) {\n    spawnOptions.stdio = [\"ignore\", \"pipe\", \"pipe\"];\n  }\n\n  const client = spawn(clientCommand, clientArgs, spawnOptions);\n\n  const url = getClientUrl(\n    CLIENT_PORT,\n    authDisabled,\n    sessionToken,\n    SERVER_PORT,\n  );\n\n  // Give vite time to start before opening or logging the URL\n  setTimeout(() => {\n    console.log(`\\n🚀 MCP Inspector is up and running at:\\n   ${url}\\n`);\n    if (process.env.MCP_AUTO_OPEN_ENABLED !== \"false\") {\n      console.log(\"🌐 Opening browser...\");\n      open(url);\n    }\n  }, 3000);\n\n  await new Promise((resolve) => {\n    client.subscribe({\n      complete: resolve,\n      error: (err) => {\n        if (!cancelled || process.env.DEBUG) {\n          console.error(\"Client error:\", err);\n        }\n        resolve(null);\n      },\n      next: () => {}, // We're using echoOutput\n    });\n  });\n}\n\nasync function startProdClient(clientOptions) {\n  const {\n    CLIENT_PORT,\n    SERVER_PORT,\n    authDisabled,\n    sessionToken,\n    abort,\n    cancelled,\n  } = clientOptions;\n  const inspectorClientPath = resolve(\n    __dirname,\n    \"../..\",\n    \"client\",\n    \"bin\",\n    \"client.js\",\n  );\n\n  const url = getClientUrl(\n    CLIENT_PORT,\n    authDisabled,\n    sessionToken,\n    SERVER_PORT,\n  );\n\n  await spawnPromise(\"node\", [inspectorClientPath], {\n    env: {\n      ...process.env,\n      CLIENT_PORT,\n      INSPECTOR_URL: url,\n    },\n    signal: abort.signal,\n    echoOutput: true,\n  });\n}\n\nasync function main() {\n  // Parse command line arguments\n  const args = process.argv.slice(2);\n  const envVars = {};\n  const mcpServerArgs = [];\n  let command = null;\n  let parsingFlags = true;\n  let isDev = false;\n  let transport = null;\n  let serverUrl = null;\n\n  for (let i = 0; i < args.length; i++) {\n    const arg = args[i];\n\n    if (parsingFlags && arg === \"--\") {\n      parsingFlags = false;\n      continue;\n    }\n\n    if (parsingFlags && arg === \"--dev\") {\n      isDev = true;\n      continue;\n    }\n\n    if (parsingFlags && arg === \"--transport\" && i + 1 < args.length) {\n      transport = args[++i];\n      continue;\n    }\n\n    if (parsingFlags && arg === \"--server-url\" && i + 1 < args.length) {\n      serverUrl = args[++i];\n      continue;\n    }\n\n    if (parsingFlags && arg === \"-e\" && i + 1 < args.length) {\n      const envVar = args[++i];\n      const equalsIndex = envVar.indexOf(\"=\");\n\n      if (equalsIndex !== -1) {\n        const key = envVar.substring(0, equalsIndex);\n        const value = envVar.substring(equalsIndex + 1);\n        envVars[key] = value;\n      } else {\n        envVars[envVar] = \"\";\n      }\n    } else if (!command && !isDev) {\n      command = arg;\n    } else if (!isDev) {\n      mcpServerArgs.push(arg);\n    }\n  }\n\n  const CLIENT_PORT = process.env.CLIENT_PORT ?? \"6274\";\n  const SERVER_PORT = process.env.SERVER_PORT ?? DEFAULT_MCP_PROXY_LISTEN_PORT;\n\n  console.log(\n    isDev\n      ? \"Starting MCP inspector in development mode...\"\n      : \"Starting MCP inspector...\",\n  );\n\n  // Use provided token from environment or generate a new one\n  const sessionToken =\n    process.env.MCP_PROXY_AUTH_TOKEN || randomBytes(32).toString(\"hex\");\n  const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH;\n\n  const abort = new AbortController();\n\n  let cancelled = false;\n  process.on(\"SIGINT\", () => {\n    cancelled = true;\n    abort.abort();\n  });\n\n  let server, serverOk;\n\n  try {\n    const serverOptions = {\n      SERVER_PORT,\n      CLIENT_PORT,\n      sessionToken,\n      envVars,\n      abort,\n      command,\n      mcpServerArgs,\n      transport,\n      serverUrl,\n    };\n\n    const result = isDev\n      ? await startDevServer(serverOptions)\n      : await startProdServer(serverOptions);\n\n    server = result.server;\n    serverOk = result.serverOk;\n  } catch (error) {}\n\n  if (serverOk) {\n    try {\n      const clientOptions = {\n        CLIENT_PORT,\n        SERVER_PORT,\n        authDisabled,\n        sessionToken,\n        abort,\n        cancelled,\n      };\n\n      await (isDev\n        ? startDevClient(clientOptions)\n        : startProdClient(clientOptions));\n    } catch (e) {\n      if (!cancelled || process.env.DEBUG) throw e;\n    }\n  }\n\n  return 0;\n}\n\nmain()\n  .then((_) => process.exit(0))\n  .catch((e) => {\n    console.error(e);\n    process.exit(1);\n  });\n"
  },
  {
    "path": "client/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": false,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.js\",\n    \"css\": \"src/index.css\",\n    \"baseColor\": \"slate\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  }\n}\n"
  },
  {
    "path": "client/e2e/cli-arguments.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\n// These tests verify that CLI arguments correctly set URL parameters\n// The CLI should parse config files and pass transport/serverUrl as URL params\ntest.describe(\"CLI Arguments @cli\", () => {\n  test(\"should pass transport parameter from command line\", async ({\n    page,\n  }) => {\n    // Simulate: npx . --transport sse --server-url http://localhost:3000/sse\n    await page.goto(\n      \"http://localhost:6274/?transport=sse&serverUrl=http://localhost:3000/sse\",\n    );\n\n    // Wait for the Transport Type dropdown to be visible\n    const selectTrigger = page.getByLabel(\"Transport Type\");\n    await expect(selectTrigger).toBeVisible();\n\n    // Verify transport dropdown shows SSE\n    await expect(selectTrigger).toContainText(\"SSE\");\n\n    // Verify URL field is visible and populated\n    const urlInput = page.locator(\"#sse-url-input\");\n    await expect(urlInput).toBeVisible();\n    await expect(urlInput).toHaveValue(\"http://localhost:3000/sse\");\n  });\n\n  test(\"should pass transport parameter for streamable-http\", async ({\n    page,\n  }) => {\n    // Simulate config with streamable-http transport\n    await page.goto(\n      \"http://localhost:6274/?transport=streamable-http&serverUrl=http://localhost:3000/mcp\",\n    );\n\n    // Wait for the Transport Type dropdown to be visible\n    const selectTrigger = page.getByLabel(\"Transport Type\");\n    await expect(selectTrigger).toBeVisible();\n\n    // Verify transport dropdown shows Streamable HTTP\n    await expect(selectTrigger).toContainText(\"Streamable HTTP\");\n\n    // Verify URL field is visible and populated\n    const urlInput = page.locator(\"#sse-url-input\");\n    await expect(urlInput).toBeVisible();\n    await expect(urlInput).toHaveValue(\"http://localhost:3000/mcp\");\n  });\n\n  test(\"should not pass transport parameter for stdio config\", async ({\n    page,\n  }) => {\n    // Simulate stdio config (no transport param needed)\n    await page.goto(\"http://localhost:6274/\");\n\n    // Wait for the Transport Type dropdown to be visible\n    const selectTrigger = page.getByLabel(\"Transport Type\");\n    await expect(selectTrigger).toBeVisible();\n\n    // Verify transport dropdown defaults to STDIO\n    await expect(selectTrigger).toContainText(\"STDIO\");\n\n    // Verify command/args fields are visible\n    await expect(page.locator(\"#command-input\")).toBeVisible();\n    await expect(page.locator(\"#arguments-input\")).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "client/e2e/global-teardown.js",
    "content": "import { rimraf } from \"rimraf\";\n\nasync function globalTeardown() {\n  if (!process.env.CI) {\n    console.log(\"Cleaning up test-results directory...\");\n    // Add a small delay to ensure all Playwright files are written\n    await new Promise((resolve) => setTimeout(resolve, 100));\n    await rimraf(\"./e2e/test-results\");\n    console.log(\"Test-results directory cleaned up.\");\n  }\n}\n\nexport default globalTeardown;\n\n// Call the function when this script is run directly\nif (import.meta.url === `file://${process.argv[1]}`) {\n  globalTeardown().catch(console.error);\n}\n"
  },
  {
    "path": "client/e2e/startup-state.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\n// Adjust the URL if your dev server runs on a different port\nconst APP_URL = \"http://localhost:6274/\";\n\ntest.describe(\"Startup State\", () => {\n  test(\"should not navigate to a tab when Inspector first opens\", async ({\n    page,\n  }) => {\n    await page.goto(APP_URL);\n\n    // Check that there is no hash fragment in the URL\n    const url = page.url();\n    expect(url).not.toContain(\"#\");\n  });\n});\n"
  },
  {
    "path": "client/e2e/transport-type-dropdown.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\n// Adjust the URL if your dev server runs on a different port\nconst APP_URL = \"http://localhost:6274/\";\n\ntest.describe(\"Transport Type Dropdown\", () => {\n  test(\"should have options for STDIO, SSE, and Streamable HTTP\", async ({\n    page,\n  }) => {\n    await page.goto(APP_URL);\n\n    // Wait for the Transport Type dropdown to be visible\n    const selectTrigger = page.getByLabel(\"Transport Type\");\n    await expect(selectTrigger).toBeVisible();\n\n    // Open the dropdown\n    await selectTrigger.click();\n\n    // Check for the three options\n    await expect(page.getByRole(\"option\", { name: \"STDIO\" })).toBeVisible();\n    await expect(page.getByRole(\"option\", { name: \"SSE\" })).toBeVisible();\n    await expect(\n      page.getByRole(\"option\", { name: \"Streamable HTTP\" }),\n    ).toBeVisible();\n  });\n\n  test(\"should show Command and Arguments fields and hide URL field when Transport Type is STDIO\", async ({\n    page,\n  }) => {\n    await page.goto(APP_URL);\n\n    // Wait for the Transport Type dropdown to be visible\n    const selectTrigger = page.getByLabel(\"Transport Type\");\n    await expect(selectTrigger).toBeVisible();\n\n    // Open the dropdown and select STDIO\n    await selectTrigger.click();\n    await page.getByRole(\"option\", { name: \"STDIO\" }).click();\n\n    // Wait for the form to update\n    await page.waitForTimeout(100);\n\n    // Check that Command and Arguments fields are visible\n    await expect(page.locator(\"#command-input\")).toBeVisible();\n    await expect(page.locator(\"#arguments-input\")).toBeVisible();\n\n    // Check that URL field is not visible\n    await expect(page.locator(\"#sse-url-input\")).not.toBeVisible();\n\n    // Also verify the labels are present\n    await expect(page.getByText(\"Command\")).toBeVisible();\n    await expect(page.getByText(\"Arguments\")).toBeVisible();\n    await expect(page.getByText(\"URL\")).not.toBeVisible();\n  });\n\n  test(\"should show URL field and hide Command and Arguments fields when Transport Type is SSE\", async ({\n    page,\n  }) => {\n    await page.goto(APP_URL);\n\n    // Wait for the Transport Type dropdown to be visible\n    const selectTrigger = page.getByLabel(\"Transport Type\");\n    await expect(selectTrigger).toBeVisible();\n\n    // Open the dropdown and select SSE\n    await selectTrigger.click();\n    await page.getByRole(\"option\", { name: \"SSE\" }).click();\n\n    // Wait for the form to update\n    await page.waitForTimeout(100);\n\n    // Check that URL field is visible\n    await expect(page.locator(\"#sse-url-input\")).toBeVisible();\n\n    // Check that Command and Arguments fields are not visible\n    await expect(page.locator(\"#command-input\")).not.toBeVisible();\n    await expect(page.locator(\"#arguments-input\")).not.toBeVisible();\n\n    // Also verify the labels are present/absent\n    await expect(page.getByText(\"URL\")).toBeVisible();\n    await expect(page.getByText(\"Command\")).not.toBeVisible();\n    await expect(page.getByText(\"Arguments\")).not.toBeVisible();\n  });\n\n  test(\"should show URL field and hide Command and Arguments fields when Transport Type is Streamable HTTP\", async ({\n    page,\n  }) => {\n    await page.goto(APP_URL);\n\n    // Wait for the Transport Type dropdown to be visible\n    const selectTrigger = page.getByLabel(\"Transport Type\");\n    await expect(selectTrigger).toBeVisible();\n\n    // Open the dropdown and select Streamable HTTP\n    await selectTrigger.click();\n    await page.getByRole(\"option\", { name: \"Streamable HTTP\" }).click();\n\n    // Wait for the form to update\n    await page.waitForTimeout(100);\n\n    // Check that URL field is visible\n    await expect(page.locator(\"#sse-url-input\")).toBeVisible();\n\n    // Check that Command and Arguments fields are not visible\n    await expect(page.locator(\"#command-input\")).not.toBeVisible();\n    await expect(page.locator(\"#arguments-input\")).not.toBeVisible();\n\n    // Also verify the labels are present/absent\n    await expect(page.getByText(\"URL\")).toBeVisible();\n    await expect(page.getByText(\"Command\")).not.toBeVisible();\n    await expect(page.getByText(\"Arguments\")).not.toBeVisible();\n  });\n});\n"
  },
  {
    "path": "client/eslint.config.js",
    "content": "import js from \"@eslint/js\";\nimport globals from \"globals\";\nimport reactHooks from \"eslint-plugin-react-hooks\";\nimport reactRefresh from \"eslint-plugin-react-refresh\";\nimport tseslint from \"typescript-eslint\";\n\nexport default tseslint.config(\n  { ignores: [\"dist\"] },\n  {\n    extends: [js.configs.recommended, ...tseslint.configs.recommended],\n    files: [\"**/*.{ts,tsx}\"],\n    languageOptions: {\n      ecmaVersion: 2020,\n      globals: globals.browser,\n    },\n    plugins: {\n      \"react-hooks\": reactHooks,\n      \"react-refresh\": reactRefresh,\n    },\n    rules: {\n      ...reactHooks.configs.recommended.rules,\n      \"react-refresh/only-export-components\": [\n        \"warn\",\n        { allowConstantExport: true },\n      ],\n    },\n  },\n);\n"
  },
  {
    "path": "client/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/mcp.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>MCP Inspector</title>\n  </head>\n  <body>\n    <div id=\"root\" class=\"w-full\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "client/jest.config.cjs",
    "content": "module.exports = {\n  preset: \"ts-jest\",\n  testEnvironment: \"jest-fixed-jsdom\",\n  moduleNameMapper: {\n    \"^@/(.*)$\": \"<rootDir>/src/$1\",\n    \"\\\\.css$\": \"<rootDir>/src/__mocks__/styleMock.js\",\n  },\n  transform: {\n    \"^.+\\\\.tsx?$\": [\n      \"ts-jest\",\n      {\n        jsx: \"react-jsx\",\n        tsconfig: \"tsconfig.jest.json\",\n      },\n    ],\n    \"^.+\\\\.m?js$\": [\n      \"ts-jest\",\n      {\n        tsconfig: \"tsconfig.jest.json\",\n      },\n    ],\n  },\n  extensionsToTreatAsEsm: [\".ts\", \".tsx\"],\n  transformIgnorePatterns: [\"node_modules/(?!(@modelcontextprotocol)/)\"],\n  testRegex: \"(/__tests__/.*|(\\\\.|/)(test|spec))\\\\.(jsx?|tsx?)$\",\n  // Exclude directories and files that don't need to be tested\n  testPathIgnorePatterns: [\n    \"/node_modules/\",\n    \"/dist/\",\n    \"/bin/\",\n    \"/e2e/\",\n    \"\\\\.config\\\\.(js|ts|cjs|mjs)$\",\n  ],\n  // Exclude the same patterns from coverage reports\n  coveragePathIgnorePatterns: [\n    \"/node_modules/\",\n    \"/dist/\",\n    \"/bin/\",\n    \"/e2e/\",\n    \"\\\\.config\\\\.(js|ts|cjs|mjs)$\",\n  ],\n  randomize: true,\n};\n"
  },
  {
    "path": "client/package.json",
    "content": "{\n  \"name\": \"@modelcontextprotocol/inspector-client\",\n  \"version\": \"0.21.1\",\n  \"description\": \"Client-side application for the Model Context Protocol inspector\",\n  \"license\": \"SEE LICENSE IN LICENSE\",\n  \"author\": \"Model Context Protocol a Series of LF Projects, LLC.\",\n  \"homepage\": \"https://modelcontextprotocol.io\",\n  \"bugs\": \"https://github.com/modelcontextprotocol/inspector/issues\",\n  \"type\": \"module\",\n  \"bin\": {\n    \"mcp-inspector-client\": \"./bin/start.js\"\n  },\n  \"files\": [\n    \"bin\",\n    \"dist\",\n    \"LICENSE\"\n  ],\n  \"scripts\": {\n    \"dev\": \"vite --port 6274\",\n    \"build\": \"tsc -b && vite build\",\n    \"lint\": \"eslint .\",\n    \"preview\": \"vite preview --port 6274\",\n    \"test\": \"jest --config jest.config.cjs\",\n    \"test:watch\": \"jest --config jest.config.cjs --watch\",\n    \"test:e2e\": \"playwright test e2e && npm run cleanup:e2e\",\n    \"cleanup:e2e\": \"node e2e/global-teardown.js\"\n  },\n  \"dependencies\": {\n    \"@mcp-ui/client\": \"^6.0.0\",\n    \"@modelcontextprotocol/ext-apps\": \"^1.0.0\",\n    \"@modelcontextprotocol/sdk\": \"^1.25.2\",\n    \"@radix-ui/react-checkbox\": \"^1.1.4\",\n    \"@radix-ui/react-dialog\": \"^1.1.3\",\n    \"@radix-ui/react-icons\": \"^1.3.0\",\n    \"@radix-ui/react-label\": \"^2.1.0\",\n    \"@radix-ui/react-popover\": \"^1.1.3\",\n    \"@radix-ui/react-select\": \"^2.1.2\",\n    \"@radix-ui/react-slot\": \"^1.1.0\",\n    \"@radix-ui/react-switch\": \"^1.2.6\",\n    \"@radix-ui/react-tabs\": \"^1.1.1\",\n    \"@radix-ui/react-toast\": \"^1.2.6\",\n    \"@radix-ui/react-tooltip\": \"^1.1.8\",\n    \"ajv\": \"^6.12.6\",\n    \"class-variance-authority\": \"^0.7.0\",\n    \"clsx\": \"^2.1.1\",\n    \"cmdk\": \"^1.0.4\",\n    \"lucide-react\": \"^0.523.0\",\n    \"pkce-challenge\": \"^4.1.0\",\n    \"prismjs\": \"^1.30.0\",\n    \"react\": \"^18.3.1\",\n    \"react-dom\": \"^18.3.1\",\n    \"react-simple-code-editor\": \"^0.14.1\",\n    \"serve-handler\": \"^6.1.6\",\n    \"tailwind-merge\": \"^2.5.3\",\n    \"zod\": \"^3.25.76\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"^9.11.1\",\n    \"@testing-library/jest-dom\": \"^6.6.3\",\n    \"@testing-library/react\": \"^16.2.0\",\n    \"@types/jest\": \"^29.5.14\",\n    \"@types/node\": \"^22.17.0\",\n    \"@types/prismjs\": \"^1.26.5\",\n    \"@types/react\": \"^18.3.23\",\n    \"@types/react-dom\": \"^18.3.0\",\n    \"@types/serve-handler\": \"^6.1.4\",\n    \"@vitejs/plugin-react\": \"^5.0.4\",\n    \"autoprefixer\": \"^10.4.20\",\n    \"co\": \"^4.6.0\",\n    \"eslint\": \"^9.11.1\",\n    \"eslint-plugin-react-hooks\": \"^5.1.0-rc.0\",\n    \"eslint-plugin-react-refresh\": \"^0.4.12\",\n    \"globals\": \"^15.9.0\",\n    \"jest\": \"^29.7.0\",\n    \"jest-environment-jsdom\": \"^29.7.0\",\n    \"jest-fixed-jsdom\": \"^0.0.9\",\n    \"postcss\": \"^8.5.6\",\n    \"tailwindcss\": \"^3.4.13\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"ts-jest\": \"^29.4.0\",\n    \"typescript\": \"^5.5.3\",\n    \"typescript-eslint\": \"^8.38.0\",\n    \"vite\": \"^7.1.11\"\n  }\n}\n"
  },
  {
    "path": "client/playwright.config.ts",
    "content": "import { defineConfig, devices } from \"@playwright/test\";\n\n/**\n * @see https://playwright.dev/docs/test-configuration\n */\nexport default defineConfig({\n  /* Run your local dev server before starting the tests */\n  webServer: {\n    cwd: \"..\",\n    command: \"npm run dev\",\n    url: \"http://localhost:6274\",\n    reuseExistingServer: !process.env.CI,\n  },\n\n  testDir: \"./e2e\",\n  outputDir: \"./e2e/test-results\",\n  /* Run tests in files in parallel */\n  fullyParallel: true,\n  /* Fail the build on CI if you accidentally left test.only in the source code. */\n  forbidOnly: !!process.env.CI,\n  /* Retry on CI only */\n  retries: process.env.CI ? 2 : 0,\n  /* Opt out of parallel tests on CI. */\n  workers: process.env.CI ? 1 : undefined,\n  /* Reporter to use. See https://playwright.dev/docs/test-reporters */\n  reporter: process.env.CI\n    ? [\n        [\"html\", { outputFolder: \"playwright-report\" }],\n        [\"json\", { outputFile: \"results.json\" }],\n        [\"line\"],\n      ]\n    : [[\"line\"]],\n  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */\n  use: {\n    /* Base URL to use in actions like `await page.goto('/')`. */\n    baseURL: \"http://localhost:6274\",\n\n    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */\n    trace: \"on-first-retry\",\n\n    /* Take screenshots on failure */\n    screenshot: \"only-on-failure\",\n\n    /* Record video on failure */\n    video: \"retain-on-failure\",\n  },\n\n  /* Configure projects for major browsers */\n  projects: [\n    {\n      name: \"chromium\",\n      use: { ...devices[\"Desktop Chrome\"] },\n    },\n\n    {\n      name: \"firefox\",\n      use: { ...devices[\"Desktop Firefox\"] },\n    },\n\n    // Skip WebKit on macOS due to compatibility issues\n    ...(process.platform !== \"darwin\"\n      ? [\n          {\n            name: \"webkit\",\n            use: { ...devices[\"Desktop Safari\"] },\n          },\n        ]\n      : []),\n  ],\n});\n"
  },
  {
    "path": "client/postcss.config.js",
    "content": "export default {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "client/src/App.css",
    "content": ".logo {\n  height: 6em;\n  padding: 1.5em;\n  will-change: filter;\n  transition: filter 300ms;\n}\n.logo:hover {\n  filter: drop-shadow(0 0 2em #646cffaa);\n}\n.logo.react:hover {\n  filter: drop-shadow(0 0 2em #61dafbaa);\n}\n\n@keyframes logo-spin {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n@media (prefers-reduced-motion: no-preference) {\n  a:nth-of-type(2) .logo {\n    animation: logo-spin infinite 20s linear;\n  }\n}\n\n.card {\n  padding: 2em;\n}\n\n.read-the-docs {\n  color: #888;\n}\n"
  },
  {
    "path": "client/src/App.tsx",
    "content": "import {\n  ClientRequest,\n  CompatibilityCallToolResult,\n  CompatibilityCallToolResultSchema,\n  CreateMessageResult,\n  EmptyResultSchema,\n  GetPromptResultSchema,\n  ListPromptsResultSchema,\n  ListResourcesResultSchema,\n  ListResourceTemplatesResultSchema,\n  ListToolsResultSchema,\n  ReadResourceResultSchema,\n  Resource,\n  ResourceTemplate,\n  Root,\n  ServerNotification,\n  Tool,\n  LoggingLevel,\n  Task,\n  GetTaskResultSchema,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { OAuthTokensSchema } from \"@modelcontextprotocol/sdk/shared/auth.js\";\nimport type {\n  AnySchema,\n  SchemaOutput,\n} from \"@modelcontextprotocol/sdk/server/zod-compat.js\";\nimport { SESSION_KEYS, getServerSpecificKey } from \"./lib/constants\";\nimport {\n  hasValidMetaName,\n  hasValidMetaPrefix,\n  isReservedMetaKey,\n} from \"@/utils/metaUtils\";\nimport { getToolUiResourceUri } from \"@modelcontextprotocol/ext-apps/app-bridge\";\nimport { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from \"./lib/auth-types\";\nimport { OAuthStateMachine } from \"./lib/oauth-state-machine\";\nimport { cacheToolOutputSchemas } from \"./utils/schemaUtils\";\nimport { cleanParams } from \"./utils/paramUtils\";\nimport type { JsonSchemaType } from \"./utils/jsonUtils\";\nimport React, {\n  Suspense,\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { useConnection } from \"./lib/hooks/useConnection\";\nimport {\n  useDraggablePane,\n  useDraggableSidebar,\n} from \"./lib/hooks/useDraggablePane\";\n\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  AppWindow,\n  Bell,\n  Files,\n  FolderTree,\n  Hammer,\n  Hash,\n  Key,\n  ListTodo,\n  MessageSquare,\n  Settings,\n} from \"lucide-react\";\n\nimport { z } from \"zod\";\nimport \"./App.css\";\nimport AuthDebugger from \"./components/AuthDebugger\";\nimport ConsoleTab from \"./components/ConsoleTab\";\nimport HistoryAndNotifications from \"./components/HistoryAndNotifications\";\nimport PingTab from \"./components/PingTab\";\nimport PromptsTab, { Prompt } from \"./components/PromptsTab\";\nimport ResourcesTab from \"./components/ResourcesTab\";\nimport RootsTab from \"./components/RootsTab\";\nimport SamplingTab, { PendingRequest } from \"./components/SamplingTab\";\nimport Sidebar from \"./components/Sidebar\";\nimport ToolsTab from \"./components/ToolsTab\";\nimport TasksTab from \"./components/TasksTab\";\nimport AppsTab from \"./components/AppsTab\";\nimport { InspectorConfig } from \"./lib/configurationTypes\";\nimport {\n  getMCPProxyAddress,\n  getMCPProxyAuthToken,\n  getInitialSseUrl,\n  getInitialTransportType,\n  getInitialCommand,\n  getInitialArgs,\n  initializeInspectorConfig,\n  saveInspectorConfig,\n  getMCPTaskTtl,\n} from \"./utils/configUtils\";\nimport ElicitationTab, {\n  PendingElicitationRequest,\n  ElicitationResponse,\n} from \"./components/ElicitationTab\";\nimport {\n  CustomHeaders,\n  migrateFromLegacyAuth,\n} from \"./lib/types/customHeaders\";\nimport MetadataTab from \"./components/MetadataTab\";\n\nconst CONFIG_LOCAL_STORAGE_KEY = \"inspectorConfig_v1\";\n\ntype PrefilledAppsToolCall = {\n  id: number;\n  toolName: string;\n  params: Record<string, unknown>;\n  result: CompatibilityCallToolResult;\n};\n\nconst hasAppResourceUri = (tool: Tool): boolean => {\n  return Boolean(getToolUiResourceUri(tool));\n};\n\nconst cloneToolParams = (\n  source: Record<string, unknown>,\n): Record<string, unknown> => {\n  try {\n    return structuredClone(source);\n  } catch {\n    return { ...source };\n  }\n};\n\nconst filterReservedMetadata = (\n  metadata: Record<string, string>,\n): Record<string, string> => {\n  return Object.entries(metadata).reduce<Record<string, string>>(\n    (acc, [key, value]) => {\n      if (\n        !isReservedMetaKey(key) &&\n        hasValidMetaPrefix(key) &&\n        hasValidMetaName(key)\n      ) {\n        acc[key] = value;\n      }\n      return acc;\n    },\n    {},\n  );\n};\n\nconst App = () => {\n  const [resources, setResources] = useState<Resource[]>([]);\n  const [resourceTemplates, setResourceTemplates] = useState<\n    ResourceTemplate[]\n  >([]);\n  const [resourceContent, setResourceContent] = useState<string>(\"\");\n  const [resourceContentMap, setResourceContentMap] = useState<\n    Record<string, string>\n  >({});\n  const [fetchingResources, setFetchingResources] = useState<Set<string>>(\n    new Set(),\n  );\n  const [prompts, setPrompts] = useState<Prompt[]>([]);\n  const [promptContent, setPromptContent] = useState<string>(\"\");\n  const [tools, setTools] = useState<Tool[]>([]);\n  const [tasks, setTasks] = useState<Task[]>([]);\n  const [toolResult, setToolResult] =\n    useState<CompatibilityCallToolResult | null>(null);\n  const [prefilledAppsToolCall, setPrefilledAppsToolCall] =\n    useState<PrefilledAppsToolCall | null>(null);\n  const [errors, setErrors] = useState<Record<string, string | null>>({\n    resources: null,\n    prompts: null,\n    tools: null,\n    tasks: null,\n  });\n  const [command, setCommand] = useState<string>(getInitialCommand);\n  const [args, setArgs] = useState<string>(getInitialArgs);\n\n  const [sseUrl, setSseUrl] = useState<string>(getInitialSseUrl);\n  const [transportType, setTransportType] = useState<\n    \"stdio\" | \"sse\" | \"streamable-http\"\n  >(getInitialTransportType);\n  const [connectionType, setConnectionType] = useState<\"direct\" | \"proxy\">(\n    () => {\n      return (\n        (localStorage.getItem(\"lastConnectionType\") as \"direct\" | \"proxy\") ||\n        \"proxy\"\n      );\n    },\n  );\n  const [logLevel, setLogLevel] = useState<LoggingLevel>(\"debug\");\n  const [notifications, setNotifications] = useState<ServerNotification[]>([]);\n  const [roots, setRoots] = useState<Root[]>([]);\n  const [env, setEnv] = useState<Record<string, string>>({});\n\n  const [config, setConfig] = useState<InspectorConfig>(() =>\n    initializeInspectorConfig(CONFIG_LOCAL_STORAGE_KEY),\n  );\n  const [bearerToken, setBearerToken] = useState<string>(() => {\n    return localStorage.getItem(\"lastBearerToken\") || \"\";\n  });\n\n  const [headerName, setHeaderName] = useState<string>(() => {\n    return localStorage.getItem(\"lastHeaderName\") || \"\";\n  });\n\n  const [oauthClientId, setOauthClientId] = useState<string>(() => {\n    return localStorage.getItem(\"lastOauthClientId\") || \"\";\n  });\n\n  const [oauthScope, setOauthScope] = useState<string>(() => {\n    return localStorage.getItem(\"lastOauthScope\") || \"\";\n  });\n\n  const [oauthClientSecret, setOauthClientSecret] = useState<string>(() => {\n    return localStorage.getItem(\"lastOauthClientSecret\") || \"\";\n  });\n\n  // Custom headers state with migration from legacy auth\n  const [customHeaders, setCustomHeaders] = useState<CustomHeaders>(() => {\n    const savedHeaders = localStorage.getItem(\"lastCustomHeaders\");\n    if (savedHeaders) {\n      try {\n        return JSON.parse(savedHeaders);\n      } catch (error) {\n        console.warn(\n          `Failed to parse custom headers: \"${savedHeaders}\", will try legacy migration`,\n          error,\n        );\n        // Fall back to migration if JSON parsing fails\n      }\n    }\n\n    // Migrate from legacy auth if available\n    const legacyToken = localStorage.getItem(\"lastBearerToken\") || \"\";\n    const legacyHeaderName = localStorage.getItem(\"lastHeaderName\") || \"\";\n\n    if (legacyToken) {\n      return migrateFromLegacyAuth(legacyToken, legacyHeaderName);\n    }\n\n    // Default to empty array\n    return [\n      {\n        name: \"Authorization\",\n        value: \"Bearer \",\n        enabled: false,\n      },\n    ];\n  });\n\n  const [pendingSampleRequests, setPendingSampleRequests] = useState<\n    Array<\n      PendingRequest & {\n        resolve: (result: CreateMessageResult) => void;\n        reject: (error: Error) => void;\n      }\n    >\n  >([]);\n  const [pendingElicitationRequests, setPendingElicitationRequests] = useState<\n    Array<\n      PendingElicitationRequest & {\n        resolve: (response: ElicitationResponse) => void;\n        decline: (error: Error) => void;\n      }\n    >\n  >([]);\n  const [isAuthDebuggerVisible, setIsAuthDebuggerVisible] = useState(false);\n\n  const [authState, setAuthState] =\n    useState<AuthDebuggerState>(EMPTY_DEBUGGER_STATE);\n\n  // Metadata state - persisted in localStorage\n  const [metadata, setMetadata] = useState<Record<string, string>>(() => {\n    const savedMetadata = localStorage.getItem(\"lastMetadata\");\n    if (savedMetadata) {\n      try {\n        const parsed = JSON.parse(savedMetadata);\n        if (parsed && typeof parsed === \"object\") {\n          return filterReservedMetadata(parsed);\n        }\n      } catch (error) {\n        console.warn(\"Failed to parse saved metadata:\", error);\n      }\n    }\n    return {};\n  });\n\n  const updateAuthState = (updates: Partial<AuthDebuggerState>) => {\n    setAuthState((prev) => ({ ...prev, ...updates }));\n  };\n\n  const handleMetadataChange = (newMetadata: Record<string, string>) => {\n    const sanitizedMetadata = filterReservedMetadata(newMetadata);\n    setMetadata(sanitizedMetadata);\n    localStorage.setItem(\"lastMetadata\", JSON.stringify(sanitizedMetadata));\n  };\n  const nextRequestId = useRef(0);\n  const rootsRef = useRef<Root[]>([]);\n\n  const [selectedResource, setSelectedResource] = useState<Resource | null>(\n    null,\n  );\n  const [resourceSubscriptions, setResourceSubscriptions] = useState<\n    Set<string>\n  >(new Set<string>());\n\n  const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);\n  const [selectedTool, setSelectedTool] = useState<Tool | null>(null);\n  const [selectedTask, setSelectedTask] = useState<Task | null>(null);\n  const [isPollingTask, setIsPollingTask] = useState(false);\n  const [nextResourceCursor, setNextResourceCursor] = useState<\n    string | undefined\n  >();\n  const [nextResourceTemplateCursor, setNextResourceTemplateCursor] = useState<\n    string | undefined\n  >();\n  const [nextPromptCursor, setNextPromptCursor] = useState<\n    string | undefined\n  >();\n  const [nextToolCursor, setNextToolCursor] = useState<string | undefined>();\n  const [nextTaskCursor, setNextTaskCursor] = useState<string | undefined>();\n  const progressTokenRef = useRef(0);\n  const prefilledAppsToolCallIdRef = useRef(0);\n\n  const [activeTab, setActiveTab] = useState<string>(() => {\n    const hash = window.location.hash.slice(1);\n    const initialTab = hash || \"resources\";\n    return initialTab;\n  });\n\n  const currentTabRef = useRef<string>(activeTab);\n  const lastToolCallOriginTabRef = useRef<string>(activeTab);\n\n  useEffect(() => {\n    currentTabRef.current = activeTab;\n  }, [activeTab]);\n\n  const navigateToOriginatingTab = (originatingTab?: string) => {\n    if (!originatingTab) return;\n\n    const validTabs = [\n      ...(serverCapabilities?.resources ? [\"resources\"] : []),\n      ...(serverCapabilities?.prompts ? [\"prompts\"] : []),\n      ...(serverCapabilities?.tools ? [\"tools\"] : []),\n      ...(serverCapabilities?.tasks ? [\"tasks\"] : []),\n      \"apps\",\n      \"ping\",\n      \"sampling\",\n      \"elicitations\",\n      \"roots\",\n      \"auth\",\n      \"metadata\",\n    ];\n\n    if (!validTabs.includes(originatingTab)) return;\n\n    setActiveTab(originatingTab);\n    window.location.hash = originatingTab;\n\n    setTimeout(() => {\n      setActiveTab(originatingTab);\n      window.location.hash = originatingTab;\n    }, 100);\n  };\n\n  const { height: historyPaneHeight, handleDragStart } = useDraggablePane(300);\n  const {\n    width: sidebarWidth,\n    isDragging: isSidebarDragging,\n    handleDragStart: handleSidebarDragStart,\n  } = useDraggableSidebar(320);\n\n  const selectedTaskRef = useRef<Task | null>(null);\n  useEffect(() => {\n    selectedTaskRef.current = selectedTask;\n  }, [selectedTask]);\n\n  const {\n    connectionStatus,\n    serverCapabilities,\n    serverImplementation,\n    mcpClient,\n    requestHistory,\n    clearRequestHistory,\n    makeRequest,\n    cancelTask: cancelMcpTask,\n    listTasks: listMcpTasks,\n    sendNotification,\n    handleCompletion,\n    completionsSupported,\n    connect: connectMcpServer,\n    disconnect: disconnectMcpServer,\n  } = useConnection({\n    transportType,\n    command,\n    args,\n    sseUrl,\n    env,\n    customHeaders,\n    oauthClientId,\n    oauthClientSecret,\n    oauthScope,\n    config,\n    connectionType,\n    onNotification: (notification) => {\n      setNotifications((prev) => [...prev, notification as ServerNotification]);\n\n      if (notification.method === \"notifications/tasks/list_changed\") {\n        void listTasks();\n      }\n\n      if (notification.method === \"notifications/tasks/status\") {\n        const task = notification.params as unknown as Task;\n        setTasks((prev) => {\n          const exists = prev.some((t) => t.taskId === task.taskId);\n          if (exists) {\n            return prev.map((t) => (t.taskId === task.taskId ? task : t));\n          } else {\n            return [task, ...prev];\n          }\n        });\n        if (selectedTaskRef.current?.taskId === task.taskId) {\n          setSelectedTask(task);\n        }\n      }\n    },\n    onPendingRequest: (request, resolve, reject) => {\n      const currentTab = lastToolCallOriginTabRef.current;\n      setPendingSampleRequests((prev) => [\n        ...prev,\n        {\n          id: nextRequestId.current++,\n          request,\n          originatingTab: currentTab,\n          resolve,\n          reject,\n        },\n      ]);\n\n      setActiveTab(\"sampling\");\n      window.location.hash = \"sampling\";\n    },\n    onElicitationRequest: (request, resolve) => {\n      const currentTab = lastToolCallOriginTabRef.current;\n\n      setPendingElicitationRequests((prev) => [\n        ...prev,\n        {\n          id: nextRequestId.current++,\n          request: {\n            id: nextRequestId.current,\n            message: request.params.message,\n            requestedSchema: request.params.requestedSchema,\n          },\n          originatingTab: currentTab,\n          resolve,\n          decline: (error: Error) => {\n            console.error(\"Elicitation request rejected:\", error);\n          },\n        },\n      ]);\n\n      setActiveTab(\"elicitations\");\n      window.location.hash = \"elicitations\";\n    },\n    getRoots: () => rootsRef.current,\n    defaultLoggingLevel: logLevel,\n    metadata,\n  });\n\n  useEffect(() => {\n    if (serverCapabilities) {\n      const hash = window.location.hash.slice(1);\n\n      const validTabs = [\n        ...(serverCapabilities?.resources ? [\"resources\"] : []),\n        ...(serverCapabilities?.prompts ? [\"prompts\"] : []),\n        ...(serverCapabilities?.tools ? [\"tools\"] : []),\n        ...(serverCapabilities?.tasks ? [\"tasks\"] : []),\n        \"apps\",\n        \"ping\",\n        \"sampling\",\n        \"elicitations\",\n        \"roots\",\n        \"auth\",\n        \"metadata\",\n      ];\n\n      const isValidTab = validTabs.includes(hash);\n\n      if (!isValidTab) {\n        const defaultTab = serverCapabilities?.resources\n          ? \"resources\"\n          : serverCapabilities?.prompts\n            ? \"prompts\"\n            : serverCapabilities?.tools\n              ? \"tools\"\n              : serverCapabilities?.tasks\n                ? \"tasks\"\n                : \"ping\";\n\n        setActiveTab(defaultTab);\n        window.location.hash = defaultTab;\n      }\n    }\n  }, [serverCapabilities]);\n\n  useEffect(() => {\n    if (mcpClient && activeTab === \"tasks\") {\n      void listTasks();\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [mcpClient, activeTab]);\n\n  useEffect(() => {\n    if (mcpClient && activeTab === \"apps\" && serverCapabilities?.tools) {\n      void listTools();\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [mcpClient, activeTab, serverCapabilities?.tools]);\n\n  useEffect(() => {\n    localStorage.setItem(\"lastCommand\", command);\n  }, [command]);\n\n  useEffect(() => {\n    localStorage.setItem(\"lastArgs\", args);\n  }, [args]);\n\n  useEffect(() => {\n    localStorage.setItem(\"lastSseUrl\", sseUrl);\n  }, [sseUrl]);\n\n  useEffect(() => {\n    localStorage.setItem(\"lastTransportType\", transportType);\n  }, [transportType]);\n\n  useEffect(() => {\n    localStorage.setItem(\"lastConnectionType\", connectionType);\n  }, [connectionType]);\n\n  useEffect(() => {\n    if (bearerToken) {\n      localStorage.setItem(\"lastBearerToken\", bearerToken);\n    } else {\n      localStorage.removeItem(\"lastBearerToken\");\n    }\n  }, [bearerToken]);\n\n  useEffect(() => {\n    if (headerName) {\n      localStorage.setItem(\"lastHeaderName\", headerName);\n    } else {\n      localStorage.removeItem(\"lastHeaderName\");\n    }\n  }, [headerName]);\n\n  useEffect(() => {\n    localStorage.setItem(\"lastCustomHeaders\", JSON.stringify(customHeaders));\n  }, [customHeaders]);\n\n  // Auto-migrate from legacy auth when custom headers are empty but legacy auth exists\n  useEffect(() => {\n    if (customHeaders.length === 0 && (bearerToken || headerName)) {\n      const migratedHeaders = migrateFromLegacyAuth(bearerToken, headerName);\n      if (migratedHeaders.length > 0) {\n        setCustomHeaders(migratedHeaders);\n        // Clear legacy auth after migration\n        setBearerToken(\"\");\n        setHeaderName(\"\");\n      }\n    }\n  }, [bearerToken, headerName, customHeaders, setCustomHeaders]);\n\n  useEffect(() => {\n    localStorage.setItem(\"lastOauthClientId\", oauthClientId);\n  }, [oauthClientId]);\n\n  useEffect(() => {\n    localStorage.setItem(\"lastOauthScope\", oauthScope);\n  }, [oauthScope]);\n\n  useEffect(() => {\n    localStorage.setItem(\"lastOauthClientSecret\", oauthClientSecret);\n  }, [oauthClientSecret]);\n\n  useEffect(() => {\n    saveInspectorConfig(CONFIG_LOCAL_STORAGE_KEY, config);\n  }, [config]);\n\n  const onOAuthConnect = useCallback(\n    (serverUrl: string) => {\n      setSseUrl(serverUrl);\n      setIsAuthDebuggerVisible(false);\n      void connectMcpServer();\n    },\n    [connectMcpServer],\n  );\n\n  const onOAuthDebugConnect = useCallback(\n    async ({\n      authorizationCode,\n      errorMsg,\n      restoredState,\n    }: {\n      authorizationCode?: string;\n      errorMsg?: string;\n      restoredState?: AuthDebuggerState;\n    }) => {\n      setIsAuthDebuggerVisible(true);\n\n      if (errorMsg) {\n        updateAuthState({\n          latestError: new Error(errorMsg),\n        });\n        return;\n      }\n\n      if (restoredState && authorizationCode) {\n        let currentState: AuthDebuggerState = {\n          ...restoredState,\n          authorizationCode,\n          oauthStep: \"token_request\",\n          isInitiatingAuth: true,\n          statusMessage: null,\n          latestError: null,\n        };\n\n        try {\n          const stateMachine = new OAuthStateMachine(sseUrl, (updates) => {\n            currentState = { ...currentState, ...updates };\n          });\n\n          while (\n            currentState.oauthStep !== \"complete\" &&\n            currentState.oauthStep !== \"authorization_code\"\n          ) {\n            await stateMachine.executeStep(currentState);\n          }\n\n          if (currentState.oauthStep === \"complete\") {\n            updateAuthState({\n              ...currentState,\n              statusMessage: {\n                type: \"success\",\n                message: \"Authentication completed successfully\",\n              },\n              isInitiatingAuth: false,\n            });\n          }\n        } catch (error) {\n          console.error(\"OAuth continuation error:\", error);\n          updateAuthState({\n            latestError:\n              error instanceof Error ? error : new Error(String(error)),\n            statusMessage: {\n              type: \"error\",\n              message: `Failed to complete OAuth flow: ${error instanceof Error ? error.message : String(error)}`,\n            },\n            isInitiatingAuth: false,\n          });\n        }\n      } else if (authorizationCode) {\n        updateAuthState({\n          authorizationCode,\n          oauthStep: \"token_request\",\n        });\n      }\n    },\n    [sseUrl],\n  );\n\n  useEffect(() => {\n    const loadOAuthTokens = async () => {\n      try {\n        if (sseUrl) {\n          const key = getServerSpecificKey(SESSION_KEYS.TOKENS, sseUrl);\n          const tokens = sessionStorage.getItem(key);\n          if (tokens) {\n            const parsedTokens = await OAuthTokensSchema.parseAsync(\n              JSON.parse(tokens),\n            );\n            updateAuthState({\n              oauthTokens: parsedTokens,\n              oauthStep: \"complete\",\n            });\n          }\n        }\n      } catch (error) {\n        console.error(\"Error loading OAuth tokens:\", error);\n      }\n    };\n\n    loadOAuthTokens();\n  }, [sseUrl]);\n\n  useEffect(() => {\n    const headers: HeadersInit = {};\n    const { token: proxyAuthToken, header: proxyAuthTokenHeader } =\n      getMCPProxyAuthToken(config);\n    if (proxyAuthToken) {\n      headers[proxyAuthTokenHeader] = `Bearer ${proxyAuthToken}`;\n    }\n\n    fetch(`${getMCPProxyAddress(config)}/config`, { headers })\n      .then((response) => response.json())\n      .then((data) => {\n        setEnv(data.defaultEnvironment);\n        if (data.defaultCommand) {\n          setCommand(data.defaultCommand);\n        }\n        if (data.defaultArgs) {\n          setArgs(data.defaultArgs);\n        }\n        if (data.defaultTransport) {\n          setTransportType(\n            data.defaultTransport as \"stdio\" | \"sse\" | \"streamable-http\",\n          );\n        }\n        if (data.defaultServerUrl) {\n          setSseUrl(data.defaultServerUrl);\n        }\n      })\n      .catch((error) =>\n        console.error(\"Error fetching default environment:\", error),\n      );\n  }, [config]);\n\n  useEffect(() => {\n    rootsRef.current = roots;\n  }, [roots]);\n\n  useEffect(() => {\n    if (mcpClient && !window.location.hash) {\n      const defaultTab = serverCapabilities?.resources\n        ? \"resources\"\n        : serverCapabilities?.prompts\n          ? \"prompts\"\n          : serverCapabilities?.tools\n            ? \"tools\"\n            : serverCapabilities?.tasks\n              ? \"tasks\"\n              : \"ping\";\n      window.location.hash = defaultTab;\n    } else if (!mcpClient && window.location.hash) {\n      // Clear hash when disconnected - completely remove the fragment\n      window.history.replaceState(\n        null,\n        \"\",\n        window.location.pathname + window.location.search,\n      );\n    }\n  }, [mcpClient, serverCapabilities]);\n\n  useEffect(() => {\n    const handleHashChange = () => {\n      const hash = window.location.hash.slice(1);\n      if (hash && hash !== activeTab) {\n        setActiveTab(hash);\n      }\n    };\n\n    window.addEventListener(\"hashchange\", handleHashChange);\n    return () => window.removeEventListener(\"hashchange\", handleHashChange);\n  }, [activeTab]);\n\n  const handleApproveSampling = (id: number, result: CreateMessageResult) => {\n    setPendingSampleRequests((prev) => {\n      const request = prev.find((r) => r.id === id);\n      request?.resolve(result);\n\n      navigateToOriginatingTab(request?.originatingTab);\n\n      return prev.filter((r) => r.id !== id);\n    });\n  };\n\n  const handleRejectSampling = (id: number) => {\n    setPendingSampleRequests((prev) => {\n      const request = prev.find((r) => r.id === id);\n      request?.reject(new Error(\"Sampling request rejected\"));\n\n      navigateToOriginatingTab(request?.originatingTab);\n\n      return prev.filter((r) => r.id !== id);\n    });\n  };\n\n  const handleResolveElicitation = (\n    id: number,\n    response: ElicitationResponse,\n  ) => {\n    setPendingElicitationRequests((prev) => {\n      const request = prev.find((r) => r.id === id);\n      if (request) {\n        request.resolve(response);\n\n        if (request.originatingTab) {\n          const originatingTab = request.originatingTab;\n\n          const validTabs = [\n            ...(serverCapabilities?.resources ? [\"resources\"] : []),\n            ...(serverCapabilities?.prompts ? [\"prompts\"] : []),\n            ...(serverCapabilities?.tools ? [\"tools\"] : []),\n            ...(serverCapabilities?.tasks ? [\"tasks\"] : []),\n            \"apps\",\n            \"ping\",\n            \"sampling\",\n            \"elicitations\",\n            \"roots\",\n            \"auth\",\n            \"metadata\",\n          ];\n\n          if (validTabs.includes(originatingTab)) {\n            setActiveTab(originatingTab);\n            window.location.hash = originatingTab;\n\n            setTimeout(() => {\n              setActiveTab(originatingTab);\n              window.location.hash = originatingTab;\n            }, 100);\n          }\n        }\n      }\n      return prev.filter((r) => r.id !== id);\n    });\n  };\n\n  const clearError = (tabKey: keyof typeof errors) => {\n    setErrors((prev) => ({ ...prev, [tabKey]: null }));\n  };\n\n  const sendMCPRequest = async <T extends AnySchema>(\n    request: ClientRequest,\n    schema: T,\n    tabKey?: keyof typeof errors,\n  ): Promise<SchemaOutput<T>> => {\n    try {\n      const response = await makeRequest(request, schema);\n      if (tabKey !== undefined) {\n        clearError(tabKey);\n      }\n      return response;\n    } catch (e) {\n      const errorString = (e as Error).message ?? String(e);\n      if (tabKey !== undefined) {\n        setErrors((prev) => ({\n          ...prev,\n          [tabKey]: errorString,\n        }));\n      }\n      throw e;\n    }\n  };\n\n  const listResources = async () => {\n    const response = await sendMCPRequest(\n      {\n        method: \"resources/list\" as const,\n        params: nextResourceCursor ? { cursor: nextResourceCursor } : {},\n      },\n      ListResourcesResultSchema,\n      \"resources\",\n    );\n    setResources(resources.concat(response.resources ?? []));\n    setNextResourceCursor(response.nextCursor);\n  };\n\n  const listResourceTemplates = async () => {\n    const response = await sendMCPRequest(\n      {\n        method: \"resources/templates/list\" as const,\n        params: nextResourceTemplateCursor\n          ? { cursor: nextResourceTemplateCursor }\n          : {},\n      },\n      ListResourceTemplatesResultSchema,\n      \"resources\",\n    );\n    setResourceTemplates(\n      resourceTemplates.concat(response.resourceTemplates ?? []),\n    );\n    setNextResourceTemplateCursor(response.nextCursor);\n  };\n\n  const getPrompt = async (name: string, args: Record<string, string> = {}) => {\n    lastToolCallOriginTabRef.current = currentTabRef.current;\n\n    const response = await sendMCPRequest(\n      {\n        method: \"prompts/get\" as const,\n        params: { name, arguments: args },\n      },\n      GetPromptResultSchema,\n      \"prompts\",\n    );\n    setPromptContent(JSON.stringify(response, null, 2));\n  };\n\n  const readResource = async (uri: string) => {\n    if (fetchingResources.has(uri) || resourceContentMap[uri]) {\n      return;\n    }\n\n    console.log(\"[App] Reading resource:\", uri);\n    setFetchingResources((prev) => new Set(prev).add(uri));\n    lastToolCallOriginTabRef.current = currentTabRef.current;\n\n    try {\n      const response = await sendMCPRequest(\n        {\n          method: \"resources/read\" as const,\n          params: { uri },\n        },\n        ReadResourceResultSchema,\n        \"resources\",\n      );\n      console.log(\"[App] Resource read response:\", {\n        uri,\n        responseLength: JSON.stringify(response).length,\n        hasContents: !!(response as { contents?: unknown[] }).contents,\n      });\n      const content = JSON.stringify(response, null, 2);\n      setResourceContent(content);\n      setResourceContentMap((prev) => ({\n        ...prev,\n        [uri]: content,\n      }));\n    } catch (error) {\n      console.error(`[App] Failed to read resource ${uri}:`, error);\n      const errorString = (error as Error).message ?? String(error);\n      setResourceContentMap((prev) => ({\n        ...prev,\n        [uri]: JSON.stringify({ error: errorString }),\n      }));\n    } finally {\n      setFetchingResources((prev) => {\n        const next = new Set(prev);\n        next.delete(uri);\n        return next;\n      });\n    }\n  };\n\n  const subscribeToResource = async (uri: string) => {\n    if (!resourceSubscriptions.has(uri)) {\n      await sendMCPRequest(\n        {\n          method: \"resources/subscribe\" as const,\n          params: { uri },\n        },\n        z.object({}),\n        \"resources\",\n      );\n      const clone = new Set(resourceSubscriptions);\n      clone.add(uri);\n      setResourceSubscriptions(clone);\n    }\n  };\n\n  const unsubscribeFromResource = async (uri: string) => {\n    if (resourceSubscriptions.has(uri)) {\n      await sendMCPRequest(\n        {\n          method: \"resources/unsubscribe\" as const,\n          params: { uri },\n        },\n        z.object({}),\n        \"resources\",\n      );\n      const clone = new Set(resourceSubscriptions);\n      clone.delete(uri);\n      setResourceSubscriptions(clone);\n    }\n  };\n\n  const listPrompts = async () => {\n    const response = await sendMCPRequest(\n      {\n        method: \"prompts/list\" as const,\n        params: nextPromptCursor ? { cursor: nextPromptCursor } : {},\n      },\n      ListPromptsResultSchema,\n      \"prompts\",\n    );\n    setPrompts(response.prompts);\n    setNextPromptCursor(response.nextCursor);\n  };\n\n  const listTools = async () => {\n    const response = await sendMCPRequest(\n      {\n        method: \"tools/list\" as const,\n        params: nextToolCursor ? { cursor: nextToolCursor } : {},\n      },\n      ListToolsResultSchema,\n      \"tools\",\n    );\n    setTools(response.tools);\n    setNextToolCursor(response.nextCursor);\n    cacheToolOutputSchemas(response.tools);\n  };\n\n  const callTool = async (\n    name: string,\n    params: Record<string, unknown>,\n    toolMetadata?: Record<string, unknown>,\n    runAsTask?: boolean,\n  ): Promise<CompatibilityCallToolResult> => {\n    lastToolCallOriginTabRef.current = currentTabRef.current;\n\n    try {\n      // Find the tool schema to clean parameters properly\n      const tool = tools.find((t) => t.name === name);\n      const cleanedParams = tool?.inputSchema\n        ? cleanParams(params, tool.inputSchema as JsonSchemaType)\n        : params;\n\n      // Merge general metadata with tool-specific metadata\n      // Tool-specific metadata takes precedence over general metadata\n      const mergedMetadata = {\n        ...metadata, // General metadata\n        progressToken: progressTokenRef.current++,\n        ...toolMetadata, // Tool-specific metadata\n      };\n\n      const request: ClientRequest = {\n        method: \"tools/call\" as const,\n        params: {\n          name,\n          arguments: cleanedParams,\n          _meta: mergedMetadata,\n        },\n      };\n\n      if (runAsTask) {\n        request.params = {\n          ...request.params,\n          task: {\n            ttl: getMCPTaskTtl(config),\n          },\n        };\n      }\n\n      const response = await sendMCPRequest(\n        request,\n        CompatibilityCallToolResultSchema,\n        \"tools\",\n      );\n\n      // Check if this was a task-augmented request that returned a task reference\n      // The server returns { task: { taskId, status, ... } } when a task is created\n      const isTaskResult = (\n        res: unknown,\n      ): res is {\n        task: { taskId: string; status: string; pollInterval: number };\n      } =>\n        !!res &&\n        typeof res === \"object\" &&\n        \"task\" in res &&\n        !!res.task &&\n        typeof res.task === \"object\" &&\n        \"taskId\" in res.task;\n\n      if (runAsTask && isTaskResult(response)) {\n        const taskId = response.task.taskId;\n        const pollInterval = response.task.pollInterval;\n        // Set polling state BEFORE setting tool result for proper UI update\n        setIsPollingTask(true);\n        // Safely extract any _meta from the original response (if present)\n        const initialResponseMeta =\n          response &&\n          typeof response === \"object\" &&\n          \"_meta\" in (response as Record<string, unknown>)\n            ? ((response as { _meta?: Record<string, unknown> })._meta ?? {})\n            : undefined;\n        let latestToolResult: CompatibilityCallToolResult = {\n          content: [\n            {\n              type: \"text\",\n              text: `Task created: ${taskId}. Polling for status...`,\n            },\n          ],\n          _meta: {\n            ...(initialResponseMeta || {}),\n            \"io.modelcontextprotocol/related-task\": { taskId },\n          },\n        };\n        setToolResult(latestToolResult);\n\n        // Polling loop\n        let taskCompleted = false;\n        while (!taskCompleted) {\n          try {\n            // Wait for 1 second before polling\n            await new Promise((resolve) => setTimeout(resolve, pollInterval));\n\n            const taskStatus = await sendMCPRequest(\n              {\n                method: \"tasks/get\",\n                params: { taskId },\n              },\n              GetTaskResultSchema,\n            );\n\n            if (\n              taskStatus.status === \"completed\" ||\n              taskStatus.status === \"failed\" ||\n              taskStatus.status === \"cancelled\"\n            ) {\n              taskCompleted = true;\n              console.log(\n                `Polling complete for task ${taskId}: ${taskStatus.status}`,\n              );\n\n              if (taskStatus.status === \"completed\") {\n                console.log(`Fetching result for task ${taskId}`);\n                const result = await sendMCPRequest(\n                  {\n                    method: \"tasks/result\",\n                    params: { taskId },\n                  },\n                  CompatibilityCallToolResultSchema,\n                );\n                console.log(`Result received for task ${taskId}:`, result);\n                latestToolResult = result as CompatibilityCallToolResult;\n                setToolResult(latestToolResult);\n\n                // Refresh tasks list to show completed state\n                void listTasks();\n              } else {\n                latestToolResult = {\n                  content: [\n                    {\n                      type: \"text\",\n                      text: `Task ${taskStatus.status}: ${taskStatus.statusMessage || \"No additional information\"}`,\n                    },\n                  ],\n                  isError: true,\n                };\n                setToolResult(latestToolResult);\n                // Refresh tasks list to show failed/cancelled state\n                void listTasks();\n              }\n            } else {\n              // Update status message while polling\n              // Safely extract any _meta from the original response (if present)\n              const pollingResponseMeta =\n                response &&\n                typeof response === \"object\" &&\n                \"_meta\" in (response as Record<string, unknown>)\n                  ? ((response as { _meta?: Record<string, unknown> })._meta ??\n                    {})\n                  : undefined;\n              latestToolResult = {\n                content: [\n                  {\n                    type: \"text\",\n                    text: `Task status: ${taskStatus.status}${taskStatus.statusMessage ? ` - ${taskStatus.statusMessage}` : \"\"}. Polling...`,\n                  },\n                ],\n                _meta: {\n                  ...(pollingResponseMeta || {}),\n                  \"io.modelcontextprotocol/related-task\": { taskId },\n                },\n              };\n              setToolResult(latestToolResult);\n              // Refresh tasks list to show progress\n              void listTasks();\n            }\n          } catch (pollingError) {\n            console.error(\"Error polling task status:\", pollingError);\n            latestToolResult = {\n              content: [\n                {\n                  type: \"text\",\n                  text: `Error polling task status: ${pollingError instanceof Error ? pollingError.message : String(pollingError)}`,\n                },\n              ],\n              isError: true,\n            };\n            setToolResult(latestToolResult);\n            taskCompleted = true;\n          }\n        }\n        setIsPollingTask(false);\n        // Clear any validation errors since tool execution completed\n        setErrors((prev) => ({ ...prev, tools: null }));\n        return latestToolResult;\n      } else {\n        const directResult = response as CompatibilityCallToolResult;\n        setToolResult(directResult);\n        // Clear any validation errors since tool execution completed\n        setErrors((prev) => ({ ...prev, tools: null }));\n        return directResult;\n      }\n    } catch (e) {\n      const toolResult: CompatibilityCallToolResult = {\n        content: [\n          {\n            type: \"text\",\n            text: (e as Error).message ?? String(e),\n          },\n        ],\n        isError: true,\n      };\n      setToolResult(toolResult);\n      // Clear validation errors - tool execution errors are shown in ToolResults\n      setErrors((prev) => ({ ...prev, tools: null }));\n      return toolResult;\n    }\n  };\n\n  const listTasks = useCallback(async () => {\n    try {\n      const response = await listMcpTasks(nextTaskCursor);\n      setTasks(response.tasks);\n      setNextTaskCursor(response.nextCursor);\n      // Inline error clear to avoid extra dependency on clearError\n      setErrors((prev) => ({ ...prev, tasks: null }));\n    } catch (e) {\n      setErrors((prev) => ({\n        ...prev,\n        tasks: (e as Error).message ?? String(e),\n      }));\n    }\n  }, [listMcpTasks, nextTaskCursor]);\n\n  const cancelTask = async (taskId: string) => {\n    try {\n      const response = await cancelMcpTask(taskId);\n      setTasks((prev) => prev.map((t) => (t.taskId === taskId ? response : t)));\n      if (selectedTask?.taskId === taskId) {\n        setSelectedTask(response);\n      }\n      clearError(\"tasks\");\n    } catch (e) {\n      setErrors((prev) => ({\n        ...prev,\n        tasks: (e as Error).message ?? String(e),\n      }));\n    }\n  };\n\n  const handleRootsChange = async () => {\n    await sendNotification({ method: \"notifications/roots/list_changed\" });\n  };\n\n  const handleClearNotifications = () => {\n    setNotifications([]);\n  };\n\n  const sendLogLevelRequest = async (level: LoggingLevel) => {\n    await sendMCPRequest(\n      {\n        method: \"logging/setLevel\" as const,\n        params: { level },\n      },\n      z.object({}),\n    );\n    setLogLevel(level);\n  };\n\n  const AuthDebuggerWrapper = () => (\n    <TabsContent value=\"auth\">\n      <AuthDebugger\n        serverUrl={sseUrl}\n        onBack={() => setIsAuthDebuggerVisible(false)}\n        authState={authState}\n        updateAuthState={updateAuthState}\n      />\n    </TabsContent>\n  );\n\n  if (window.location.pathname === \"/oauth/callback\") {\n    const OAuthCallback = React.lazy(\n      () => import(\"./components/OAuthCallback\"),\n    );\n    return (\n      <Suspense fallback={<div>Loading...</div>}>\n        <OAuthCallback onConnect={onOAuthConnect} />\n      </Suspense>\n    );\n  }\n\n  if (window.location.pathname === \"/oauth/callback/debug\") {\n    const OAuthDebugCallback = React.lazy(\n      () => import(\"./components/OAuthDebugCallback\"),\n    );\n    return (\n      <Suspense fallback={<div>Loading...</div>}>\n        <OAuthDebugCallback onConnect={onOAuthDebugConnect} />\n      </Suspense>\n    );\n  }\n\n  return (\n    <div className=\"flex h-screen bg-background\">\n      <div\n        style={{\n          width: sidebarWidth,\n          minWidth: 200,\n          maxWidth: 600,\n          transition: isSidebarDragging ? \"none\" : \"width 0.15s\",\n        }}\n        className=\"bg-card border-r border-border flex flex-col h-full relative\"\n      >\n        <Sidebar\n          connectionStatus={connectionStatus}\n          transportType={transportType}\n          setTransportType={setTransportType}\n          command={command}\n          setCommand={setCommand}\n          args={args}\n          setArgs={setArgs}\n          sseUrl={sseUrl}\n          setSseUrl={setSseUrl}\n          env={env}\n          setEnv={setEnv}\n          config={config}\n          setConfig={setConfig}\n          customHeaders={customHeaders}\n          setCustomHeaders={setCustomHeaders}\n          oauthClientId={oauthClientId}\n          setOauthClientId={setOauthClientId}\n          oauthClientSecret={oauthClientSecret}\n          setOauthClientSecret={setOauthClientSecret}\n          oauthScope={oauthScope}\n          setOauthScope={setOauthScope}\n          onConnect={connectMcpServer}\n          onDisconnect={disconnectMcpServer}\n          logLevel={logLevel}\n          sendLogLevelRequest={sendLogLevelRequest}\n          loggingSupported={!!serverCapabilities?.logging || false}\n          connectionType={connectionType}\n          setConnectionType={setConnectionType}\n          serverImplementation={serverImplementation}\n        />\n        <div\n          onMouseDown={handleSidebarDragStart}\n          style={{\n            cursor: \"col-resize\",\n            position: \"absolute\",\n            top: 0,\n            right: 0,\n            width: 6,\n            height: \"100%\",\n            zIndex: 10,\n            background: isSidebarDragging ? \"rgba(0,0,0,0.08)\" : \"transparent\",\n          }}\n          aria-label=\"Resize sidebar\"\n          data-testid=\"sidebar-drag-handle\"\n        />\n      </div>\n      <div className=\"flex-1 flex flex-col overflow-hidden\">\n        <div className=\"flex-1 overflow-auto\">\n          {mcpClient ? (\n            <Tabs\n              value={activeTab}\n              className=\"w-full p-4\"\n              onValueChange={(value) => {\n                setActiveTab(value);\n                window.location.hash = value;\n              }}\n            >\n              <TabsList className=\"mb-4 py-0\">\n                <TabsTrigger\n                  value=\"resources\"\n                  disabled={!serverCapabilities?.resources}\n                >\n                  <Files className=\"w-4 h-4 mr-2\" />\n                  Resources\n                </TabsTrigger>\n                <TabsTrigger\n                  value=\"prompts\"\n                  disabled={!serverCapabilities?.prompts}\n                >\n                  <MessageSquare className=\"w-4 h-4 mr-2\" />\n                  Prompts\n                </TabsTrigger>\n                <TabsTrigger\n                  value=\"tools\"\n                  disabled={!serverCapabilities?.tools}\n                >\n                  <Hammer className=\"w-4 h-4 mr-2\" />\n                  Tools\n                </TabsTrigger>\n                <TabsTrigger\n                  value=\"tasks\"\n                  disabled={!serverCapabilities?.tasks}\n                >\n                  <ListTodo className=\"w-4 h-4 mr-2\" />\n                  Tasks\n                </TabsTrigger>\n                <TabsTrigger value=\"apps\">\n                  <AppWindow className=\"w-4 h-4 mr-2\" />\n                  Apps\n                </TabsTrigger>\n                <TabsTrigger value=\"ping\">\n                  <Bell className=\"w-4 h-4 mr-2\" />\n                  Ping\n                </TabsTrigger>\n                <TabsTrigger value=\"sampling\" className=\"relative\">\n                  <Hash className=\"w-4 h-4 mr-2\" />\n                  Sampling\n                  {pendingSampleRequests.length > 0 && (\n                    <span className=\"absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center\">\n                      {pendingSampleRequests.length}\n                    </span>\n                  )}\n                </TabsTrigger>\n                <TabsTrigger value=\"elicitations\" className=\"relative\">\n                  <MessageSquare className=\"w-4 h-4 mr-2\" />\n                  Elicitations\n                  {pendingElicitationRequests.length > 0 && (\n                    <span className=\"absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center\">\n                      {pendingElicitationRequests.length}\n                    </span>\n                  )}\n                </TabsTrigger>\n                <TabsTrigger value=\"roots\">\n                  <FolderTree className=\"w-4 h-4 mr-2\" />\n                  Roots\n                </TabsTrigger>\n                <TabsTrigger value=\"auth\">\n                  <Key className=\"w-4 h-4 mr-2\" />\n                  Auth\n                </TabsTrigger>\n                <TabsTrigger value=\"metadata\">\n                  <Settings className=\"w-4 h-4 mr-2\" />\n                  Metadata\n                </TabsTrigger>\n              </TabsList>\n\n              <div className=\"w-full\">\n                {!serverCapabilities?.resources &&\n                !serverCapabilities?.prompts &&\n                !serverCapabilities?.tools ? (\n                  <>\n                    <div className=\"flex items-center justify-center p-4\">\n                      <p className=\"text-lg text-gray-500 dark:text-gray-400\">\n                        The connected server does not support any MCP\n                        capabilities\n                      </p>\n                    </div>\n                    <PingTab\n                      onPingClick={() => {\n                        void sendMCPRequest(\n                          {\n                            method: \"ping\" as const,\n                          },\n                          EmptyResultSchema,\n                        );\n                      }}\n                    />\n                  </>\n                ) : (\n                  <>\n                    <ResourcesTab\n                      resources={resources}\n                      resourceTemplates={resourceTemplates}\n                      listResources={() => {\n                        clearError(\"resources\");\n                        listResources();\n                      }}\n                      clearResources={() => {\n                        setResources([]);\n                        setNextResourceCursor(undefined);\n                      }}\n                      listResourceTemplates={() => {\n                        clearError(\"resources\");\n                        listResourceTemplates();\n                      }}\n                      clearResourceTemplates={() => {\n                        setResourceTemplates([]);\n                        setNextResourceTemplateCursor(undefined);\n                      }}\n                      readResource={(uri) => {\n                        clearError(\"resources\");\n                        readResource(uri);\n                      }}\n                      selectedResource={selectedResource}\n                      setSelectedResource={(resource) => {\n                        clearError(\"resources\");\n                        setSelectedResource(resource);\n                      }}\n                      resourceSubscriptionsSupported={\n                        serverCapabilities?.resources?.subscribe || false\n                      }\n                      resourceSubscriptions={resourceSubscriptions}\n                      subscribeToResource={(uri) => {\n                        clearError(\"resources\");\n                        subscribeToResource(uri);\n                      }}\n                      unsubscribeFromResource={(uri) => {\n                        clearError(\"resources\");\n                        unsubscribeFromResource(uri);\n                      }}\n                      handleCompletion={handleCompletion}\n                      completionsSupported={completionsSupported}\n                      resourceContent={resourceContent}\n                      nextCursor={nextResourceCursor}\n                      nextTemplateCursor={nextResourceTemplateCursor}\n                      error={errors.resources}\n                    />\n                    <PromptsTab\n                      prompts={prompts}\n                      listPrompts={() => {\n                        clearError(\"prompts\");\n                        listPrompts();\n                      }}\n                      clearPrompts={() => {\n                        setPrompts([]);\n                        setNextPromptCursor(undefined);\n                      }}\n                      getPrompt={(name, args) => {\n                        clearError(\"prompts\");\n                        getPrompt(name, args);\n                      }}\n                      selectedPrompt={selectedPrompt}\n                      setSelectedPrompt={(prompt) => {\n                        clearError(\"prompts\");\n                        setSelectedPrompt(prompt);\n                        setPromptContent(\"\");\n                      }}\n                      handleCompletion={handleCompletion}\n                      completionsSupported={completionsSupported}\n                      promptContent={promptContent}\n                      nextCursor={nextPromptCursor}\n                      error={errors.prompts}\n                    />\n                    <ToolsTab\n                      serverSupportsTaskRequests={\n                        !!serverCapabilities?.tasks?.requests?.tools?.call\n                      }\n                      tools={tools}\n                      listTools={() => {\n                        clearError(\"tools\");\n                        listTools();\n                      }}\n                      clearTools={() => {\n                        setTools([]);\n                        setNextToolCursor(undefined);\n                        cacheToolOutputSchemas([]);\n                      }}\n                      callTool={async (\n                        name: string,\n                        params: Record<string, unknown>,\n                        metadata?: Record<string, unknown>,\n                        runAsTask?: boolean,\n                      ) => {\n                        clearError(\"tools\");\n                        setToolResult(null);\n                        const result = await callTool(\n                          name,\n                          params,\n                          metadata,\n                          runAsTask,\n                        );\n                        const calledTool = tools.find(\n                          (tool) => tool.name === name,\n                        );\n                        if (calledTool && hasAppResourceUri(calledTool)) {\n                          setPrefilledAppsToolCall({\n                            id: ++prefilledAppsToolCallIdRef.current,\n                            toolName: name,\n                            params: cloneToolParams(params),\n                            result,\n                          });\n                        } else {\n                          setPrefilledAppsToolCall(null);\n                        }\n                        return result;\n                      }}\n                      selectedTool={selectedTool}\n                      setSelectedTool={(tool) => {\n                        clearError(\"tools\");\n                        setSelectedTool(tool);\n                        setToolResult(null);\n                      }}\n                      toolResult={toolResult}\n                      isPollingTask={isPollingTask}\n                      nextCursor={nextToolCursor}\n                      error={errors.tools}\n                      resourceContent={resourceContentMap}\n                      onReadResource={(uri: string) => {\n                        clearError(\"resources\");\n                        readResource(uri);\n                      }}\n                    />\n                    <TasksTab\n                      tasks={tasks}\n                      listTasks={() => {\n                        clearError(\"tasks\");\n                        listTasks();\n                      }}\n                      clearTasks={() => {\n                        setTasks([]);\n                        setNextTaskCursor(undefined);\n                      }}\n                      cancelTask={cancelTask}\n                      selectedTask={selectedTask}\n                      setSelectedTask={(task) => {\n                        clearError(\"tasks\");\n                        setSelectedTask(task);\n                      }}\n                      error={errors.tasks}\n                      nextCursor={nextTaskCursor}\n                    />\n                    <AppsTab\n                      sandboxPath={`${getMCPProxyAddress(config)}/sandbox`}\n                      tools={tools}\n                      listTools={() => {\n                        clearError(\"tools\");\n                        listTools();\n                      }}\n                      callTool={async (\n                        name: string,\n                        params: Record<string, unknown>,\n                        metadata?: Record<string, unknown>,\n                        runAsTask?: boolean,\n                      ) => {\n                        clearError(\"tools\");\n                        setToolResult(null);\n                        return callTool(name, params, metadata, runAsTask);\n                      }}\n                      prefilledToolCall={prefilledAppsToolCall}\n                      onPrefilledToolCallConsumed={(callId) => {\n                        setPrefilledAppsToolCall((prev) =>\n                          prev?.id === callId ? null : prev,\n                        );\n                      }}\n                      error={errors.tools}\n                      mcpClient={mcpClient}\n                      onNotification={(notification) => {\n                        setNotifications((prev) => [...prev, notification]);\n                      }}\n                    />\n                    <ConsoleTab />\n                    <PingTab\n                      onPingClick={() => {\n                        void sendMCPRequest(\n                          {\n                            method: \"ping\" as const,\n                          },\n                          EmptyResultSchema,\n                        );\n                      }}\n                    />\n                    <SamplingTab\n                      pendingRequests={pendingSampleRequests}\n                      onApprove={handleApproveSampling}\n                      onReject={handleRejectSampling}\n                    />\n                    <ElicitationTab\n                      pendingRequests={pendingElicitationRequests}\n                      onResolve={handleResolveElicitation}\n                    />\n                    <RootsTab\n                      roots={roots}\n                      setRoots={setRoots}\n                      onRootsChange={handleRootsChange}\n                    />\n                    <AuthDebuggerWrapper />\n                    <MetadataTab\n                      metadata={metadata}\n                      onMetadataChange={handleMetadataChange}\n                    />\n                  </>\n                )}\n              </div>\n            </Tabs>\n          ) : isAuthDebuggerVisible ? (\n            <Tabs\n              defaultValue={\"auth\"}\n              className=\"w-full p-4\"\n              onValueChange={(value) => (window.location.hash = value)}\n            >\n              <AuthDebuggerWrapper />\n            </Tabs>\n          ) : (\n            <div className=\"flex flex-col items-center justify-center h-full gap-4\">\n              <p className=\"text-lg text-gray-500 dark:text-gray-400\">\n                Connect to an MCP server to start inspecting\n              </p>\n              <div className=\"flex items-center gap-2\">\n                <p className=\"text-sm text-muted-foreground\">\n                  Need to configure authentication?\n                </p>\n                <Button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  onClick={() => setIsAuthDebuggerVisible(true)}\n                >\n                  Open Auth Settings\n                </Button>\n              </div>\n            </div>\n          )}\n        </div>\n        <div\n          className=\"relative border-t border-border\"\n          style={{\n            height: `${historyPaneHeight}px`,\n          }}\n        >\n          <div\n            className=\"absolute w-full h-4 -top-2 cursor-row-resize flex items-center justify-center hover:bg-accent/50 dark:hover:bg-input/40\"\n            onMouseDown={handleDragStart}\n          >\n            <div className=\"w-8 h-1 rounded-full bg-border\" />\n          </div>\n          <div className=\"h-full overflow-auto\">\n            <HistoryAndNotifications\n              requestHistory={requestHistory}\n              serverNotifications={notifications}\n              onClearHistory={clearRequestHistory}\n              onClearNotifications={handleClearNotifications}\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default App;\n"
  },
  {
    "path": "client/src/__mocks__/styleMock.js",
    "content": "module.exports = {};\n"
  },
  {
    "path": "client/src/__tests__/App.config.test.tsx",
    "content": "import { render, waitFor } from \"@testing-library/react\";\nimport App from \"../App\";\nimport { DEFAULT_INSPECTOR_CONFIG } from \"../lib/constants\";\nimport { InspectorConfig } from \"../lib/configurationTypes\";\nimport * as configUtils from \"../utils/configUtils\";\n\n// Mock auth dependencies first\njest.mock(\"@modelcontextprotocol/sdk/client/auth.js\", () => ({\n  auth: jest.fn(),\n}));\n\njest.mock(\"../lib/oauth-state-machine\", () => ({\n  OAuthStateMachine: jest.fn(),\n}));\n\njest.mock(\"../lib/auth\", () => ({\n  InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({\n    tokens: jest.fn().mockResolvedValue(null),\n    clear: jest.fn(),\n  })),\n  DebugInspectorOAuthClientProvider: jest.fn(),\n}));\n\n// Mock the config utils\njest.mock(\"../utils/configUtils\", () => ({\n  ...jest.requireActual(\"../utils/configUtils\"),\n  getMCPProxyAddress: jest.fn(() => \"http://localhost:6277\"),\n  getMCPProxyAuthToken: jest.fn((config: InspectorConfig) => ({\n    token: config.MCP_PROXY_AUTH_TOKEN.value,\n    header: \"X-MCP-Proxy-Auth\",\n  })),\n  getInitialTransportType: jest.fn(() => \"stdio\"),\n  getInitialSseUrl: jest.fn(() => \"http://localhost:3001/sse\"),\n  getInitialCommand: jest.fn(() => \"mcp-server-everything\"),\n  getInitialArgs: jest.fn(() => \"\"),\n  initializeInspectorConfig: jest.fn(() => DEFAULT_INSPECTOR_CONFIG),\n  saveInspectorConfig: jest.fn(),\n}));\n\n// Get references to the mocked functions\nconst mockGetMCPProxyAuthToken = configUtils.getMCPProxyAuthToken as jest.Mock;\nconst mockInitializeInspectorConfig =\n  configUtils.initializeInspectorConfig as jest.Mock;\n\n// Mock other dependencies\njest.mock(\"../lib/hooks/useConnection\", () => ({\n  useConnection: () => ({\n    connectionStatus: \"disconnected\",\n    serverCapabilities: null,\n    mcpClient: null,\n    requestHistory: [],\n    clearRequestHistory: jest.fn(),\n    makeRequest: jest.fn(),\n    sendNotification: jest.fn(),\n    handleCompletion: jest.fn(),\n    completionsSupported: false,\n    connect: jest.fn(),\n    disconnect: jest.fn(),\n  }),\n}));\n\njest.mock(\"../lib/hooks/useDraggablePane\", () => ({\n  useDraggablePane: () => ({\n    height: 300,\n    handleDragStart: jest.fn(),\n  }),\n  useDraggableSidebar: () => ({\n    width: 320,\n    isDragging: false,\n    handleDragStart: jest.fn(),\n  }),\n}));\n\njest.mock(\"../components/Sidebar\", () => ({\n  __esModule: true,\n  default: () => <div>Sidebar</div>,\n}));\n\n// Mock fetch\nglobal.fetch = jest.fn();\n\ndescribe(\"App - Config Endpoint\", () => {\n  beforeEach(() => {\n    jest.clearAllMocks();\n    (global.fetch as jest.Mock).mockResolvedValue({\n      json: () =>\n        Promise.resolve({\n          defaultEnvironment: { TEST_ENV: \"test\" },\n          defaultCommand: \"test-command\",\n          defaultArgs: \"test-args\",\n        }),\n    });\n  });\n\n  afterEach(() => {\n    jest.clearAllMocks();\n\n    // Reset getMCPProxyAuthToken to default behavior\n    mockGetMCPProxyAuthToken.mockImplementation((config: InspectorConfig) => ({\n      token: config.MCP_PROXY_AUTH_TOKEN.value,\n      header: \"X-MCP-Proxy-Auth\",\n    }));\n  });\n\n  test(\"sends X-MCP-Proxy-Auth header when fetching config with proxy auth token\", async () => {\n    const mockConfig = {\n      ...DEFAULT_INSPECTOR_CONFIG,\n      MCP_PROXY_AUTH_TOKEN: {\n        ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,\n        value: \"test-proxy-token\",\n      },\n    };\n\n    // Mock initializeInspectorConfig to return our test config\n    mockInitializeInspectorConfig.mockReturnValue(mockConfig);\n\n    render(<App />);\n\n    await waitFor(() => {\n      expect(global.fetch).toHaveBeenCalledWith(\n        \"http://localhost:6277/config\",\n        {\n          headers: {\n            \"X-MCP-Proxy-Auth\": \"Bearer test-proxy-token\",\n          },\n        },\n      );\n    });\n  });\n\n  test(\"does not send auth header when proxy auth token is empty\", async () => {\n    const mockConfig = {\n      ...DEFAULT_INSPECTOR_CONFIG,\n      MCP_PROXY_AUTH_TOKEN: {\n        ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,\n        value: \"\",\n      },\n    };\n\n    // Mock initializeInspectorConfig to return our test config\n    mockInitializeInspectorConfig.mockReturnValue(mockConfig);\n\n    render(<App />);\n\n    await waitFor(() => {\n      expect(global.fetch).toHaveBeenCalledWith(\n        \"http://localhost:6277/config\",\n        {\n          headers: {},\n        },\n      );\n    });\n  });\n\n  test(\"uses custom header name if getMCPProxyAuthToken returns different header\", async () => {\n    const mockConfig = {\n      ...DEFAULT_INSPECTOR_CONFIG,\n      MCP_PROXY_AUTH_TOKEN: {\n        ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,\n        value: \"test-proxy-token\",\n      },\n    };\n\n    // Mock to return a custom header name\n    mockGetMCPProxyAuthToken.mockReturnValue({\n      token: \"test-proxy-token\",\n      header: \"X-Custom-Auth\",\n    });\n    mockInitializeInspectorConfig.mockReturnValue(mockConfig);\n\n    render(<App />);\n\n    await waitFor(() => {\n      expect(global.fetch).toHaveBeenCalledWith(\n        \"http://localhost:6277/config\",\n        {\n          headers: {\n            \"X-Custom-Auth\": \"Bearer test-proxy-token\",\n          },\n        },\n      );\n    });\n  });\n\n  test(\"config endpoint response updates app state\", async () => {\n    const mockConfig = {\n      ...DEFAULT_INSPECTOR_CONFIG,\n      MCP_PROXY_AUTH_TOKEN: {\n        ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,\n        value: \"test-proxy-token\",\n      },\n    };\n\n    mockInitializeInspectorConfig.mockReturnValue(mockConfig);\n\n    render(<App />);\n\n    await waitFor(() => {\n      expect(global.fetch).toHaveBeenCalledTimes(1);\n    });\n\n    // Verify the fetch was called with correct parameters\n    expect(global.fetch).toHaveBeenCalledWith(\n      \"http://localhost:6277/config\",\n      expect.objectContaining({\n        headers: expect.objectContaining({\n          \"X-MCP-Proxy-Auth\": \"Bearer test-proxy-token\",\n        }),\n      }),\n    );\n  });\n\n  test(\"handles config endpoint errors gracefully\", async () => {\n    const mockConfig = {\n      ...DEFAULT_INSPECTOR_CONFIG,\n      MCP_PROXY_AUTH_TOKEN: {\n        ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,\n        value: \"test-proxy-token\",\n      },\n    };\n\n    mockInitializeInspectorConfig.mockReturnValue(mockConfig);\n\n    // Mock fetch to reject\n    (global.fetch as jest.Mock).mockRejectedValue(new Error(\"Network error\"));\n\n    // Spy on console.error\n    const consoleErrorSpy = jest.spyOn(console, \"error\").mockImplementation();\n\n    render(<App />);\n\n    await waitFor(() => {\n      expect(consoleErrorSpy).toHaveBeenCalledWith(\n        \"Error fetching default environment:\",\n        expect.any(Error),\n      );\n    });\n\n    consoleErrorSpy.mockRestore();\n  });\n});\n"
  },
  {
    "path": "client/src/__tests__/App.routing.test.tsx",
    "content": "import { render, waitFor } from \"@testing-library/react\";\nimport App from \"../App\";\nimport { useConnection } from \"../lib/hooks/useConnection\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\n\n// Mock auth dependencies first\njest.mock(\"@modelcontextprotocol/sdk/client/auth.js\", () => ({\n  auth: jest.fn(),\n}));\n\njest.mock(\"../lib/oauth-state-machine\", () => ({\n  OAuthStateMachine: jest.fn(),\n}));\n\njest.mock(\"../lib/auth\", () => ({\n  InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({\n    tokens: jest.fn().mockResolvedValue(null),\n    clear: jest.fn(),\n  })),\n  DebugInspectorOAuthClientProvider: jest.fn(),\n}));\n\n// Mock the config utils\njest.mock(\"../utils/configUtils\", () => ({\n  ...jest.requireActual(\"../utils/configUtils\"),\n  getMCPProxyAddress: jest.fn(() => \"http://localhost:6277\"),\n  getMCPProxyAuthToken: jest.fn(() => ({\n    token: \"\",\n    header: \"X-MCP-Proxy-Auth\",\n  })),\n  getInitialTransportType: jest.fn(() => \"stdio\"),\n  getInitialSseUrl: jest.fn(() => \"http://localhost:3001/sse\"),\n  getInitialCommand: jest.fn(() => \"mcp-server-everything\"),\n  getInitialArgs: jest.fn(() => \"\"),\n  initializeInspectorConfig: jest.fn(() => ({})),\n  saveInspectorConfig: jest.fn(),\n}));\n\n// Default connection state is disconnected\nconst disconnectedConnectionState = {\n  connectionStatus: \"disconnected\" as const,\n  serverCapabilities: null,\n  mcpClient: null,\n  requestHistory: [],\n  clearRequestHistory: jest.fn(),\n  makeRequest: jest.fn(),\n  sendNotification: jest.fn(),\n  handleCompletion: jest.fn(),\n  completionsSupported: false,\n  connect: jest.fn(),\n  disconnect: jest.fn(),\n  serverImplementation: null,\n};\n\n// Connected state for tests that need an active connection\nconst connectedConnectionState = {\n  ...disconnectedConnectionState,\n  connectionStatus: \"connected\" as const,\n  serverCapabilities: {},\n  mcpClient: {\n    request: jest.fn(),\n    notification: jest.fn(),\n    close: jest.fn(),\n  } as unknown as Client,\n};\n\n// Mock required dependencies, but unrelated to routing.\njest.mock(\"../lib/hooks/useDraggablePane\", () => ({\n  useDraggablePane: () => ({\n    height: 300,\n    handleDragStart: jest.fn(),\n  }),\n  useDraggableSidebar: () => ({\n    width: 320,\n    isDragging: false,\n    handleDragStart: jest.fn(),\n  }),\n}));\n\njest.mock(\"../components/Sidebar\", () => ({\n  __esModule: true,\n  default: () => <div>Sidebar</div>,\n}));\n\n// Mock fetch\nglobal.fetch = jest.fn().mockResolvedValue({ json: () => Promise.resolve({}) });\n\n// Use an empty module mock, so that mock state can be reset between tests.\njest.mock(\"../lib/hooks/useConnection\", () => ({\n  useConnection: jest.fn(),\n}));\n\ndescribe(\"App - URL Fragment Routing\", () => {\n  const mockUseConnection = jest.mocked(useConnection);\n\n  beforeEach(() => {\n    jest.restoreAllMocks();\n\n    // Inspector starts disconnected.\n    mockUseConnection.mockReturnValue(disconnectedConnectionState);\n  });\n\n  test(\"does not set hash when starting disconnected\", async () => {\n    render(<App />);\n\n    await waitFor(() => {\n      expect(window.location.hash).toBe(\"\");\n    });\n  });\n\n  test(\"sets default hash based on server capabilities priority\", async () => {\n    // Tab priority follows UI order: Resources | Prompts | Tools | Ping | Sampling | Roots | Auth\n    //\n    // Server capabilities determine the first three tabs; if none are present, falls back to Ping.\n\n    const testCases = [\n      {\n        capabilities: { resources: { listChanged: true, subscribe: true } },\n        expected: \"#resources\",\n      },\n      {\n        capabilities: { prompts: { listChanged: true, subscribe: true } },\n        expected: \"#prompts\",\n      },\n      {\n        capabilities: { tools: { listChanged: true, subscribe: true } },\n        expected: \"#tools\",\n      },\n      { capabilities: {}, expected: \"#ping\" },\n    ];\n\n    const { rerender } = render(<App />);\n\n    for (const { capabilities, expected } of testCases) {\n      window.location.hash = \"\";\n      mockUseConnection.mockReturnValue({\n        ...connectedConnectionState,\n        serverCapabilities: capabilities,\n      });\n\n      rerender(<App />);\n\n      await waitFor(() => {\n        expect(window.location.hash).toBe(expected);\n      });\n    }\n  });\n\n  test(\"clears hash when disconnected\", async () => {\n    // Start with a hash set (simulating a connection)\n    window.location.hash = \"#resources\";\n\n    // App starts disconnected (default mock)\n    render(<App />);\n\n    // Should clear the hash when disconnected\n    await waitFor(() => {\n      expect(window.location.hash).toBe(\"\");\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/__tests__/App.samplingNavigation.test.tsx",
    "content": "import {\n  act,\n  fireEvent,\n  render,\n  screen,\n  waitFor,\n} from \"@testing-library/react\";\nimport App from \"../App\";\nimport { useConnection } from \"../lib/hooks/useConnection\";\nimport type { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport type {\n  CreateMessageRequest,\n  CreateMessageResult,\n} from \"@modelcontextprotocol/sdk/types.js\";\n\ntype OnPendingRequestHandler = (\n  request: CreateMessageRequest,\n  resolve: (result: CreateMessageResult) => void,\n  reject: (error: Error) => void,\n) => void;\n\ntype SamplingRequestMockProps = {\n  request: { id: number };\n  onApprove: (id: number, result: CreateMessageResult) => void;\n  onReject: (id: number) => void;\n};\n\ntype UseConnectionReturn = ReturnType<typeof useConnection>;\n\n// Mock auth dependencies first\njest.mock(\"@modelcontextprotocol/sdk/client/auth.js\", () => ({\n  auth: jest.fn(),\n}));\n\njest.mock(\"../lib/oauth-state-machine\", () => ({\n  OAuthStateMachine: jest.fn(),\n}));\n\njest.mock(\"../lib/auth\", () => ({\n  InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({\n    tokens: jest.fn().mockResolvedValue(null),\n    clear: jest.fn(),\n  })),\n  DebugInspectorOAuthClientProvider: jest.fn(),\n}));\n\njest.mock(\"../utils/configUtils\", () => ({\n  ...jest.requireActual(\"../utils/configUtils\"),\n  getMCPProxyAddress: jest.fn(() => \"http://localhost:6277\"),\n  getMCPProxyAuthToken: jest.fn(() => ({\n    token: \"\",\n    header: \"X-MCP-Proxy-Auth\",\n  })),\n  getInitialTransportType: jest.fn(() => \"stdio\"),\n  getInitialSseUrl: jest.fn(() => \"http://localhost:3001/sse\"),\n  getInitialCommand: jest.fn(() => \"mcp-server-everything\"),\n  getInitialArgs: jest.fn(() => \"\"),\n  initializeInspectorConfig: jest.fn(() => ({})),\n  saveInspectorConfig: jest.fn(),\n}));\n\njest.mock(\"../lib/hooks/useDraggablePane\", () => ({\n  useDraggablePane: () => ({\n    height: 300,\n    handleDragStart: jest.fn(),\n  }),\n  useDraggableSidebar: () => ({\n    width: 320,\n    isDragging: false,\n    handleDragStart: jest.fn(),\n  }),\n}));\n\njest.mock(\"../components/Sidebar\", () => ({\n  __esModule: true,\n  default: () => <div>Sidebar</div>,\n}));\n\njest.mock(\"../lib/hooks/useToast\", () => ({\n  useToast: () => ({ toast: jest.fn() }),\n}));\n\n// Keep the test focused on navigation; avoid DynamicJsonForm/schema complexity.\njest.mock(\"../components/SamplingRequest\", () => ({\n  __esModule: true,\n  default: ({ request, onApprove, onReject }: SamplingRequestMockProps) => (\n    <div data-testid=\"sampling-request\">\n      <div>sampling-request-{request.id}</div>\n      <button\n        type=\"button\"\n        onClick={() =>\n          onApprove(request.id, {\n            model: \"stub-model\",\n            stopReason: \"endTurn\",\n            role: \"assistant\",\n            content: { type: \"text\", text: \"\" },\n          })\n        }\n      >\n        Approve\n      </button>\n      <button type=\"button\" onClick={() => onReject(request.id)}>\n        Reject\n      </button>\n    </div>\n  ),\n}));\n\n// Mock fetch\nglobal.fetch = jest.fn().mockResolvedValue({ json: () => Promise.resolve({}) });\n\njest.mock(\"../lib/hooks/useConnection\", () => ({\n  useConnection: jest.fn(),\n}));\n\ndescribe(\"App - Sampling auto-navigation\", () => {\n  const mockUseConnection = jest.mocked(useConnection);\n\n  const baseConnectionState = {\n    connectionStatus: \"connected\" as const,\n    serverCapabilities: { tools: { listChanged: true, subscribe: true } },\n    mcpClient: {\n      request: jest.fn(),\n      notification: jest.fn(),\n      close: jest.fn(),\n    } as unknown as Client,\n    requestHistory: [],\n    clearRequestHistory: jest.fn(),\n    makeRequest: jest.fn(),\n    sendNotification: jest.fn(),\n    handleCompletion: jest.fn(),\n    completionsSupported: false,\n    connect: jest.fn(),\n    disconnect: jest.fn(),\n    serverImplementation: null,\n    cancelTask: jest.fn(),\n    listTasks: jest.fn(),\n  };\n\n  beforeEach(() => {\n    jest.restoreAllMocks();\n    window.location.hash = \"#tools\";\n  });\n\n  test(\"switches to #sampling when a sampling request arrives and switches back to #tools after approve\", async () => {\n    let capturedOnPendingRequest: OnPendingRequestHandler | undefined;\n\n    mockUseConnection.mockImplementation((options) => {\n      capturedOnPendingRequest = (\n        options as { onPendingRequest?: OnPendingRequestHandler }\n      ).onPendingRequest;\n      return baseConnectionState as unknown as UseConnectionReturn;\n    });\n\n    render(<App />);\n\n    // Ensure we start on tools.\n    await waitFor(() => {\n      expect(window.location.hash).toBe(\"#tools\");\n    });\n\n    const resolve = jest.fn();\n    const reject = jest.fn();\n\n    act(() => {\n      if (!capturedOnPendingRequest) {\n        throw new Error(\"Expected onPendingRequest to be provided\");\n      }\n\n      capturedOnPendingRequest(\n        {\n          method: \"sampling/createMessage\",\n          params: { messages: [], maxTokens: 1 },\n        },\n        resolve,\n        reject,\n      );\n    });\n\n    await waitFor(() => {\n      expect(window.location.hash).toBe(\"#sampling\");\n      expect(screen.getByTestId(\"sampling-request\")).toBeTruthy();\n    });\n\n    fireEvent.click(screen.getByText(\"Approve\"));\n\n    await waitFor(() => {\n      expect(resolve).toHaveBeenCalled();\n      expect(window.location.hash).toBe(\"#tools\");\n    });\n  });\n\n  test(\"switches back to #tools after reject\", async () => {\n    let capturedOnPendingRequest: OnPendingRequestHandler | undefined;\n\n    mockUseConnection.mockImplementation((options) => {\n      capturedOnPendingRequest = (\n        options as { onPendingRequest?: OnPendingRequestHandler }\n      ).onPendingRequest;\n      return baseConnectionState as unknown as UseConnectionReturn;\n    });\n\n    render(<App />);\n\n    await waitFor(() => {\n      expect(window.location.hash).toBe(\"#tools\");\n    });\n\n    const resolve = jest.fn();\n    const reject = jest.fn();\n\n    act(() => {\n      if (!capturedOnPendingRequest) {\n        throw new Error(\"Expected onPendingRequest to be provided\");\n      }\n\n      capturedOnPendingRequest(\n        {\n          method: \"sampling/createMessage\",\n          params: { messages: [], maxTokens: 1 },\n        },\n        resolve,\n        reject,\n      );\n    });\n\n    await waitFor(() => {\n      expect(window.location.hash).toBe(\"#sampling\");\n      expect(screen.getByTestId(\"sampling-request\")).toBeTruthy();\n    });\n\n    fireEvent.click(screen.getByRole(\"button\", { name: /Reject/i }));\n\n    await waitFor(() => {\n      expect(reject).toHaveBeenCalled();\n      expect(window.location.hash).toBe(\"#tools\");\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/__tests__/App.toolsAppsPrefill.test.tsx",
    "content": "import { fireEvent, render, screen, waitFor } from \"@testing-library/react\";\nimport \"@testing-library/jest-dom\";\nimport App from \"../App\";\nimport { useConnection } from \"../lib/hooks/useConnection\";\nimport type { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\n\ntype ToolListEntry = {\n  name: string;\n  inputSchema: {\n    type: \"object\";\n    properties: Record<string, unknown>;\n  };\n  _meta: Record<string, unknown>;\n};\n\ntype AppsTabProps = {\n  tools: ToolListEntry[];\n  prefilledToolCall?: {\n    id: number;\n    toolName: string;\n    params: Record<string, unknown>;\n    result: {\n      content: Array<{ type: string; text: string }>;\n    };\n  } | null;\n  onPrefilledToolCallConsumed?: (callId: number) => void;\n};\n\n// Mock auth dependencies first\njest.mock(\"@modelcontextprotocol/sdk/client/auth.js\", () => ({\n  auth: jest.fn(),\n}));\n\njest.mock(\"../lib/oauth-state-machine\", () => ({\n  OAuthStateMachine: jest.fn(),\n}));\n\njest.mock(\"../lib/auth\", () => ({\n  InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({\n    tokens: jest.fn().mockResolvedValue(null),\n    clear: jest.fn(),\n  })),\n  DebugInspectorOAuthClientProvider: jest.fn(),\n}));\n\njest.mock(\"../utils/configUtils\", () => ({\n  ...jest.requireActual(\"../utils/configUtils\"),\n  getMCPProxyAddress: jest.fn(() => \"http://localhost:6277\"),\n  getMCPProxyAuthToken: jest.fn(() => ({\n    token: \"\",\n    header: \"X-MCP-Proxy-Auth\",\n  })),\n  getInitialTransportType: jest.fn(() => \"stdio\"),\n  getInitialSseUrl: jest.fn(() => \"http://localhost:3001/sse\"),\n  getInitialCommand: jest.fn(() => \"mcp-server-everything\"),\n  getInitialArgs: jest.fn(() => \"\"),\n  initializeInspectorConfig: jest.fn(() => ({})),\n  saveInspectorConfig: jest.fn(),\n}));\n\njest.mock(\"../lib/hooks/useDraggablePane\", () => ({\n  useDraggablePane: () => ({\n    height: 300,\n    handleDragStart: jest.fn(),\n  }),\n  useDraggableSidebar: () => ({\n    width: 320,\n    isDragging: false,\n    handleDragStart: jest.fn(),\n  }),\n}));\n\njest.mock(\"../components/Sidebar\", () => ({\n  __esModule: true,\n  default: () => <div>Sidebar</div>,\n}));\n\njest.mock(\"../components/ResourcesTab\", () => ({\n  __esModule: true,\n  default: () => <div>ResourcesTab</div>,\n}));\n\njest.mock(\"../components/PromptsTab\", () => ({\n  __esModule: true,\n  default: () => <div>PromptsTab</div>,\n}));\n\njest.mock(\"../components/TasksTab\", () => ({\n  __esModule: true,\n  default: () => <div>TasksTab</div>,\n}));\n\njest.mock(\"../components/ConsoleTab\", () => ({\n  __esModule: true,\n  default: () => <div>ConsoleTab</div>,\n}));\n\njest.mock(\"../components/PingTab\", () => ({\n  __esModule: true,\n  default: () => <div>PingTab</div>,\n}));\n\njest.mock(\"../components/SamplingTab\", () => ({\n  __esModule: true,\n  default: () => <div>SamplingTab</div>,\n}));\n\njest.mock(\"../components/RootsTab\", () => ({\n  __esModule: true,\n  default: () => <div>RootsTab</div>,\n}));\n\njest.mock(\"../components/ElicitationTab\", () => ({\n  __esModule: true,\n  default: () => <div>ElicitationTab</div>,\n}));\n\njest.mock(\"../components/MetadataTab\", () => ({\n  __esModule: true,\n  default: () => <div>MetadataTab</div>,\n}));\n\njest.mock(\"../components/AuthDebugger\", () => ({\n  __esModule: true,\n  default: () => <div>AuthDebugger</div>,\n}));\n\njest.mock(\"../components/HistoryAndNotifications\", () => ({\n  __esModule: true,\n  default: () => <div>HistoryAndNotifications</div>,\n}));\n\njest.mock(\"../components/ToolsTab\", () => ({\n  __esModule: true,\n  default: ({\n    listTools,\n    callTool,\n  }: {\n    listTools: () => void;\n    callTool: (\n      name: string,\n      params: Record<string, unknown>,\n      metadata?: Record<string, unknown>,\n      runAsTask?: boolean,\n    ) => Promise<unknown>;\n  }) => (\n    <div data-testid=\"tools-tab\">\n      <button type=\"button\" onClick={listTools}>\n        mock list tools\n      </button>\n      <button\n        type=\"button\"\n        onClick={() => {\n          void callTool(\"weatherApp\", { city: \"Lisbon\" });\n        }}\n      >\n        mock run app tool\n      </button>\n    </div>\n  ),\n}));\n\njest.mock(\"../components/AppsTab\", () => ({\n  __esModule: true,\n  default: (props: AppsTabProps) => {\n    const prefilled =\n      props && \"prefilledToolCall\" in props ? props.prefilledToolCall : null;\n    const tools = props && \"tools\" in props ? props.tools : [];\n\n    return (\n      <div data-testid=\"apps-tab\">\n        <div data-testid=\"apps-tools\">{JSON.stringify(tools)}</div>\n        <div data-testid=\"apps-prefilled\">\n          {JSON.stringify(prefilled ?? null)}\n        </div>\n        {prefilled && (\n          <button\n            type=\"button\"\n            onClick={() => props?.onPrefilledToolCallConsumed?.(prefilled.id)}\n          >\n            consume prefilled\n          </button>\n        )}\n      </div>\n    );\n  },\n}));\n\nglobal.fetch = jest.fn().mockResolvedValue({ json: () => Promise.resolve({}) });\n\njest.mock(\"../lib/hooks/useConnection\", () => ({\n  useConnection: jest.fn(),\n}));\n\ndescribe(\"App - Tools to Apps prefilled handoff\", () => {\n  const mockUseConnection = jest.mocked(useConnection);\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n    window.location.hash = \"#tools\";\n  });\n\n  it(\"passes prefilled call data to AppsTab for tools using _meta['ui/resourceUri']\", async () => {\n    const makeRequest = jest.fn(async (request: { method: string }) => {\n      if (request.method === \"tools/list\") {\n        return {\n          tools: [\n            {\n              name: \"weatherApp\",\n              inputSchema: {\n                type: \"object\",\n                properties: {\n                  city: { type: \"string\" },\n                },\n              },\n              _meta: {\n                \"ui/resourceUri\": \"ui://weather-app\",\n              },\n            },\n          ],\n          nextCursor: undefined,\n        };\n      }\n\n      if (request.method === \"tools/call\") {\n        return {\n          content: [{ type: \"text\", text: \"weather result\" }],\n        };\n      }\n\n      throw new Error(`Unexpected method: ${request.method}`);\n    });\n\n    mockUseConnection.mockReturnValue({\n      connectionStatus: \"connected\",\n      serverCapabilities: { tools: { listChanged: true } },\n      serverImplementation: null,\n      mcpClient: {\n        request: jest.fn(),\n        notification: jest.fn(),\n        close: jest.fn(),\n      } as unknown as Client,\n      requestHistory: [],\n      clearRequestHistory: jest.fn(),\n      makeRequest,\n      cancelTask: jest.fn(),\n      listTasks: jest.fn(),\n      sendNotification: jest.fn(),\n      handleCompletion: jest.fn(),\n      completionsSupported: false,\n      connect: jest.fn(),\n      disconnect: jest.fn(),\n    } as ReturnType<typeof useConnection>);\n\n    render(<App />);\n\n    fireEvent.click(screen.getByRole(\"button\", { name: /mock list tools/i }));\n\n    await waitFor(() => {\n      const tools = JSON.parse(\n        screen.getByTestId(\"apps-tools\").textContent || \"[]\",\n      );\n      expect(tools).toHaveLength(1);\n      expect(tools[0].name).toBe(\"weatherApp\");\n    });\n\n    fireEvent.click(screen.getByRole(\"button\", { name: /mock run app tool/i }));\n\n    await waitFor(() => {\n      const prefilled = JSON.parse(\n        screen.getByTestId(\"apps-prefilled\").textContent || \"null\",\n      );\n      expect(prefilled.toolName).toBe(\"weatherApp\");\n      expect(prefilled.params).toEqual({ city: \"Lisbon\" });\n      expect(prefilled.result.content[0].text).toBe(\"weather result\");\n    });\n\n    fireEvent.click(screen.getByRole(\"button\", { name: /consume prefilled/i }));\n\n    await waitFor(() => {\n      expect(screen.getByTestId(\"apps-prefilled\")).toHaveTextContent(\"null\");\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/components/AppRenderer.tsx",
    "content": "import { useMemo, useState } from \"react\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport {\n  Tool,\n  ContentBlock,\n  CompatibilityCallToolResult,\n  CallToolResult,\n  CallToolResultSchema,\n  ServerNotification,\n  LoggingMessageNotificationParams,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport {\n  AppRenderer as McpUiAppRenderer,\n  type McpUiHostContext,\n  type RequestHandlerExtra,\n} from \"@mcp-ui/client\";\nimport {\n  type McpUiMessageRequest,\n  type McpUiMessageResult,\n} from \"@modelcontextprotocol/ext-apps/app-bridge\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport { AlertCircle } from \"lucide-react\";\nimport { useToast } from \"@/lib/hooks/useToast\";\n\ninterface AppRendererProps {\n  sandboxPath: string;\n  tool: Tool;\n  mcpClient: Client | null;\n  toolInput?: Record<string, unknown>;\n  toolResult?: CompatibilityCallToolResult | null;\n  onNotification?: (notification: ServerNotification) => void;\n}\n\nconst AppRenderer = ({\n  sandboxPath,\n  tool,\n  mcpClient,\n  toolInput,\n  toolResult,\n  onNotification,\n}: AppRendererProps) => {\n  const [error, setError] = useState<string | null>(null);\n  const { toast } = useToast();\n\n  const normalizedToolResult = useMemo<CallToolResult | undefined>(() => {\n    if (!toolResult) {\n      return undefined;\n    }\n\n    if (\"content\" in toolResult) {\n      const parsedResult = CallToolResultSchema.safeParse(toolResult);\n      return parsedResult.success ? parsedResult.data : undefined;\n    }\n\n    if (\"toolResult\" in toolResult) {\n      const parsedResult = CallToolResultSchema.safeParse(\n        toolResult.toolResult,\n      );\n      return parsedResult.success ? parsedResult.data : undefined;\n    }\n\n    return undefined;\n  }, [toolResult]);\n\n  const hostContext: McpUiHostContext = useMemo(\n    () => ({\n      theme: document.documentElement.classList.contains(\"dark\")\n        ? \"dark\"\n        : \"light\",\n    }),\n    [],\n  );\n\n  const handleOpenLink = async ({ url }: { url: string }) => {\n    let isError = true;\n    if (url.startsWith(\"https://\") || url.startsWith(\"http://\")) {\n      window.open(url, \"_blank\");\n      isError = false;\n    }\n    return { isError };\n  };\n\n  const handleMessage = async (\n    params: McpUiMessageRequest[\"params\"],\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    _extra: RequestHandlerExtra,\n  ): Promise<McpUiMessageResult> => {\n    const message = params.content\n      .filter((block): block is ContentBlock & { type: \"text\" } =>\n        Boolean(block.type === \"text\"),\n      )\n      .map((block) => block.text)\n      .join(\"\\n\");\n\n    if (message) {\n      toast({\n        description: message,\n      });\n    }\n\n    return {};\n  };\n\n  const handleLoggingMessage = (params: LoggingMessageNotificationParams) => {\n    if (onNotification) {\n      onNotification({\n        method: \"notifications/message\",\n        params,\n      } as ServerNotification);\n    }\n  };\n\n  if (!mcpClient) {\n    return (\n      <Alert>\n        <AlertCircle className=\"h-4 w-4\" />\n        <AlertDescription>Waiting for MCP client...</AlertDescription>\n      </Alert>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col h-full\">\n      {error && (\n        <Alert variant=\"destructive\" className=\"mb-4\">\n          <AlertCircle className=\"h-4 w-4\" />\n          <AlertDescription>{error}</AlertDescription>\n        </Alert>\n      )}\n\n      <div\n        className=\"flex-1 border rounded overflow-hidden\"\n        style={{ minHeight: \"400px\" }}\n      >\n        <McpUiAppRenderer\n          client={mcpClient}\n          onOpenLink={handleOpenLink}\n          onMessage={handleMessage}\n          onLoggingMessage={handleLoggingMessage}\n          toolName={tool.name}\n          hostContext={hostContext}\n          toolInput={toolInput}\n          toolResult={normalizedToolResult}\n          sandbox={{\n            url: new URL(sandboxPath, window.location.origin),\n          }}\n          onError={(err) => setError(err.message)}\n        />\n      </div>\n    </div>\n  );\n};\n\nexport default AppRenderer;\n"
  },
  {
    "path": "client/src/components/AppsTab.tsx",
    "content": "import { useEffect, useState, useCallback, useRef } from \"react\";\nimport { TabsContent } from \"@/components/ui/tabs\";\nimport { Button } from \"@/components/ui/button\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport {\n  AlertCircle,\n  X,\n  Play,\n  Loader2,\n  ChevronRight,\n  Maximize2,\n  Minimize2,\n} from \"lucide-react\";\nimport {\n  Tool,\n  ServerNotification,\n  CompatibilityCallToolResult,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { getToolUiResourceUri } from \"@modelcontextprotocol/ext-apps/app-bridge\";\nimport AppRenderer from \"./AppRenderer\";\nimport ListPane from \"./ListPane\";\nimport IconDisplay, { WithIcons } from \"./IconDisplay\";\nimport { Label } from \"@/components/ui/label\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Input } from \"@/components/ui/input\";\nimport DynamicJsonForm, { DynamicJsonFormRef } from \"./DynamicJsonForm\";\nimport { JsonSchemaType, JsonValue } from \"@/utils/jsonUtils\";\nimport {\n  generateDefaultValue,\n  isPropertyRequired,\n  normalizeUnionType,\n  resolveRef,\n} from \"@/utils/schemaUtils\";\n\ninterface AppsTabProps {\n  sandboxPath: string;\n  tools: Tool[];\n  listTools: () => void;\n  callTool: (\n    name: string,\n    params: Record<string, unknown>,\n    metadata?: Record<string, unknown>,\n    runAsTask?: boolean,\n  ) => Promise<CompatibilityCallToolResult>;\n  prefilledToolCall?: {\n    id: number;\n    toolName: string;\n    params: Record<string, unknown>;\n    result: CompatibilityCallToolResult;\n  } | null;\n  onPrefilledToolCallConsumed?: (callId: number) => void;\n  error: string | null;\n  mcpClient: Client | null;\n  onNotification?: (notification: ServerNotification) => void;\n}\n\n// Type guard to check if a tool has UI metadata\nconst hasUIMetadata = (tool: Tool): boolean => {\n  return !!getToolUiResourceUri(tool);\n};\n\nconst cloneToolParams = (\n  source: Record<string, unknown>,\n): Record<string, unknown> => {\n  try {\n    return structuredClone(source);\n  } catch {\n    return { ...source };\n  }\n};\n\nconst AppsTab = ({\n  sandboxPath,\n  tools,\n  listTools,\n  callTool,\n  prefilledToolCall,\n  onPrefilledToolCallConsumed,\n  error,\n  mcpClient,\n  onNotification,\n}: AppsTabProps) => {\n  const [appTools, setAppTools] = useState<Tool[]>([]);\n  const [selectedTool, setSelectedTool] = useState<Tool | null>(null);\n  const [params, setParams] = useState<Record<string, unknown>>({});\n  const [isAppOpen, setIsAppOpen] = useState(false);\n  const [isOpeningApp, setIsOpeningApp] = useState(false);\n  const [isMaximized, setIsMaximized] = useState(false);\n  const [hasValidationErrors, setHasValidationErrors] = useState(false);\n  const [submittedParams, setSubmittedParams] = useState<\n    Record<string, unknown> | undefined\n  >(undefined);\n  const [submittedToolResult, setSubmittedToolResult] =\n    useState<CompatibilityCallToolResult | null>(null);\n  const formRefs = useRef<Record<string, DynamicJsonFormRef | null>>({});\n  const openAppRunIdRef = useRef(0);\n  const prefillingParamsRef = useRef<Record<string, unknown> | null>(null);\n  const consumedPrefilledCallIdRef = useRef<number | null>(null);\n\n  const buildInitialParams = useCallback((tool: Tool) => {\n    const initialParams = Object.entries(tool.inputSchema.properties ?? []).map(\n      ([key, value]) => {\n        const resolvedValue = resolveRef(\n          value as JsonSchemaType,\n          tool.inputSchema as JsonSchemaType,\n        );\n        return [\n          key,\n          generateDefaultValue(\n            resolvedValue,\n            key,\n            tool.inputSchema as JsonSchemaType,\n          ),\n        ];\n      },\n    );\n    return Object.fromEntries(initialParams);\n  }, []);\n\n  // Function to check if any form has validation errors\n  const checkValidationErrors = useCallback(() => {\n    const errors = Object.values(formRefs.current).some(\n      (ref) => ref && !ref.validateJson().isValid,\n    );\n    setHasValidationErrors(errors);\n    return errors;\n  }, []);\n\n  // Filter tools that have UI metadata\n  useEffect(() => {\n    const filtered = tools.filter(hasUIMetadata);\n    console.log(\"[AppsTab] Filtered app tools:\", {\n      totalTools: tools.length,\n      appTools: filtered.length,\n      appToolNames: filtered.map((t) => t.name),\n    });\n    setAppTools(filtered);\n\n    // If current selected tool is no longer available, reset selection\n    if (selectedTool && !filtered.find((t) => t.name === selectedTool.name)) {\n      setSelectedTool(null);\n      setIsAppOpen(false);\n      setSubmittedParams(undefined);\n      setSubmittedToolResult(null);\n    }\n  }, [tools, selectedTool]);\n\n  useEffect(() => {\n    if (selectedTool) {\n      const prefillingParams = prefillingParamsRef.current;\n      if (prefillingParams) {\n        setParams(prefillingParams);\n        prefillingParamsRef.current = null;\n      } else {\n        setParams(buildInitialParams(selectedTool));\n      }\n      setHasValidationErrors(false);\n      formRefs.current = {};\n    } else {\n      setParams({});\n      setIsAppOpen(false);\n      setSubmittedParams(undefined);\n      setSubmittedToolResult(null);\n    }\n  }, [buildInitialParams, selectedTool]);\n\n  useEffect(() => {\n    if (!prefilledToolCall) {\n      return;\n    }\n\n    if (consumedPrefilledCallIdRef.current === prefilledToolCall.id) {\n      return;\n    }\n\n    const matchingTool = appTools.find(\n      (tool) => tool.name === prefilledToolCall.toolName,\n    );\n    if (!matchingTool) {\n      return;\n    }\n\n    const hydratedParams = cloneToolParams(prefilledToolCall.params);\n\n    openAppRunIdRef.current += 1;\n    setIsOpeningApp(false);\n    prefillingParamsRef.current = hydratedParams;\n    setSelectedTool(matchingTool);\n    setSubmittedParams(hydratedParams);\n    setSubmittedToolResult(prefilledToolCall.result);\n    setIsAppOpen(true);\n    setIsMaximized(false);\n    consumedPrefilledCallIdRef.current = prefilledToolCall.id;\n    onPrefilledToolCallConsumed?.(prefilledToolCall.id);\n  }, [appTools, onPrefilledToolCallConsumed, prefilledToolCall]);\n\n  const handleRefresh = useCallback(() => {\n    listTools();\n  }, [listTools]);\n\n  const executeToolAndOpenApp = useCallback(\n    async (tool: Tool, toolParams: Record<string, unknown>) => {\n      const runId = ++openAppRunIdRef.current;\n      const runParams = cloneToolParams(toolParams);\n      prefillingParamsRef.current = null;\n      setIsOpeningApp(true);\n      setSubmittedParams(runParams);\n      setSubmittedToolResult(null);\n      try {\n        const result = await callTool(tool.name, runParams);\n\n        if (runId !== openAppRunIdRef.current) {\n          return;\n        }\n\n        setSubmittedParams(runParams);\n        setSubmittedToolResult(result);\n        setIsAppOpen(true);\n      } catch {\n        if (runId !== openAppRunIdRef.current) {\n          return;\n        }\n\n        setSubmittedToolResult(null);\n        setIsAppOpen(false);\n      } finally {\n        if (runId === openAppRunIdRef.current) {\n          setIsOpeningApp(false);\n        }\n      }\n    },\n    [callTool],\n  );\n\n  const handleCloseApp = useCallback(() => {\n    openAppRunIdRef.current += 1;\n    setIsOpeningApp(false);\n    setIsAppOpen(false);\n    setSubmittedToolResult(null);\n  }, []);\n\n  const handleOpenApp = useCallback(async () => {\n    if (!selectedTool || checkValidationErrors()) {\n      return;\n    }\n\n    await executeToolAndOpenApp(selectedTool, params);\n  }, [checkValidationErrors, executeToolAndOpenApp, params, selectedTool]);\n\n  const handleSelectTool = useCallback(\n    (tool: Tool) => {\n      openAppRunIdRef.current += 1;\n      setIsOpeningApp(false);\n      prefillingParamsRef.current = null;\n      setSelectedTool(tool);\n      setSubmittedParams(undefined);\n      setSubmittedToolResult(null);\n      const hasFields =\n        tool.inputSchema.properties &&\n        Object.keys(tool.inputSchema.properties).length > 0;\n\n      if (hasFields) {\n        setIsAppOpen(false);\n        return;\n      }\n\n      const initialParams = buildInitialParams(tool);\n      void executeToolAndOpenApp(tool, initialParams);\n    },\n    [buildInitialParams, executeToolAndOpenApp],\n  );\n\n  const handleDeselectTool = useCallback(() => {\n    openAppRunIdRef.current += 1;\n    setIsOpeningApp(false);\n    prefillingParamsRef.current = null;\n    setSelectedTool(null);\n    setIsAppOpen(false);\n    setIsMaximized(false);\n    setSubmittedParams(undefined);\n    setSubmittedToolResult(null);\n  }, []);\n\n  return (\n    <TabsContent value=\"apps\" className=\"space-y-4\">\n      <div\n        className={\n          isMaximized\n            ? \"grid grid-cols-1 gap-4\"\n            : \"grid grid-cols-1 md:grid-cols-2 gap-4\"\n        }\n      >\n        {!isMaximized && (\n          <ListPane\n            items={appTools}\n            listItems={handleRefresh}\n            setSelectedItem={handleSelectTool}\n            renderItem={(tool) => {\n              return (\n                <div className=\"flex items-start w-full gap-2\">\n                  <div className=\"flex-shrink-0 mt-1\">\n                    <IconDisplay icons={(tool as WithIcons).icons} size=\"sm\" />\n                  </div>\n                  <div className=\"flex flex-col flex-1 min-w-0\">\n                    <span className=\"truncate font-semibold\">{tool.name}</span>\n                    {tool.description && (\n                      <span className=\"text-sm text-gray-500 text-left line-clamp-2\">\n                        {tool.description}\n                      </span>\n                    )}\n                  </div>\n                  <ChevronRight className=\"w-4 h-4 flex-shrink-0 text-gray-400 mt-1\" />\n                </div>\n              );\n            }}\n            title=\"MCP Apps\"\n            buttonText=\"Refresh Apps\"\n          />\n        )}\n\n        <div className=\"bg-card border border-border rounded-lg shadow\">\n          <div className=\"p-4 border-b border-gray-200 dark:border-border\">\n            <div className=\"flex items-center justify-between\">\n              <div className=\"flex items-center gap-2\">\n                {selectedTool && (\n                  <IconDisplay\n                    icons={(selectedTool as WithIcons).icons}\n                    size=\"md\"\n                  />\n                )}\n                <h3 className=\"font-semibold\">\n                  {selectedTool ? selectedTool.name : \"Select an app\"}\n                </h3>\n              </div>\n              <div className=\"flex items-center gap-2\">\n                {selectedTool && isAppOpen && (\n                  <Button\n                    onClick={() => setIsMaximized(!isMaximized)}\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    aria-label={isMaximized ? \"Minimize\" : \"Maximize\"}\n                  >\n                    {isMaximized ? (\n                      <Minimize2 className=\"w-4 h-4\" />\n                    ) : (\n                      <Maximize2 className=\"w-4 h-4\" />\n                    )}\n                  </Button>\n                )}\n                {selectedTool && (\n                  <Button\n                    onClick={handleDeselectTool}\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    aria-label=\"Close app\"\n                  >\n                    <X className=\"w-4 h-4\" />\n                  </Button>\n                )}\n              </div>\n            </div>\n          </div>\n\n          <div className=\"p-4\">\n            {error && (\n              <Alert variant=\"destructive\" className=\"mb-4\">\n                <AlertCircle className=\"h-4 w-4\" />\n                <AlertDescription>{error}</AlertDescription>\n              </Alert>\n            )}\n\n            {selectedTool ? (\n              (() => {\n                const hasFields =\n                  selectedTool.inputSchema.properties &&\n                  Object.keys(selectedTool.inputSchema.properties).length > 0;\n\n                return (\n                  <div className=\"space-y-4\">\n                    {!isAppOpen ? (\n                      <div className=\"space-y-4\">\n                        {selectedTool.description && (\n                          <p className=\"text-sm text-gray-600 dark:text-gray-400 whitespace-pre-wrap\">\n                            {selectedTool.description}\n                          </p>\n                        )}\n\n                        <div className=\"space-y-4 border rounded-lg p-4 bg-muted/30\">\n                          <h4 className=\"font-medium text-sm\">App Input</h4>\n                          {Object.entries(\n                            selectedTool.inputSchema.properties ?? [],\n                          ).map(([key, value]) => {\n                            // First resolve any $ref references\n                            const resolvedValue = resolveRef(\n                              value as JsonSchemaType,\n                              selectedTool.inputSchema as JsonSchemaType,\n                            );\n                            const prop = normalizeUnionType(resolvedValue);\n                            const inputSchema =\n                              selectedTool.inputSchema as JsonSchemaType;\n                            const required = isPropertyRequired(\n                              key,\n                              inputSchema,\n                            );\n\n                            return (\n                              <div key={key} className=\"space-y-2\">\n                                <div className=\"flex justify-between\">\n                                  <Label\n                                    htmlFor={key}\n                                    className=\"block text-sm font-medium text-gray-700 dark:text-gray-300\"\n                                  >\n                                    {key}\n                                    {required && (\n                                      <span className=\"text-red-500 ml-1\">\n                                        *\n                                      </span>\n                                    )}\n                                  </Label>\n                                  {prop.nullable ? (\n                                    <div className=\"flex items-center space-x-2\">\n                                      <Checkbox\n                                        id={`${key}-null`}\n                                        checked={params[key] === null}\n                                        onCheckedChange={(checked: boolean) =>\n                                          setParams({\n                                            ...params,\n                                            [key]: checked\n                                              ? null\n                                              : prop.type === \"array\"\n                                                ? undefined\n                                                : prop.default !== null\n                                                  ? prop.default\n                                                  : prop.type === \"boolean\"\n                                                    ? false\n                                                    : prop.type === \"string\"\n                                                      ? \"\"\n                                                      : undefined,\n                                          })\n                                        }\n                                      />\n                                      <label\n                                        htmlFor={`${key}-null`}\n                                        className=\"text-sm font-medium text-gray-700 dark:text-gray-300\"\n                                      >\n                                        null\n                                      </label>\n                                    </div>\n                                  ) : null}\n                                </div>\n\n                                <div\n                                  className={`${prop.nullable && params[key] === null ? \"pointer-events-none opacity-50\" : \"\"}`}\n                                >\n                                  {prop.type === \"boolean\" ? (\n                                    <div className=\"flex items-center space-x-2\">\n                                      <Checkbox\n                                        id={key}\n                                        checked={!!params[key]}\n                                        onCheckedChange={(checked: boolean) =>\n                                          setParams({\n                                            ...params,\n                                            [key]: checked,\n                                          })\n                                        }\n                                      />\n                                      <label\n                                        htmlFor={key}\n                                        className=\"text-sm font-medium text-gray-700 dark:text-gray-300\"\n                                      >\n                                        {prop.description ||\n                                          \"Toggle this option\"}\n                                      </label>\n                                    </div>\n                                  ) : prop.type === \"string\" && prop.enum ? (\n                                    <Select\n                                      value={\n                                        params[key] === undefined\n                                          ? \"\"\n                                          : String(params[key])\n                                      }\n                                      onValueChange={(value) => {\n                                        setParams({\n                                          ...params,\n                                          [key]:\n                                            value === \"\" ? undefined : value,\n                                        });\n                                      }}\n                                    >\n                                      <SelectTrigger id={key}>\n                                        <SelectValue\n                                          placeholder={\n                                            prop.description ||\n                                            \"Select an option\"\n                                          }\n                                        />\n                                      </SelectTrigger>\n                                      <SelectContent>\n                                        {prop.enum.map((option) => (\n                                          <SelectItem\n                                            key={option}\n                                            value={option}\n                                          >\n                                            {option}\n                                          </SelectItem>\n                                        ))}\n                                      </SelectContent>\n                                    </Select>\n                                  ) : prop.type === \"string\" ? (\n                                    <Textarea\n                                      id={key}\n                                      placeholder={prop.description}\n                                      value={\n                                        params[key] === undefined\n                                          ? \"\"\n                                          : String(params[key])\n                                      }\n                                      onChange={(e) => {\n                                        setParams({\n                                          ...params,\n                                          [key]:\n                                            e.target.value === \"\"\n                                              ? undefined\n                                              : e.target.value,\n                                        });\n                                      }}\n                                    />\n                                  ) : prop.type === \"object\" ||\n                                    prop.type === \"array\" ? (\n                                    <DynamicJsonForm\n                                      ref={(ref) =>\n                                        (formRefs.current[key] = ref)\n                                      }\n                                      schema={{\n                                        type: prop.type,\n                                        properties: prop.properties,\n                                        description: prop.description,\n                                        items: prop.items,\n                                      }}\n                                      value={\n                                        (params[key] as JsonValue) ??\n                                        generateDefaultValue(prop)\n                                      }\n                                      onChange={(newValue: JsonValue) => {\n                                        setParams({\n                                          ...params,\n                                          [key]: newValue,\n                                        });\n                                        setTimeout(checkValidationErrors, 100);\n                                      }}\n                                    />\n                                  ) : prop.type === \"number\" ||\n                                    prop.type === \"integer\" ? (\n                                    <Input\n                                      type=\"number\"\n                                      id={key}\n                                      placeholder={prop.description}\n                                      value={\n                                        params[key] === undefined\n                                          ? \"\"\n                                          : String(params[key])\n                                      }\n                                      onChange={(e) => {\n                                        const value = e.target.value;\n                                        if (value === \"\") {\n                                          setParams({\n                                            ...params,\n                                            [key]: undefined,\n                                          });\n                                        } else {\n                                          const num = Number(value);\n                                          setParams({\n                                            ...params,\n                                            [key]: isNaN(num) ? value : num,\n                                          });\n                                        }\n                                      }}\n                                    />\n                                  ) : (\n                                    <DynamicJsonForm\n                                      ref={(ref) =>\n                                        (formRefs.current[key] = ref)\n                                      }\n                                      schema={{\n                                        type: prop.type,\n                                        properties: prop.properties,\n                                        description: prop.description,\n                                        items: prop.items,\n                                      }}\n                                      value={params[key] as JsonValue}\n                                      onChange={(newValue: JsonValue) => {\n                                        setParams({\n                                          ...params,\n                                          [key]: newValue,\n                                        });\n                                        setTimeout(checkValidationErrors, 100);\n                                      }}\n                                    />\n                                  )}\n                                </div>\n                              </div>\n                            );\n                          })}\n\n                          <Button\n                            onClick={() => void handleOpenApp()}\n                            className=\"w-full\"\n                            disabled={hasValidationErrors || isOpeningApp}\n                          >\n                            {isOpeningApp ? (\n                              <Loader2 className=\"w-4 h-4 mr-2 animate-spin\" />\n                            ) : (\n                              <Play className=\"w-4 h-4 mr-2\" />\n                            )}\n                            {isOpeningApp ? \"Opening App...\" : \"Open App\"}\n                          </Button>\n                        </div>\n                      </div>\n                    ) : (\n                      <div className=\"space-y-4\">\n                        {hasFields && (\n                          <div className=\"flex justify-end\">\n                            <Button\n                              onClick={handleCloseApp}\n                              variant=\"outline\"\n                              size=\"sm\"\n                            >\n                              Back to Input\n                            </Button>\n                          </div>\n                        )}\n                        <div className=\"h-[600px]\">\n                          <AppRenderer\n                            sandboxPath={sandboxPath}\n                            tool={selectedTool}\n                            mcpClient={mcpClient}\n                            toolInput={submittedParams}\n                            toolResult={submittedToolResult}\n                            onNotification={onNotification}\n                          />\n                        </div>\n                      </div>\n                    )}\n                  </div>\n                );\n              })()\n            ) : (\n              <div className=\"flex flex-col items-center justify-center py-12 text-muted-foreground space-y-4\">\n                <AlertCircle className=\"w-12 h-12 opacity-20\" />\n                <p>Select an app from the list to get started</p>\n                {appTools.length === 0 && (\n                  <p className=\"text-xs text-center max-w-[200px]\">\n                    No MCP Apps available. Apps are tools that include a{\" \"}\n                    <code className=\"bg-muted px-1 rounded\">\n                      _meta.ui.resourceUri\n                    </code>\n                  </p>\n                )}\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n    </TabsContent>\n  );\n};\n\nexport default AppsTab;\n"
  },
  {
    "path": "client/src/components/AuthDebugger.tsx",
    "content": "import { useCallback, useMemo, useEffect } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { DebugInspectorOAuthClientProvider } from \"../lib/auth\";\nimport { AlertCircle } from \"lucide-react\";\nimport { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from \"../lib/auth-types\";\nimport { OAuthFlowProgress } from \"./OAuthFlowProgress\";\nimport { OAuthStateMachine } from \"../lib/oauth-state-machine\";\nimport { SESSION_KEYS } from \"../lib/constants\";\nimport { validateRedirectUrl } from \"@/utils/urlValidation\";\n\nexport interface AuthDebuggerProps {\n  serverUrl: string;\n  onBack: () => void;\n  authState: AuthDebuggerState;\n  updateAuthState: (updates: Partial<AuthDebuggerState>) => void;\n}\n\ninterface StatusMessageProps {\n  message: { type: \"error\" | \"success\" | \"info\"; message: string };\n}\n\nconst StatusMessage = ({ message }: StatusMessageProps) => {\n  let bgColor: string;\n  let textColor: string;\n  let borderColor: string;\n\n  switch (message.type) {\n    case \"error\":\n      bgColor = \"bg-red-50\";\n      textColor = \"text-red-700\";\n      borderColor = \"border-red-200\";\n      break;\n    case \"success\":\n      bgColor = \"bg-green-50\";\n      textColor = \"text-green-700\";\n      borderColor = \"border-green-200\";\n      break;\n    case \"info\":\n    default:\n      bgColor = \"bg-blue-50\";\n      textColor = \"text-blue-700\";\n      borderColor = \"border-blue-200\";\n      break;\n  }\n\n  return (\n    <div\n      className={`p-3 rounded-md border ${bgColor} ${borderColor} ${textColor} mb-4`}\n    >\n      <div className=\"flex items-center gap-2\">\n        <AlertCircle className=\"h-4 w-4\" />\n        <p className=\"text-sm\">{message.message}</p>\n      </div>\n    </div>\n  );\n};\n\nconst AuthDebugger = ({\n  serverUrl: serverUrl,\n  onBack,\n  authState,\n  updateAuthState,\n}: AuthDebuggerProps) => {\n  // Check for existing tokens on mount\n  useEffect(() => {\n    if (serverUrl && !authState.oauthTokens) {\n      const checkTokens = async () => {\n        try {\n          const provider = new DebugInspectorOAuthClientProvider(serverUrl);\n          const existingTokens = await provider.tokens();\n          if (existingTokens) {\n            updateAuthState({\n              oauthTokens: existingTokens,\n              oauthStep: \"complete\",\n            });\n          }\n        } catch (error) {\n          console.error(\"Failed to load existing OAuth tokens:\", error);\n        }\n      };\n      checkTokens();\n    }\n  }, [serverUrl, updateAuthState, authState.oauthTokens]);\n\n  const startOAuthFlow = useCallback(() => {\n    if (!serverUrl) {\n      updateAuthState({\n        statusMessage: {\n          type: \"error\",\n          message:\n            \"Please enter a server URL in the sidebar before authenticating\",\n        },\n      });\n      return;\n    }\n\n    updateAuthState({\n      oauthStep: \"metadata_discovery\",\n      authorizationUrl: null,\n      statusMessage: null,\n      latestError: null,\n    });\n  }, [serverUrl, updateAuthState]);\n\n  const stateMachine = useMemo(\n    () => new OAuthStateMachine(serverUrl, updateAuthState),\n    [serverUrl, updateAuthState],\n  );\n\n  const proceedToNextStep = useCallback(async () => {\n    if (!serverUrl) return;\n\n    try {\n      updateAuthState({\n        isInitiatingAuth: true,\n        statusMessage: null,\n        latestError: null,\n      });\n\n      await stateMachine.executeStep(authState);\n    } catch (error) {\n      console.error(\"OAuth flow error:\", error);\n      updateAuthState({\n        latestError: error instanceof Error ? error : new Error(String(error)),\n      });\n    } finally {\n      updateAuthState({ isInitiatingAuth: false });\n    }\n  }, [serverUrl, authState, updateAuthState, stateMachine]);\n\n  const handleQuickOAuth = useCallback(async () => {\n    if (!serverUrl) {\n      updateAuthState({\n        statusMessage: {\n          type: \"error\",\n          message:\n            \"Please enter a server URL in the sidebar before authenticating\",\n        },\n      });\n      return;\n    }\n\n    updateAuthState({ isInitiatingAuth: true, statusMessage: null });\n    try {\n      // Step through the OAuth flow using the state machine instead of the auth() function\n      let currentState: AuthDebuggerState = {\n        ...authState,\n        oauthStep: \"metadata_discovery\",\n        authorizationUrl: null,\n        latestError: null,\n      };\n\n      const oauthMachine = new OAuthStateMachine(serverUrl, (updates) => {\n        // Update our temporary state during the process\n        currentState = { ...currentState, ...updates };\n        // But don't call updateAuthState yet\n      });\n\n      // Manually step through each stage of the OAuth flow\n      while (currentState.oauthStep !== \"complete\") {\n        await oauthMachine.executeStep(currentState);\n        // In quick mode, we'll just redirect to the authorization URL\n        if (\n          currentState.oauthStep === \"authorization_code\" &&\n          currentState.authorizationUrl\n        ) {\n          // Validate the URL before redirecting\n          try {\n            validateRedirectUrl(currentState.authorizationUrl);\n          } catch (error) {\n            updateAuthState({\n              ...currentState,\n              isInitiatingAuth: false,\n              latestError:\n                error instanceof Error ? error : new Error(String(error)),\n              statusMessage: {\n                type: \"error\",\n                message: `Invalid authorization URL: ${error instanceof Error ? error.message : String(error)}`,\n              },\n            });\n            return;\n          }\n\n          // Store the current auth state before redirecting\n          sessionStorage.setItem(\n            SESSION_KEYS.AUTH_DEBUGGER_STATE,\n            JSON.stringify(currentState),\n          );\n          // Open the authorization URL automatically\n          window.location.href = currentState.authorizationUrl.toString();\n          break;\n        }\n      }\n\n      // After the flow completes or reaches a user-input step, update the app state\n      updateAuthState({\n        ...currentState,\n        statusMessage: {\n          type: \"info\",\n          message:\n            currentState.oauthStep === \"complete\"\n              ? \"Authentication completed successfully\"\n              : \"Please complete authentication in the opened window and enter the code\",\n        },\n      });\n    } catch (error) {\n      console.error(\"OAuth initialization error:\", error);\n      updateAuthState({\n        statusMessage: {\n          type: \"error\",\n          message: `Failed to start OAuth flow: ${error instanceof Error ? error.message : String(error)}`,\n        },\n      });\n    } finally {\n      updateAuthState({ isInitiatingAuth: false });\n    }\n  }, [serverUrl, updateAuthState, authState]);\n\n  const handleClearOAuth = useCallback(() => {\n    if (serverUrl) {\n      const serverAuthProvider = new DebugInspectorOAuthClientProvider(\n        serverUrl,\n      );\n      serverAuthProvider.clear();\n      updateAuthState({\n        ...EMPTY_DEBUGGER_STATE,\n        statusMessage: {\n          type: \"success\",\n          message: \"OAuth tokens cleared successfully\",\n        },\n      });\n\n      // Clear success message after 3 seconds\n      setTimeout(() => {\n        updateAuthState({ statusMessage: null });\n      }, 3000);\n    }\n  }, [serverUrl, updateAuthState]);\n\n  return (\n    <div className=\"w-full p-4\">\n      <div className=\"flex justify-between items-center mb-6\">\n        <h2 className=\"text-2xl font-bold\">Authentication Settings</h2>\n        <Button variant=\"outline\" onClick={onBack}>\n          Back to Connect\n        </Button>\n      </div>\n\n      <div className=\"w-full space-y-6\">\n        <div className=\"flex flex-col gap-6\">\n          <div className=\"grid w-full gap-2\">\n            <p className=\"text-muted-foreground mb-4\">\n              Configure authentication settings for your MCP server connection.\n            </p>\n\n            <div className=\"rounded-md border p-6 space-y-6\">\n              <h3 className=\"text-lg font-medium\">OAuth Authentication</h3>\n              <p className=\"text-sm text-muted-foreground mb-2\">\n                Use OAuth to securely authenticate with the MCP server.\n              </p>\n\n              {authState.statusMessage && (\n                <StatusMessage message={authState.statusMessage} />\n              )}\n\n              <div className=\"space-y-4\">\n                {authState.oauthTokens && (\n                  <div className=\"space-y-2\">\n                    <p className=\"text-sm font-medium\">Access Token:</p>\n                    <div className=\"bg-muted p-2 rounded-md text-xs overflow-x-auto\">\n                      {authState.oauthTokens.access_token.substring(0, 25)}...\n                    </div>\n                  </div>\n                )}\n\n                <div className=\"flex gap-4\">\n                  <Button\n                    variant=\"outline\"\n                    onClick={startOAuthFlow}\n                    disabled={authState.isInitiatingAuth}\n                  >\n                    {authState.oauthTokens\n                      ? \"Guided Token Refresh\"\n                      : \"Guided OAuth Flow\"}\n                  </Button>\n\n                  <Button\n                    onClick={handleQuickOAuth}\n                    disabled={authState.isInitiatingAuth}\n                  >\n                    {authState.isInitiatingAuth\n                      ? \"Initiating...\"\n                      : authState.oauthTokens\n                        ? \"Quick Refresh\"\n                        : \"Quick OAuth Flow\"}\n                  </Button>\n\n                  <Button variant=\"outline\" onClick={handleClearOAuth}>\n                    Clear OAuth State\n                  </Button>\n                </div>\n\n                <p className=\"text-xs text-muted-foreground\">\n                  Choose \"Guided\" for step-by-step instructions or \"Quick\" for\n                  the standard automatic flow.\n                </p>\n              </div>\n            </div>\n\n            <OAuthFlowProgress\n              serverUrl={serverUrl}\n              authState={authState}\n              updateAuthState={updateAuthState}\n              proceedToNextStep={proceedToNextStep}\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default AuthDebugger;\n"
  },
  {
    "path": "client/src/components/ConsoleTab.tsx",
    "content": "import { TabsContent } from \"@/components/ui/tabs\";\n\nconst ConsoleTab = () => (\n  <TabsContent value=\"console\" className=\"h-96\">\n    <div className=\"bg-gray-900 text-gray-100 p-4 rounded-lg h-full font-mono text-sm overflow-auto\">\n      <div className=\"opacity-50\">Welcome to MCP Client Console</div>\n      {/* Console output would go here */}\n    </div>\n  </TabsContent>\n);\n\nexport default ConsoleTab;\n"
  },
  {
    "path": "client/src/components/CustomHeaders.tsx",
    "content": "import { useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { Plus, Trash2, Eye, EyeOff } from \"lucide-react\";\nimport {\n  CustomHeaders as CustomHeadersType,\n  CustomHeader,\n  createEmptyHeader,\n} from \"@/lib/types/customHeaders\";\n\ninterface CustomHeadersProps {\n  headers: CustomHeadersType;\n  onChange: (headers: CustomHeadersType) => void;\n  className?: string;\n}\n\nconst CustomHeaders = ({\n  headers,\n  onChange,\n  className,\n}: CustomHeadersProps) => {\n  const [isJsonMode, setIsJsonMode] = useState(false);\n  const [jsonValue, setJsonValue] = useState(\"\");\n  const [jsonError, setJsonError] = useState<string | null>(null);\n  const [visibleValues, setVisibleValues] = useState<Set<number>>(new Set());\n\n  const updateHeader = (\n    index: number,\n    field: keyof CustomHeader,\n    value: string | boolean,\n  ) => {\n    const newHeaders = [...headers];\n    newHeaders[index] = { ...newHeaders[index], [field]: value };\n    onChange(newHeaders);\n  };\n\n  const addHeader = () => {\n    onChange([...headers, createEmptyHeader()]);\n  };\n\n  const removeHeader = (index: number) => {\n    const newHeaders = headers.filter((_, i) => i !== index);\n    onChange(newHeaders);\n  };\n\n  const toggleValueVisibility = (index: number) => {\n    const newVisible = new Set(visibleValues);\n    if (newVisible.has(index)) {\n      newVisible.delete(index);\n    } else {\n      newVisible.add(index);\n    }\n    setVisibleValues(newVisible);\n  };\n\n  const switchToJsonMode = () => {\n    const jsonObject: Record<string, string> = {};\n    headers.forEach((header) => {\n      if (header.enabled && header.name.trim() && header.value.trim()) {\n        jsonObject[header.name.trim()] = header.value.trim();\n      }\n    });\n    setJsonValue(JSON.stringify(jsonObject, null, 2));\n    setJsonError(null);\n    setIsJsonMode(true);\n  };\n\n  const switchToFormMode = () => {\n    try {\n      const parsed = JSON.parse(jsonValue);\n      if (\n        typeof parsed !== \"object\" ||\n        parsed === null ||\n        Array.isArray(parsed)\n      ) {\n        setJsonError(\"JSON must be an object with string key-value pairs\");\n        return;\n      }\n\n      const newHeaders: CustomHeadersType = Object.entries(parsed).map(\n        ([name, value]) => ({\n          name,\n          value: String(value),\n          enabled: true,\n        }),\n      );\n\n      onChange(newHeaders);\n      setJsonError(null);\n      setIsJsonMode(false);\n    } catch {\n      setJsonError(\"Invalid JSON format\");\n    }\n  };\n\n  const handleJsonChange = (value: string) => {\n    setJsonValue(value);\n    setJsonError(null);\n  };\n\n  if (isJsonMode) {\n    return (\n      <div className={`space-y-3 ${className}`}>\n        <div className=\"flex justify-between items-center gap-2\">\n          <h4 className=\"text-sm font-semibold flex-shrink-0\">\n            Custom Headers (JSON)\n          </h4>\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            size=\"sm\"\n            onClick={switchToFormMode}\n            className=\"flex-shrink-0\"\n          >\n            Switch to Form\n          </Button>\n        </div>\n        <div className=\"space-y-2\">\n          <Textarea\n            value={jsonValue}\n            onChange={(e) => handleJsonChange(e.target.value)}\n            placeholder='{\\n  \"Authorization\": \"Bearer token123\",\\n  \"X-Tenant-ID\": \"acme-inc\",\\n  \"X-Environment\": \"staging\"\\n}'\n            className=\"font-mono text-sm min-h-[100px] resize-none\"\n          />\n          {jsonError && <p className=\"text-sm text-red-600\">{jsonError}</p>}\n          <p className=\"text-xs text-muted-foreground\">\n            Enter headers as a JSON object with string key-value pairs.\n          </p>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className={`space-y-3 ${className}`}>\n      <div className=\"flex justify-between items-center gap-2\">\n        <h4 className=\"text-sm font-semibold flex-shrink-0\">Custom Headers</h4>\n        <div className=\"flex gap-1 flex-shrink-0\">\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            size=\"sm\"\n            onClick={switchToJsonMode}\n            className=\"text-xs px-2\"\n          >\n            JSON\n          </Button>\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            size=\"sm\"\n            onClick={addHeader}\n            className=\"text-xs px-2\"\n            data-testid=\"add-header-button\"\n          >\n            <Plus className=\"w-3 h-3 mr-1\" />\n            Add\n          </Button>\n        </div>\n      </div>\n\n      {headers.length === 0 ? (\n        <div className=\"text-center py-4 text-muted-foreground\">\n          <p className=\"text-sm\">No custom headers configured</p>\n          <p className=\"text-xs mt-1\">Click \"Add\" to get started</p>\n        </div>\n      ) : (\n        <div className=\"space-y-2 max-h-[300px] overflow-y-auto\">\n          {headers.map((header, index) => (\n            <div\n              key={index}\n              className=\"flex items-start gap-2 p-2 border rounded-md\"\n            >\n              <Switch\n                checked={header.enabled}\n                onCheckedChange={(enabled) =>\n                  updateHeader(index, \"enabled\", enabled)\n                }\n                className=\"shrink-0 mt-2\"\n              />\n              <div className=\"flex-1 min-w-0 space-y-2\">\n                <Input\n                  placeholder=\"Header Name\"\n                  value={header.name}\n                  onChange={(e) => updateHeader(index, \"name\", e.target.value)}\n                  className=\"font-mono text-xs\"\n                  data-testid={`header-name-input-${index}`}\n                />\n                <div className=\"relative\">\n                  <Input\n                    placeholder=\"Header Value\"\n                    value={header.value}\n                    onChange={(e) =>\n                      updateHeader(index, \"value\", e.target.value)\n                    }\n                    type={visibleValues.has(index) ? \"text\" : \"password\"}\n                    className=\"font-mono text-xs pr-8\"\n                    data-testid={`header-value-input-${index}`}\n                  />\n                  <Button\n                    type=\"button\"\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    onClick={() => toggleValueVisibility(index)}\n                    className=\"absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6 p-0\"\n                  >\n                    {visibleValues.has(index) ? (\n                      <EyeOff className=\"w-3 h-3\" />\n                    ) : (\n                      <Eye className=\"w-3 h-3\" />\n                    )}\n                  </Button>\n                </div>\n              </div>\n              <Button\n                type=\"button\"\n                variant=\"ghost\"\n                size=\"sm\"\n                onClick={() => removeHeader(index)}\n                className=\"shrink-0 text-red-600 hover:text-red-700 hover:bg-red-50 h-6 w-6 p-0 mt-2\"\n              >\n                <Trash2 className=\"w-3 h-3\" />\n              </Button>\n            </div>\n          ))}\n        </div>\n      )}\n\n      {headers.length > 0 && (\n        <p className=\"text-xs text-muted-foreground\">\n          Use the toggle to enable/disable headers. Only enabled headers with\n          both name and value will be sent.\n        </p>\n      )}\n    </div>\n  );\n};\n\nexport default CustomHeaders;\n"
  },
  {
    "path": "client/src/components/DynamicJsonForm.tsx",
    "content": "import {\n  useState,\n  useEffect,\n  useCallback,\n  useRef,\n  forwardRef,\n  useImperativeHandle,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport JsonEditor from \"./JsonEditor\";\nimport { updateValueAtPath } from \"@/utils/jsonUtils\";\nimport { generateDefaultValue } from \"@/utils/schemaUtils\";\nimport type {\n  JsonValue,\n  JsonSchemaType,\n  JsonSchemaConst,\n} from \"@/utils/jsonUtils\";\nimport { useToast } from \"@/lib/hooks/useToast\";\nimport { CheckCheck, Copy } from \"lucide-react\";\n\ninterface DynamicJsonFormProps {\n  schema: JsonSchemaType;\n  value: JsonValue;\n  onChange: (value: JsonValue) => void;\n  maxDepth?: number;\n}\n\nexport interface DynamicJsonFormRef {\n  validateJson: () => { isValid: boolean; error: string | null };\n  hasJsonError: () => boolean;\n}\n\nconst isTypeSupported = (\n  type: JsonSchemaType[\"type\"],\n  supportedTypes: string[],\n): boolean => {\n  if (Array.isArray(type)) {\n    return type.every((t) => supportedTypes.includes(t));\n  }\n  return typeof type === \"string\" && supportedTypes.includes(type);\n};\n\nconst isSimpleObject = (schema: JsonSchemaType): boolean => {\n  const supportedTypes = [\"string\", \"number\", \"integer\", \"boolean\", \"null\"];\n  if (schema.type && isTypeSupported(schema.type, supportedTypes)) return true;\n  if (schema.type === \"object\") {\n    return Object.values(schema.properties ?? {}).every(\n      (prop) => prop.type && isTypeSupported(prop.type, supportedTypes),\n    );\n  }\n  if (schema.type === \"array\") {\n    return !!schema.items && isSimpleObject(schema.items);\n  }\n  return false;\n};\n\nconst getArrayItemDefault = (schema: JsonSchemaType): JsonValue => {\n  if (\"default\" in schema && schema.default !== undefined) {\n    return schema.default;\n  }\n\n  switch (schema.type) {\n    case \"string\":\n      return \"\";\n    case \"number\":\n    case \"integer\":\n      return 0;\n    case \"boolean\":\n      return false;\n    case \"array\":\n      return [];\n    case \"object\":\n      return {};\n    case \"null\":\n      return null;\n    default:\n      return null;\n  }\n};\n\nconst DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(\n  ({ schema, value, onChange, maxDepth = 3 }, ref) => {\n    // Determine if we can render a form at the top level.\n    // This is more permissive than isSimpleObject():\n    // - Objects with any properties are form-capable (individual complex fields may still fallback to JSON)\n    // - Arrays with defined items are form-capable\n    // - Primitive types are form-capable\n    const canRenderTopLevelForm = (s: JsonSchemaType): boolean => {\n      const primitiveTypes = [\"string\", \"number\", \"integer\", \"boolean\", \"null\"];\n\n      const hasType = Array.isArray(s.type) ? s.type.length > 0 : !!s.type;\n      if (!hasType) return false;\n\n      const includesType = (t: string) =>\n        Array.isArray(s.type)\n          ? (s.type as ReadonlyArray<string>).includes(t)\n          : s.type === t;\n\n      // Primitive at top-level\n      if (primitiveTypes.some(includesType)) return true;\n\n      // Object with properties\n      if (includesType(\"object\")) {\n        const keys = Object.keys(s.properties ?? {});\n        return keys.length > 0;\n      }\n\n      // Array with items\n      if (includesType(\"array\")) {\n        return !!s.items;\n      }\n\n      return false;\n    };\n\n    const isOnlyJSON = !canRenderTopLevelForm(schema);\n    const [isJsonMode, setIsJsonMode] = useState(isOnlyJSON);\n    const [jsonError, setJsonError] = useState<string>();\n    const [copiedJson, setCopiedJson] = useState<boolean>(false);\n    const { toast } = useToast();\n\n    // Store the raw JSON string to allow immediate feedback during typing\n    // while deferring parsing until the user stops typing\n    const [rawJsonValue, setRawJsonValue] = useState<string>(\n      JSON.stringify(value ?? generateDefaultValue(schema), null, 2),\n    );\n    const [numericInputDrafts, setNumericInputDrafts] = useState<\n      Record<string, string>\n    >({});\n\n    // Use a ref to manage debouncing timeouts to avoid parsing JSON\n    // on every keystroke which would be inefficient and error-prone\n    const timeoutRef = useRef<ReturnType<typeof setTimeout>>();\n\n    const hasJsonError = () => {\n      return !!jsonError;\n    };\n\n    const getPathKey = (path: string[]) =>\n      path.length === 0 ? \"$root\" : path.join(\".\");\n\n    const getNumericDisplayValue = (\n      path: string[],\n      currentValue: JsonValue,\n    ): string => {\n      const pathKey = getPathKey(path);\n      if (Object.prototype.hasOwnProperty.call(numericInputDrafts, pathKey)) {\n        return numericInputDrafts[pathKey];\n      }\n      return typeof currentValue === \"number\" ? currentValue.toString() : \"\";\n    };\n\n    const updateNumericDraft = (path: string[], draftValue: string) => {\n      const pathKey = getPathKey(path);\n      setNumericInputDrafts((prev) => ({ ...prev, [pathKey]: draftValue }));\n    };\n\n    const clearNumericDraft = (path: string[]) => {\n      const pathKey = getPathKey(path);\n      setNumericInputDrafts((prev) => {\n        if (!Object.prototype.hasOwnProperty.call(prev, pathKey)) {\n          return prev;\n        }\n        const next = { ...prev };\n        delete next[pathKey];\n        return next;\n      });\n    };\n\n    // Debounce JSON parsing and parent updates to handle typing gracefully\n    const debouncedUpdateParent = useCallback(\n      (jsonString: string) => {\n        // Clear any existing timeout\n        if (timeoutRef.current) {\n          clearTimeout(timeoutRef.current);\n        }\n\n        // Set a new timeout\n        timeoutRef.current = setTimeout(() => {\n          try {\n            const parsed = JSON.parse(jsonString);\n            onChange(parsed);\n            setJsonError(undefined);\n          } catch (err) {\n            // For invalid JSON, set error and reset to default if it's clearly malformed\n            const errorMessage =\n              err instanceof Error ? err.message : \"Invalid JSON\";\n            setJsonError(errorMessage);\n\n            // Reset to default for clearly invalid JSON (not just incomplete typing)\n            const trimmed = jsonString?.trim();\n            if (trimmed && trimmed.length > 5 && !trimmed.match(/^[\\s[{]/)) {\n              onChange(generateDefaultValue(schema));\n            }\n          }\n        }, 300);\n      },\n      [onChange, setJsonError, schema],\n    );\n\n    // Update rawJsonValue when value prop changes\n    useEffect(() => {\n      if (!isJsonMode) {\n        setRawJsonValue(\n          JSON.stringify(value ?? generateDefaultValue(schema), null, 2),\n        );\n      }\n    }, [value, schema, isJsonMode]);\n\n    const handleSwitchToFormMode = () => {\n      if (isJsonMode) {\n        // When switching to Form mode, ensure we have valid JSON\n        try {\n          const parsed = JSON.parse(rawJsonValue);\n          // Update the parent component's state with the parsed value\n          onChange(parsed);\n          // Switch to form mode\n          setIsJsonMode(false);\n        } catch (err) {\n          setJsonError(err instanceof Error ? err.message : \"Invalid JSON\");\n        }\n      } else {\n        // Update raw JSON value when switching to JSON mode\n        setRawJsonValue(\n          JSON.stringify(value ?? generateDefaultValue(schema), null, 2),\n        );\n        setIsJsonMode(true);\n      }\n    };\n\n    const formatJson = () => {\n      try {\n        const jsonStr = rawJsonValue?.trim();\n        if (!jsonStr) {\n          return;\n        }\n        const formatted = JSON.stringify(JSON.parse(jsonStr), null, 2);\n        setRawJsonValue(formatted);\n        debouncedUpdateParent(formatted);\n        setJsonError(undefined);\n      } catch (err) {\n        setJsonError(err instanceof Error ? err.message : \"Invalid JSON\");\n      }\n    };\n\n    const validateJson = () => {\n      if (!isJsonMode) return { isValid: true, error: null };\n      try {\n        const jsonStr = rawJsonValue?.trim();\n        if (!jsonStr) return { isValid: true, error: null };\n        const parsed = JSON.parse(jsonStr);\n        // Clear any pending debounced update and immediately update parent\n        if (timeoutRef.current) {\n          clearTimeout(timeoutRef.current);\n        }\n        onChange(parsed);\n        setJsonError(undefined);\n        return { isValid: true, error: null };\n      } catch (err) {\n        const errorMessage =\n          err instanceof Error ? err.message : \"Invalid JSON\";\n        setJsonError(errorMessage);\n        return { isValid: false, error: errorMessage };\n      }\n    };\n\n    const handleCopyJson = useCallback(async () => {\n      try {\n        await navigator.clipboard.writeText(\n          JSON.stringify(value, null, 2) ?? \"[]\",\n        );\n        setCopiedJson(true);\n\n        toast({\n          title: \"JSON copied\",\n          description:\n            \"The JSON data has been successfully copied to your clipboard.\",\n        });\n\n        setTimeout(() => {\n          setCopiedJson(false);\n        }, 2000);\n      } catch (error) {\n        toast({\n          title: \"Error\",\n          description: `Failed to copy JSON: ${error instanceof Error ? error.message : String(error)}`,\n          variant: \"destructive\",\n        });\n      }\n    }, [toast, value]);\n\n    useImperativeHandle(ref, () => ({\n      validateJson,\n      hasJsonError,\n    }));\n\n    const renderFormFields = (\n      propSchema: JsonSchemaType,\n      currentValue: JsonValue,\n      path: string[] = [],\n      depth: number = 0,\n      parentSchema?: JsonSchemaType,\n      propertyName?: string,\n    ) => {\n      if (\n        depth >= maxDepth &&\n        (propSchema.type === \"object\" || propSchema.type === \"array\")\n      ) {\n        // Render as JSON editor when max depth is reached\n        return (\n          <JsonEditor\n            value={JSON.stringify(\n              currentValue ??\n                generateDefaultValue(propSchema, propertyName, parentSchema),\n              null,\n              2,\n            )}\n            onChange={(newValue) => {\n              try {\n                const parsed = JSON.parse(newValue);\n                handleFieldChange(path, parsed);\n                setJsonError(undefined);\n              } catch (err) {\n                setJsonError(\n                  err instanceof Error ? err.message : \"Invalid JSON\",\n                );\n              }\n            }}\n            error={jsonError}\n          />\n        );\n      }\n\n      // Check if this property is required in the parent schema\n      const isRequired =\n        parentSchema?.required?.includes(propertyName || \"\") ?? false;\n\n      let fieldType = propSchema.type;\n      if (Array.isArray(fieldType)) {\n        // Of the possible types, find the first non-null type to determine the control to render\n        fieldType = fieldType.find((t) => t !== \"null\") ?? fieldType[0];\n      }\n\n      switch (fieldType) {\n        case \"string\": {\n          // Titled single-select using oneOf/anyOf with const/title pairs\n          const titledOptions = (\n            (propSchema.oneOf ?? propSchema.anyOf) as\n              | (JsonSchemaType | JsonSchemaConst)[]\n              | undefined\n          )?.filter((opt): opt is JsonSchemaConst => \"const\" in opt);\n\n          if (titledOptions && titledOptions.length > 0) {\n            return (\n              <div className=\"space-y-2\">\n                {propSchema.description && (\n                  <p className=\"text-sm text-gray-600\">\n                    {propSchema.description}\n                  </p>\n                )}\n                <select\n                  value={(currentValue as string) ?? \"\"}\n                  onChange={(e) => {\n                    const val = e.target.value;\n                    if (!val && !isRequired) {\n                      handleFieldChange(path, undefined);\n                    } else {\n                      handleFieldChange(path, val);\n                    }\n                  }}\n                  required={isRequired}\n                  className=\"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800\"\n                >\n                  <option value=\"\">Select an option...</option>\n                  {titledOptions.map((option) => (\n                    <option\n                      key={String(option.const)}\n                      value={String(option.const)}\n                    >\n                      {option.title ?? String(option.const)}\n                    </option>\n                  ))}\n                </select>\n              </div>\n            );\n          }\n\n          // Untitled single-select using enum (with optional legacy enumNames for labels)\n          if (propSchema.enum) {\n            const names = Array.isArray(propSchema.enumNames)\n              ? propSchema.enumNames\n              : undefined;\n            return (\n              <div className=\"space-y-2\">\n                {propSchema.description && (\n                  <p className=\"text-sm text-gray-600\">\n                    {propSchema.description}\n                  </p>\n                )}\n                <select\n                  value={(currentValue as string) ?? \"\"}\n                  onChange={(e) => {\n                    const val = e.target.value;\n                    if (!val && !isRequired) {\n                      handleFieldChange(path, undefined);\n                    } else {\n                      handleFieldChange(path, val);\n                    }\n                  }}\n                  required={isRequired}\n                  className=\"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800\"\n                >\n                  <option value=\"\">Select an option...</option>\n                  {propSchema.enum.map((option, idx) => (\n                    <option key={option} value={option}>\n                      {names?.[idx] ?? option}\n                    </option>\n                  ))}\n                </select>\n              </div>\n            );\n          }\n\n          let inputType = \"text\";\n          switch (propSchema.format) {\n            case \"email\":\n              inputType = \"email\";\n              break;\n            case \"uri\":\n              inputType = \"url\";\n              break;\n            case \"date\":\n              inputType = \"date\";\n              break;\n            case \"date-time\":\n              inputType = \"datetime-local\";\n              break;\n            default:\n              inputType = \"text\";\n              break;\n          }\n\n          return (\n            <Input\n              type={inputType}\n              value={(currentValue as string) ?? \"\"}\n              onChange={(e) => {\n                const val = e.target.value;\n                // Always allow setting string values, including empty strings\n                handleFieldChange(path, val);\n              }}\n              placeholder={propSchema.description}\n              required={isRequired}\n              minLength={propSchema.minLength}\n              maxLength={propSchema.maxLength}\n              pattern={propSchema.pattern}\n            />\n          );\n        }\n\n        case \"number\":\n          return (\n            <Input\n              type=\"number\"\n              value={getNumericDisplayValue(path, currentValue)}\n              onChange={(e) => {\n                const val = e.target.value;\n                updateNumericDraft(path, val);\n                if (!val && !isRequired) {\n                  handleFieldChange(path, undefined);\n                } else {\n                  const num = Number(val);\n                  if (!isNaN(num)) {\n                    handleFieldChange(path, num);\n                  }\n                }\n              }}\n              onBlur={(e) => {\n                const val = e.target.value;\n                if (!val) {\n                  clearNumericDraft(path);\n                  return;\n                }\n\n                const num = Number(val);\n                if (!isNaN(num)) {\n                  handleFieldChange(path, num);\n                }\n                clearNumericDraft(path);\n              }}\n              placeholder={propSchema.description}\n              required={isRequired}\n              min={propSchema.minimum}\n              max={propSchema.maximum}\n            />\n          );\n\n        case \"integer\":\n          return (\n            <Input\n              type=\"number\"\n              step=\"1\"\n              value={getNumericDisplayValue(path, currentValue)}\n              onChange={(e) => {\n                const val = e.target.value;\n                updateNumericDraft(path, val);\n                if (!val && !isRequired) {\n                  handleFieldChange(path, undefined);\n                } else {\n                  const num = Number(val);\n                  if (!isNaN(num) && Number.isInteger(num)) {\n                    handleFieldChange(path, num);\n                  }\n                }\n              }}\n              onBlur={(e) => {\n                const val = e.target.value;\n                if (!val) {\n                  clearNumericDraft(path);\n                  return;\n                }\n\n                const num = Number(val);\n                if (!isNaN(num) && Number.isInteger(num)) {\n                  handleFieldChange(path, num);\n                }\n                clearNumericDraft(path);\n              }}\n              placeholder={propSchema.description}\n              required={isRequired}\n              min={propSchema.minimum}\n              max={propSchema.maximum}\n            />\n          );\n\n        case \"boolean\":\n          return (\n            <div className=\"space-y-2\">\n              {propSchema.description && (\n                <p className=\"text-sm text-gray-600\">\n                  {propSchema.description}\n                </p>\n              )}\n              <Input\n                type=\"checkbox\"\n                checked={(currentValue as boolean) ?? false}\n                onChange={(e) => handleFieldChange(path, e.target.checked)}\n                className=\"w-4 h-4\"\n                required={isRequired}\n              />\n            </div>\n          );\n        case \"null\":\n          return null;\n        case \"object\":\n          if (!propSchema.properties) {\n            return (\n              <JsonEditor\n                value={JSON.stringify(currentValue ?? {}, null, 2)}\n                onChange={(newValue) => {\n                  try {\n                    const parsed = JSON.parse(newValue);\n                    handleFieldChange(path, parsed);\n                    setJsonError(undefined);\n                  } catch (err) {\n                    setJsonError(\n                      err instanceof Error ? err.message : \"Invalid JSON\",\n                    );\n                  }\n                }}\n                error={jsonError}\n              />\n            );\n          }\n\n          return (\n            <div className=\"space-y-2 border rounded p-3\">\n              {Object.entries(propSchema.properties).map(([key, subSchema]) => (\n                <div key={key}>\n                  <label className=\"block text-sm font-medium mb-1\">\n                    {(subSchema as JsonSchemaType).title ?? key}\n                    {propSchema.required?.includes(key) && (\n                      <span className=\"text-red-500 ml-1\">*</span>\n                    )}\n                  </label>\n                  {renderFormFields(\n                    subSchema as JsonSchemaType,\n                    (currentValue as Record<string, JsonValue>)?.[key],\n                    [...path, key],\n                    depth + 1,\n                    propSchema,\n                    key,\n                  )}\n                </div>\n              ))}\n            </div>\n          );\n        case \"array\": {\n          const arrayValue = Array.isArray(currentValue) ? currentValue : [];\n          if (!propSchema.items) return null;\n\n          // Special handling: array of enums -> render multi-select control\n          const itemSchema = propSchema.items as JsonSchemaType;\n          let multiOptions: { value: string; label: string }[] | null = null;\n\n          const titledMulti = (\n            (itemSchema.anyOf ?? itemSchema.oneOf) as\n              | (JsonSchemaType | JsonSchemaConst)[]\n              | undefined\n          )?.filter((opt): opt is JsonSchemaConst => \"const\" in opt);\n\n          if (titledMulti && titledMulti.length > 0) {\n            multiOptions = titledMulti.map((o) => ({\n              value: String(o.const),\n              label: o.title ?? String(o.const),\n            }));\n          } else if (itemSchema.enum) {\n            const names = Array.isArray(itemSchema.enumNames)\n              ? itemSchema.enumNames\n              : undefined;\n            multiOptions = itemSchema.enum.map((v, i) => ({\n              value: v,\n              label: names?.[i] ?? v,\n            }));\n          }\n\n          if (multiOptions) {\n            const selectSize = Math.min(Math.max(multiOptions.length, 3), 8);\n            return (\n              <div className=\"space-y-2\">\n                {propSchema.description && (\n                  <p className=\"text-sm text-gray-600\">\n                    {propSchema.description}\n                  </p>\n                )}\n                <select\n                  multiple\n                  size={selectSize}\n                  value={arrayValue as string[]}\n                  onChange={(e) => {\n                    const selected = Array.from(\n                      (e.target as HTMLSelectElement).selectedOptions,\n                    ).map((o) => o.value);\n                    handleFieldChange(path, selected);\n                  }}\n                  className=\"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800\"\n                >\n                  {multiOptions.map((opt) => (\n                    <option key={opt.value} value={opt.value}>\n                      {opt.label}\n                    </option>\n                  ))}\n                </select>\n                {(propSchema.minItems || propSchema.maxItems) && (\n                  <p className=\"text-xs text-gray-500\">\n                    {propSchema.minItems\n                      ? `Select at least ${propSchema.minItems}. `\n                      : \"\"}\n                    {propSchema.maxItems\n                      ? `Select at most ${propSchema.maxItems}.`\n                      : \"\"}\n                  </p>\n                )}\n              </div>\n            );\n          }\n\n          // If the array items are simple, render as form fields, otherwise use JSON editor\n          if (isSimpleObject(propSchema.items)) {\n            return (\n              <div className=\"space-y-4\">\n                {propSchema.description && (\n                  <p className=\"text-sm text-gray-600\">\n                    {propSchema.description}\n                  </p>\n                )}\n\n                {propSchema.items?.description && (\n                  <p className=\"text-sm text-gray-500\">\n                    Items: {propSchema.items.description}\n                  </p>\n                )}\n\n                <div className=\"space-y-2\">\n                  {arrayValue.map((item, index) => (\n                    <div key={index} className=\"flex items-center gap-2\">\n                      {renderFormFields(\n                        propSchema.items as JsonSchemaType,\n                        item,\n                        [...path, index.toString()],\n                        depth + 1,\n                      )}\n                      <Button\n                        variant=\"outline\"\n                        size=\"sm\"\n                        onClick={() => {\n                          const newArray = [...arrayValue];\n                          newArray.splice(index, 1);\n                          handleFieldChange(path, newArray);\n                        }}\n                      >\n                        Remove\n                      </Button>\n                    </div>\n                  ))}\n                  <Button\n                    variant=\"outline\"\n                    size=\"sm\"\n                    onClick={() => {\n                      const defaultValue = getArrayItemDefault(\n                        propSchema.items as JsonSchemaType,\n                      );\n                      handleFieldChange(path, [...arrayValue, defaultValue]);\n                    }}\n                    title={\n                      propSchema.items?.description\n                        ? `Add new ${propSchema.items.description}`\n                        : \"Add new item\"\n                    }\n                  >\n                    Add Item\n                  </Button>\n                </div>\n              </div>\n            );\n          }\n\n          // For complex arrays, fall back to JSON editor\n          return (\n            <JsonEditor\n              value={JSON.stringify(currentValue ?? [], null, 2)}\n              onChange={(newValue) => {\n                try {\n                  const parsed = JSON.parse(newValue);\n                  handleFieldChange(path, parsed);\n                  setJsonError(undefined);\n                } catch (err) {\n                  setJsonError(\n                    err instanceof Error ? err.message : \"Invalid JSON\",\n                  );\n                }\n              }}\n              error={jsonError}\n            />\n          );\n        }\n        default:\n          return null;\n      }\n    };\n\n    const handleFieldChange = (path: string[], fieldValue: JsonValue) => {\n      if (path.length === 0) {\n        onChange(fieldValue);\n        return;\n      }\n\n      try {\n        const newValue = updateValueAtPath(value, path, fieldValue);\n        onChange(newValue);\n      } catch (error) {\n        console.error(\"Failed to update form value:\", error);\n        onChange(value);\n      }\n    };\n\n    const shouldUseJsonMode =\n      schema.type === \"object\" &&\n      (!schema.properties || Object.keys(schema.properties).length === 0);\n\n    useEffect(() => {\n      if (shouldUseJsonMode && !isJsonMode) {\n        setIsJsonMode(true);\n      }\n    }, [shouldUseJsonMode, isJsonMode]);\n\n    return (\n      <div className=\"space-y-4\">\n        <div className=\"flex justify-end space-x-2\">\n          {isJsonMode && (\n            <>\n              <Button\n                type=\"button\"\n                variant=\"outline\"\n                size=\"sm\"\n                onClick={handleCopyJson}\n              >\n                {copiedJson ? (\n                  <CheckCheck className=\"h-4 w-4 mr-2\" />\n                ) : (\n                  <Copy className=\"h-4 w-4 mr-2\" />\n                )}\n                Copy JSON\n              </Button>\n              <Button\n                type=\"button\"\n                variant=\"outline\"\n                size=\"sm\"\n                onClick={formatJson}\n              >\n                Format JSON\n              </Button>\n            </>\n          )}\n          {!isOnlyJSON && (\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              onClick={handleSwitchToFormMode}\n            >\n              {isJsonMode ? \"Switch to Form\" : \"Switch to JSON\"}\n            </Button>\n          )}\n        </div>\n\n        {isJsonMode ? (\n          <JsonEditor\n            value={rawJsonValue}\n            onChange={(newValue) => {\n              // Always update local state\n              setRawJsonValue(newValue);\n\n              // Use the debounced function to attempt parsing and updating parent\n              debouncedUpdateParent(newValue);\n            }}\n            error={jsonError}\n            placeholder={schema.description}\n          />\n        ) : // If schema type is object but value is not an object or is empty, and we have actual JSON data,\n        // render a simple representation of the JSON data\n        schema.type === \"object\" &&\n          (typeof value !== \"object\" ||\n            value === null ||\n            Object.keys(value).length === 0) &&\n          rawJsonValue &&\n          rawJsonValue !== \"{}\" ? (\n          <div className=\"space-y-4 border rounded-md p-4\">\n            <p className=\"text-sm text-gray-500\">\n              Form view not available for this JSON structure. Using simplified\n              view:\n            </p>\n            <pre className=\"bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto\">\n              {rawJsonValue}\n            </pre>\n            <p className=\"text-sm text-gray-500\">\n              Use JSON mode for full editing capabilities.\n            </p>\n          </div>\n        ) : (\n          renderFormFields(schema, value)\n        )}\n      </div>\n    );\n  },\n);\n\nexport default DynamicJsonForm;\n"
  },
  {
    "path": "client/src/components/ElicitationRequest.tsx",
    "content": "import { useState, useEffect } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport DynamicJsonForm from \"./DynamicJsonForm\";\nimport JsonView from \"./JsonView\";\nimport { JsonSchemaType, JsonValue } from \"@/utils/jsonUtils\";\nimport { generateDefaultValue } from \"@/utils/schemaUtils\";\nimport {\n  PendingElicitationRequest,\n  ElicitationResponse,\n} from \"./ElicitationTab\";\nimport Ajv from \"ajv\";\n\nexport type ElicitationRequestProps = {\n  request: PendingElicitationRequest;\n  onResolve: (id: number, response: ElicitationResponse) => void;\n};\n\nconst ElicitationRequest = ({\n  request,\n  onResolve,\n}: ElicitationRequestProps) => {\n  const [formData, setFormData] = useState<JsonValue>({});\n  const [validationError, setValidationError] = useState<string | null>(null);\n\n  useEffect(() => {\n    const defaultValue = generateDefaultValue(request.request.requestedSchema);\n    setFormData(defaultValue);\n    setValidationError(null);\n  }, [request.request.requestedSchema]);\n\n  const validateEmailFormat = (email: string): boolean => {\n    const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n    return emailRegex.test(email);\n  };\n\n  const validateFormData = (\n    data: JsonValue,\n    schema: JsonSchemaType,\n  ): boolean => {\n    if (\n      schema.type === \"object\" &&\n      schema.properties &&\n      typeof data === \"object\" &&\n      data !== null\n    ) {\n      const dataObj = data as Record<string, unknown>;\n\n      if (Array.isArray(schema.required)) {\n        for (const field of schema.required) {\n          const value = dataObj[field];\n          if (value === undefined || value === null || value === \"\") {\n            setValidationError(`Required field missing: ${field}`);\n            return false;\n          }\n        }\n      }\n\n      for (const [fieldName, fieldValue] of Object.entries(dataObj)) {\n        const fieldSchema = schema.properties[fieldName];\n        if (\n          fieldSchema &&\n          fieldSchema.format === \"email\" &&\n          typeof fieldValue === \"string\"\n        ) {\n          if (!validateEmailFormat(fieldValue)) {\n            setValidationError(`Invalid email format: ${fieldName}`);\n            return false;\n          }\n        }\n      }\n    }\n\n    return true;\n  };\n\n  const handleAccept = () => {\n    try {\n      if (!validateFormData(formData, request.request.requestedSchema)) {\n        return;\n      }\n\n      const ajv = new Ajv();\n      const validate = ajv.compile(request.request.requestedSchema);\n      const isValid = validate(formData);\n\n      if (!isValid) {\n        const errorMessage = ajv.errorsText(validate.errors);\n        setValidationError(errorMessage);\n        return;\n      }\n\n      onResolve(request.id, {\n        action: \"accept\",\n        content: formData as Record<string, unknown>,\n      });\n    } catch (error) {\n      setValidationError(\n        error instanceof Error ? error.message : \"Validation failed\",\n      );\n    }\n  };\n\n  const handleDecline = () => {\n    onResolve(request.id, { action: \"decline\" });\n  };\n\n  const handleCancel = () => {\n    onResolve(request.id, { action: \"cancel\" });\n  };\n\n  const schemaTitle =\n    request.request.requestedSchema.title || \"Information Request\";\n  const schemaDescription = request.request.requestedSchema.description;\n\n  return (\n    <div\n      data-testid=\"elicitation-request\"\n      className=\"flex gap-4 p-4 border rounded-lg space-y-4\"\n    >\n      <div className=\"flex-1 bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-2 rounded\">\n        <div className=\"space-y-2\">\n          <h4 className=\"font-semibold\">{schemaTitle}</h4>\n          <p className=\"text-sm\">{request.request.message}</p>\n          {schemaDescription && (\n            <p className=\"text-xs text-muted-foreground\">{schemaDescription}</p>\n          )}\n          <div className=\"mt-2\">\n            <h5 className=\"text-xs font-medium mb-1\">Request Schema:</h5>\n            <JsonView\n              data={JSON.stringify(request.request.requestedSchema, null, 2)}\n            />\n          </div>\n        </div>\n      </div>\n\n      <div className=\"flex-1 space-y-4\">\n        <div className=\"space-y-2\">\n          <h4 className=\"font-medium\">Response Form</h4>\n          <DynamicJsonForm\n            schema={request.request.requestedSchema}\n            value={formData}\n            onChange={(newValue: JsonValue) => {\n              setFormData(newValue);\n              setValidationError(null);\n            }}\n          />\n\n          {validationError && (\n            <div className=\"mt-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md\">\n              <div className=\"text-sm text-red-600 dark:text-red-400\">\n                <strong>Validation Error:</strong> {validationError}\n              </div>\n            </div>\n          )}\n        </div>\n\n        <div className=\"flex space-x-2 mt-1\">\n          <Button type=\"button\" onClick={handleAccept}>\n            Submit\n          </Button>\n          <Button type=\"button\" variant=\"outline\" onClick={handleDecline}>\n            Decline\n          </Button>\n          <Button type=\"button\" variant=\"outline\" onClick={handleCancel}>\n            Cancel\n          </Button>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default ElicitationRequest;\n"
  },
  {
    "path": "client/src/components/ElicitationTab.tsx",
    "content": "import { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport { TabsContent } from \"@/components/ui/tabs\";\nimport { JsonSchemaType } from \"@/utils/jsonUtils\";\nimport ElicitationRequest from \"./ElicitationRequest\";\n\nexport interface ElicitationRequestData {\n  id: number;\n  message: string;\n  requestedSchema: JsonSchemaType;\n}\n\nexport interface ElicitationResponse {\n  action: \"accept\" | \"decline\" | \"cancel\";\n  content?: Record<string, unknown>;\n}\n\nexport type PendingElicitationRequest = {\n  id: number;\n  request: ElicitationRequestData;\n  originatingTab?: string;\n};\n\nexport type Props = {\n  pendingRequests: PendingElicitationRequest[];\n  onResolve: (id: number, response: ElicitationResponse) => void;\n};\n\nconst ElicitationTab = ({ pendingRequests, onResolve }: Props) => {\n  return (\n    <TabsContent value=\"elicitations\">\n      <div className=\"h-96\">\n        <Alert>\n          <AlertDescription>\n            When the server requests information from the user, requests will\n            appear here for response.\n          </AlertDescription>\n        </Alert>\n        <div className=\"mt-4 space-y-4\">\n          <h3 className=\"text-lg font-semibold\">Recent Requests</h3>\n          {pendingRequests.map((request) => (\n            <ElicitationRequest\n              key={request.id}\n              request={request}\n              onResolve={onResolve}\n            />\n          ))}\n          {pendingRequests.length === 0 && (\n            <p className=\"text-gray-500\">No pending requests</p>\n          )}\n        </div>\n      </div>\n    </TabsContent>\n  );\n};\n\nexport default ElicitationTab;\n"
  },
  {
    "path": "client/src/components/HistoryAndNotifications.tsx",
    "content": "import { ServerNotification } from \"@modelcontextprotocol/sdk/types.js\";\nimport { useState } from \"react\";\nimport JsonView from \"./JsonView\";\nimport { Button } from \"@/components/ui/button\";\n\nconst HistoryAndNotifications = ({\n  requestHistory,\n  serverNotifications,\n  onClearHistory,\n  onClearNotifications,\n}: {\n  requestHistory: Array<{ request: string; response?: string }>;\n  serverNotifications: ServerNotification[];\n  onClearHistory?: () => void;\n  onClearNotifications?: () => void;\n}) => {\n  const [expandedRequests, setExpandedRequests] = useState<{\n    [key: number]: boolean;\n  }>({});\n  const [expandedNotifications, setExpandedNotifications] = useState<{\n    [key: number]: boolean;\n  }>({});\n\n  const toggleRequestExpansion = (index: number) => {\n    setExpandedRequests((prev) => ({ ...prev, [index]: !prev[index] }));\n  };\n\n  const toggleNotificationExpansion = (index: number) => {\n    setExpandedNotifications((prev) => ({ ...prev, [index]: !prev[index] }));\n  };\n\n  return (\n    <div className=\"bg-card overflow-hidden flex h-full\">\n      <div className=\"flex-1 overflow-y-auto p-4 border-r\">\n        <div className=\"flex items-center justify-between mb-4\">\n          <h2 className=\"text-lg font-semibold\">History</h2>\n          <Button\n            variant=\"outline\"\n            size=\"sm\"\n            onClick={onClearHistory}\n            disabled={requestHistory.length === 0}\n          >\n            Clear\n          </Button>\n        </div>\n        {requestHistory.length === 0 ? (\n          <p className=\"text-sm text-gray-500 dark:text-gray-400 italic\">\n            No history yet\n          </p>\n        ) : (\n          <ul className=\"space-y-3\">\n            {requestHistory\n              .slice()\n              .reverse()\n              .map((request, index) => (\n                <li\n                  key={index}\n                  className=\"text-sm text-foreground bg-secondary py-2 px-3 rounded\"\n                >\n                  <div\n                    className=\"flex justify-between items-center cursor-pointer\"\n                    onClick={() =>\n                      toggleRequestExpansion(requestHistory.length - 1 - index)\n                    }\n                  >\n                    <span className=\"font-mono\">\n                      {requestHistory.length - index}.{\" \"}\n                      {JSON.parse(request.request).method}\n                    </span>\n                    <span>\n                      {expandedRequests[requestHistory.length - 1 - index]\n                        ? \"▼\"\n                        : \"▶\"}\n                    </span>\n                  </div>\n                  {expandedRequests[requestHistory.length - 1 - index] && (\n                    <>\n                      <div className=\"mt-2\">\n                        <div className=\"flex justify-between items-center mb-1\">\n                          <span className=\"font-semibold text-blue-600\">\n                            Request:\n                          </span>\n                        </div>\n\n                        <JsonView\n                          data={request.request}\n                          className=\"bg-background\"\n                        />\n                      </div>\n                      {request.response && (\n                        <div className=\"mt-2\">\n                          <div className=\"flex justify-between items-center mb-1\">\n                            <span className=\"font-semibold text-green-600\">\n                              Response:\n                            </span>\n                          </div>\n                          <JsonView\n                            data={request.response}\n                            className=\"bg-background\"\n                          />\n                        </div>\n                      )}\n                    </>\n                  )}\n                </li>\n              ))}\n          </ul>\n        )}\n      </div>\n      <div className=\"flex-1 overflow-y-auto p-4\">\n        <div className=\"flex items-center justify-between mb-4\">\n          <h2 className=\"text-lg font-semibold\">Server Notifications</h2>\n          <Button\n            variant=\"outline\"\n            size=\"sm\"\n            onClick={onClearNotifications}\n            disabled={serverNotifications.length === 0}\n          >\n            Clear\n          </Button>\n        </div>\n        {serverNotifications.length === 0 ? (\n          <p className=\"text-sm text-gray-500 dark:text-gray-400 italic\">\n            No notifications yet\n          </p>\n        ) : (\n          <ul className=\"space-y-3\">\n            {serverNotifications\n              .slice()\n              .reverse()\n              .map((notification, index) => (\n                <li\n                  key={index}\n                  className=\"text-sm text-foreground bg-secondary py-2 px-3 rounded\"\n                >\n                  <div\n                    className=\"flex justify-between items-center cursor-pointer\"\n                    onClick={() =>\n                      toggleNotificationExpansion(\n                        serverNotifications.length - 1 - index,\n                      )\n                    }\n                  >\n                    <span className=\"font-mono\">\n                      {serverNotifications.length - index}.{\" \"}\n                      {notification.method}\n                    </span>\n                    <span>\n                      {expandedNotifications[\n                        serverNotifications.length - 1 - index\n                      ]\n                        ? \"▼\"\n                        : \"▶\"}\n                    </span>\n                  </div>\n                  {expandedNotifications[\n                    serverNotifications.length - 1 - index\n                  ] && (\n                    <div className=\"mt-2\">\n                      <div className=\"flex justify-between items-center mb-1\">\n                        <span className=\"font-semibold text-purple-600\">\n                          Details:\n                        </span>\n                      </div>\n                      <JsonView\n                        data={JSON.stringify(notification, null, 2)}\n                        className=\"bg-background\"\n                      />\n                    </div>\n                  )}\n                </li>\n              ))}\n          </ul>\n        )}\n      </div>\n    </div>\n  );\n};\n\nexport default HistoryAndNotifications;\n"
  },
  {
    "path": "client/src/components/IconDisplay.tsx",
    "content": "// Define Icon type locally since it might not be exported yet\ninterface Icon {\n  src: string;\n  mimeType?: string;\n  sizes?: string[];\n  theme?: \"light\" | \"dark\";\n}\n\n// Helper type for objects that may have icons\nexport interface WithIcons {\n  icons?: Icon[];\n}\n\ninterface IconDisplayProps {\n  icons?: Icon[];\n  className?: string;\n  size?: \"sm\" | \"md\" | \"lg\";\n}\n\nconst IconDisplay = ({\n  icons,\n  className = \"\",\n  size = \"md\",\n}: IconDisplayProps) => {\n  if (!icons || icons.length === 0) {\n    return null;\n  }\n\n  const sizeClasses = {\n    sm: \"w-4 h-4\",\n    md: \"w-6 h-6\",\n    lg: \"w-8 h-8\",\n  };\n\n  const sizeClass = sizeClasses[size];\n\n  return (\n    <div className={`flex gap-1 ${className}`}>\n      {icons.map((icon, index) => (\n        <img\n          key={index}\n          src={icon.src}\n          alt=\"\"\n          className={`${sizeClass} object-contain flex-shrink-0`}\n          style={{\n            imageRendering: \"auto\",\n          }}\n          onError={(e) => {\n            // Hide broken images\n            e.currentTarget.style.display = \"none\";\n          }}\n        />\n      ))}\n    </div>\n  );\n};\n\nexport default IconDisplay;\n"
  },
  {
    "path": "client/src/components/JsonEditor.tsx",
    "content": "import { useState, useEffect } from \"react\";\nimport Editor from \"react-simple-code-editor\";\nimport Prism from \"prismjs\";\nimport \"prismjs/components/prism-json\";\nimport \"prismjs/themes/prism.css\";\n\ninterface JsonEditorProps {\n  value: string;\n  onChange: (value: string) => void;\n  error?: string;\n  placeholder?: string;\n}\n\nconst JsonEditor = ({\n  value,\n  onChange,\n  error: externalError,\n  placeholder,\n}: JsonEditorProps) => {\n  const [editorContent, setEditorContent] = useState(value || \"\");\n  const [internalError, setInternalError] = useState<string | undefined>(\n    undefined,\n  );\n\n  useEffect(() => {\n    setEditorContent(value || \"\");\n  }, [value]);\n\n  const handleEditorChange = (newContent: string) => {\n    setEditorContent(newContent);\n    setInternalError(undefined);\n    onChange(newContent);\n  };\n\n  const displayError = internalError || externalError;\n\n  return (\n    <div className=\"relative\">\n      <div\n        className={`border rounded-md ${\n          displayError\n            ? \"border-red-500\"\n            : \"border-gray-200 dark:border-gray-800\"\n        }`}\n      >\n        <Editor\n          value={editorContent}\n          onValueChange={handleEditorChange}\n          highlight={(code) =>\n            Prism.highlight(code, Prism.languages.json, \"json\")\n          }\n          padding={10}\n          placeholder={placeholder}\n          style={{\n            fontFamily: '\"Fira code\", \"Fira Mono\", monospace',\n            fontSize: 14,\n            backgroundColor: \"transparent\",\n            minHeight: \"100px\",\n          }}\n          className=\"w-full\"\n        />\n      </div>\n      {displayError && (\n        <p className=\"text-sm text-red-500 mt-1\">{displayError}</p>\n      )}\n    </div>\n  );\n};\n\nexport default JsonEditor;\n"
  },
  {
    "path": "client/src/components/JsonView.tsx",
    "content": "import { useState, memo, useMemo, useCallback, useEffect } from \"react\";\nimport type React from \"react\";\nimport type { JsonValue } from \"@/utils/jsonUtils\";\nimport clsx from \"clsx\";\nimport { Copy, CheckCheck } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { useToast } from \"@/lib/hooks/useToast\";\nimport { getDataType, tryParseJson } from \"@/utils/jsonUtils\";\nimport useCopy from \"@/lib/hooks/useCopy\";\n\ninterface JsonViewProps {\n  data: unknown;\n  name?: string;\n  initialExpandDepth?: number;\n  className?: string;\n  withCopyButton?: boolean;\n  isError?: boolean;\n}\n\nconst JsonView = memo(\n  ({\n    data,\n    name,\n    initialExpandDepth = 3,\n    className,\n    withCopyButton = true,\n    isError = false,\n  }: JsonViewProps) => {\n    const { toast } = useToast();\n    const { copied, setCopied } = useCopy();\n\n    const normalizedData = useMemo(() => {\n      return typeof data === \"string\"\n        ? tryParseJson(data).success\n          ? tryParseJson(data).data\n          : data\n        : data;\n    }, [data]);\n\n    const handleCopy = useCallback(() => {\n      try {\n        navigator.clipboard.writeText(\n          typeof normalizedData === \"string\"\n            ? normalizedData\n            : JSON.stringify(normalizedData, null, 2),\n        );\n        setCopied(true);\n      } catch (error) {\n        toast({\n          title: \"Error\",\n          description: `There was an error coping result into the clipboard: ${error instanceof Error ? error.message : String(error)}`,\n          variant: \"destructive\",\n        });\n      }\n    }, [toast, normalizedData, setCopied]);\n\n    return (\n      <div className={clsx(\"p-4 border rounded relative\", className)}>\n        {withCopyButton && (\n          <Button\n            size=\"icon\"\n            variant=\"ghost\"\n            className=\"absolute top-2 right-2\"\n            onClick={handleCopy}\n          >\n            {copied ? (\n              <CheckCheck className=\"size-4 dark:text-green-700 text-green-600\" />\n            ) : (\n              <Copy className=\"size-4 text-foreground\" />\n            )}\n          </Button>\n        )}\n        <div className=\"font-mono text-sm transition-all duration-300\">\n          <JsonNode\n            data={normalizedData as JsonValue}\n            name={name}\n            depth={0}\n            initialExpandDepth={initialExpandDepth}\n            isError={isError}\n          />\n        </div>\n      </div>\n    );\n  },\n);\n\nJsonView.displayName = \"JsonView\";\n\ninterface JsonNodeProps {\n  data: JsonValue;\n  name?: string;\n  depth: number;\n  initialExpandDepth: number;\n  isError?: boolean;\n}\n\nconst JsonNode = memo(\n  ({\n    data,\n    name,\n    depth = 0,\n    initialExpandDepth,\n    isError = false,\n  }: JsonNodeProps) => {\n    const { toast } = useToast();\n    const [isExpanded, setIsExpanded] = useState(depth < initialExpandDepth);\n    const [typeStyleMap] = useState<Record<string, string>>({\n      number: \"text-blue-600\",\n      boolean: \"text-amber-600\",\n      null: \"text-purple-600\",\n      undefined: \"text-gray-600\",\n      string: \"text-green-600 group-hover:text-green-500\",\n      error: \"text-red-600 group-hover:text-red-500\",\n      default: \"text-gray-700\",\n    });\n    const dataType = getDataType(data);\n\n    const [copied, setCopied] = useState(false);\n    useEffect(() => {\n      let timeoutId: NodeJS.Timeout;\n      if (copied) {\n        timeoutId = setTimeout(() => setCopied(false), 500);\n      }\n      return () => {\n        if (timeoutId) clearTimeout(timeoutId);\n      };\n    }, [copied]);\n\n    const handleCopyValue = useCallback(\n      (value: JsonValue) => {\n        try {\n          let text: string;\n          const valueType = getDataType(value);\n          switch (valueType) {\n            case \"string\":\n              text = value as unknown as string;\n              break;\n            case \"number\":\n            case \"boolean\":\n              text = String(value);\n              break;\n            case \"null\":\n              text = \"null\";\n              break;\n            case \"undefined\":\n              text = \"undefined\";\n              break;\n            default:\n              text = JSON.stringify(value);\n          }\n          navigator.clipboard.writeText(text);\n          setCopied(true);\n        } catch (error) {\n          toast({\n            title: \"Error\",\n            description: `There was an error coping result into the clipboard: ${error instanceof Error ? error.message : String(error)}`,\n            variant: \"destructive\",\n          });\n        }\n      },\n      [toast],\n    );\n\n    const renderCollapsible = (isArray: boolean) => {\n      const items = isArray\n        ? (data as JsonValue[])\n        : Object.entries(data as Record<string, JsonValue>);\n      const itemCount = items.length;\n      const isEmpty = itemCount === 0;\n\n      const symbolMap = {\n        open: isArray ? \"[\" : \"{\",\n        close: isArray ? \"]\" : \"}\",\n        collapsed: isArray ? \"[ ... ]\" : \"{ ... }\",\n        empty: isArray ? \"[]\" : \"{}\",\n      };\n\n      if (isEmpty) {\n        return (\n          <div className=\"flex items-center\">\n            {name && (\n              <span className=\"mr-1 text-gray-600 dark:text-gray-400\">\n                {name}:\n              </span>\n            )}\n            <span className=\"text-gray-500\">{symbolMap.empty}</span>\n          </div>\n        );\n      }\n\n      return (\n        <div className=\"flex flex-col\">\n          <div\n            className=\"flex items-center mr-1 rounded cursor-pointer group hover:bg-gray-800/10 dark:hover:bg-gray-800/20\"\n            onClick={() => setIsExpanded(!isExpanded)}\n          >\n            {name && (\n              <span className=\"mr-1 text-gray-600 dark:text-gray-400 dark:group-hover:text-gray-100 group-hover:text-gray-400\">\n                {name}:\n              </span>\n            )}\n            {isExpanded ? (\n              <span className=\"text-gray-600 dark:text-gray-400 dark:group-hover:text-gray-100 group-hover:text-gray-400\">\n                {symbolMap.open}\n              </span>\n            ) : (\n              <>\n                <span className=\"text-gray-600 dark:group-hover:text-gray-100 group-hover:text-gray-400\">\n                  {symbolMap.collapsed}\n                </span>\n                <span className=\"ml-1 text-gray-700 dark:group-hover:text-gray-100 group-hover:text-gray-400\">\n                  {itemCount} {itemCount === 1 ? \"item\" : \"items\"}\n                </span>\n              </>\n            )}\n          </div>\n          {isExpanded && (\n            <>\n              <div className=\"pl-2 ml-4 border-l border-gray-200 dark:border-gray-800\">\n                {isArray\n                  ? (items as JsonValue[]).map((item, index) => (\n                      <div key={index} className=\"my-1\">\n                        <JsonNode\n                          data={item}\n                          name={`${index}`}\n                          depth={depth + 1}\n                          initialExpandDepth={initialExpandDepth}\n                        />\n                      </div>\n                    ))\n                  : (items as [string, JsonValue][]).map(([key, value]) => (\n                      <div key={key} className=\"my-1\">\n                        <JsonNode\n                          data={value}\n                          name={key}\n                          depth={depth + 1}\n                          initialExpandDepth={initialExpandDepth}\n                        />\n                      </div>\n                    ))}\n              </div>\n              <div className=\"text-gray-600 dark:text-gray-400\">\n                {symbolMap.close}\n              </div>\n            </>\n          )}\n        </div>\n      );\n    };\n\n    const renderString = (value: string) => {\n      const maxLength = 100;\n      const isTooLong = value.length > maxLength;\n\n      if (!isTooLong) {\n        return (\n          <div className=\"flex mr-1 rounded hover:bg-gray-800/20 group items-start\">\n            {name && (\n              <span className=\"mr-1 text-gray-600 dark:text-gray-400\">\n                {name}:\n              </span>\n            )}\n            <pre\n              className={clsx(\n                isError ? typeStyleMap.error : typeStyleMap.string,\n                \"break-all whitespace-pre-wrap\",\n              )}\n            >\n              \"{value}\"\n            </pre>\n            <Button\n              variant=\"ghost\"\n              className=\"ml-1 h-6 w-6 p-0 opacity-0 group-hover:opacity-100\"\n              onClick={(e: React.MouseEvent<HTMLButtonElement>) => {\n                e.stopPropagation();\n                handleCopyValue(value as unknown as JsonValue);\n              }}\n              aria-label={name ? `Copy value of ${name}` : \"Copy value\"}\n              title={name ? `Copy value of ${name}` : \"Copy value\"}\n            >\n              {copied ? (\n                <CheckCheck className=\"size-4 dark:text-green-700 text-green-600\" />\n              ) : (\n                <Copy className=\"size-4 text-foreground\" />\n              )}\n            </Button>\n          </div>\n        );\n      }\n\n      return (\n        <div className=\"flex mr-1 rounded group hover:bg-gray-800/20 items-start\">\n          {name && (\n            <span className=\"mr-1 text-gray-600 dark:text-gray-400 dark:group-hover:text-gray-100 group-hover:text-gray-400\">\n              {name}:\n            </span>\n          )}\n          <pre\n            className={clsx(\n              isError ? typeStyleMap.error : typeStyleMap.string,\n              \"cursor-pointer break-all whitespace-pre-wrap\",\n            )}\n            onClick={() => setIsExpanded(!isExpanded)}\n            title={isExpanded ? \"Click to collapse\" : \"Click to expand\"}\n          >\n            {isExpanded ? `\"${value}\"` : `\"${value.slice(0, maxLength)}...\"`}\n          </pre>\n          <Button\n            variant=\"ghost\"\n            className=\"ml-1 h-6 w-6 p-0 opacity-0 group-hover:opacity-100\"\n            onClick={(e: React.MouseEvent<HTMLButtonElement>) => {\n              e.stopPropagation();\n              handleCopyValue(value as unknown as JsonValue);\n            }}\n            aria-label={name ? `Copy value of ${name}` : \"Copy value\"}\n            title={name ? `Copy value of ${name}` : \"Copy value\"}\n          >\n            {copied ? (\n              <CheckCheck className=\"size-4 dark:text-green-700 text-green-600\" />\n            ) : (\n              <Copy className=\"size-4 text-foreground\" />\n            )}\n          </Button>\n        </div>\n      );\n    };\n\n    switch (dataType) {\n      case \"object\":\n      case \"array\":\n        return renderCollapsible(dataType === \"array\");\n      case \"string\":\n        return renderString(data as string);\n      default:\n        return (\n          <div className=\"flex items-center mr-1 rounded hover:bg-gray-800/20 group\">\n            {name && (\n              <span className=\"mr-1 text-gray-600 dark:text-gray-400\">\n                {name}:\n              </span>\n            )}\n            <span className={typeStyleMap[dataType] || typeStyleMap.default}>\n              {data === null ? \"null\" : String(data)}\n            </span>\n            <Button\n              variant=\"ghost\"\n              className=\"ml-1 h-6 w-6 p-0 opacity-0 group-hover:opacity-100\"\n              onClick={(e: React.MouseEvent<HTMLButtonElement>) => {\n                e.stopPropagation();\n                handleCopyValue(data as JsonValue);\n              }}\n              aria-label={name ? `Copy value of ${name}` : \"Copy value\"}\n              title={name ? `Copy value of ${name}` : \"Copy value\"}\n            >\n              {copied ? (\n                <CheckCheck className=\"size-4 dark:text-green-700 text-green-600\" />\n              ) : (\n                <Copy className=\"size-4 text-foreground\" />\n              )}\n            </Button>\n          </div>\n        );\n    }\n  },\n);\n\nJsonNode.displayName = \"JsonNode\";\n\nexport default JsonView;\n"
  },
  {
    "path": "client/src/components/ListPane.tsx",
    "content": "import { Search } from \"lucide-react\";\nimport { Button } from \"./ui/button\";\nimport { Input } from \"./ui/input\";\nimport { useState, useMemo, useRef } from \"react\";\n\ntype ListPaneProps<T> = {\n  items: T[];\n  listItems: () => void;\n  clearItems?: () => void;\n  setSelectedItem: (item: T) => void;\n  renderItem: (item: T) => React.ReactNode;\n  title: string;\n  buttonText: string;\n  isButtonDisabled?: boolean;\n};\n\nconst ListPane = <T extends object>({\n  items,\n  listItems,\n  clearItems,\n  setSelectedItem,\n  renderItem,\n  title,\n  buttonText,\n  isButtonDisabled,\n}: ListPaneProps<T>) => {\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [isSearchExpanded, setIsSearchExpanded] = useState(false);\n  const searchInputRef = useRef<HTMLInputElement>(null);\n\n  const filteredItems = useMemo(() => {\n    if (!searchQuery.trim()) return items;\n\n    return items.filter((item) => {\n      const searchableText = [\n        (item as { name?: string }).name || \"\",\n        (item as { description?: string }).description || \"\",\n      ]\n        .join(\" \")\n        .toLowerCase();\n      return searchableText.includes(searchQuery.toLowerCase());\n    });\n  }, [items, searchQuery]);\n\n  const handleSearchClick = () => {\n    setIsSearchExpanded(true);\n    setTimeout(() => {\n      searchInputRef.current?.focus();\n    }, 100);\n  };\n\n  const handleSearchBlur = () => {\n    if (!searchQuery.trim()) {\n      setIsSearchExpanded(false);\n    }\n  };\n\n  return (\n    <div className=\"bg-card border border-border rounded-lg shadow\">\n      <div className=\"p-4 border-b border-gray-200 dark:border-border\">\n        <div className=\"flex items-center justify-between gap-4\">\n          <h3 className=\"font-semibold dark:text-white flex-shrink-0\">\n            {title}\n          </h3>\n          <div className=\"flex items-center justify-end min-w-0 flex-1\">\n            {!isSearchExpanded ? (\n              <button\n                name=\"search\"\n                aria-label=\"Search\"\n                onClick={handleSearchClick}\n                className=\"p-2 hover:bg-gray-100 dark:hover:bg-secondary rounded-md transition-all duration-300 ease-in-out\"\n              >\n                <Search className=\"w-4 h-4 text-muted-foreground\" />\n              </button>\n            ) : (\n              <div className=\"flex items-center w-full max-w-xs\">\n                <div className=\"relative w-full\">\n                  <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none z-10\" />\n                  <Input\n                    ref={searchInputRef}\n                    name=\"search\"\n                    type=\"text\"\n                    placeholder=\"Search...\"\n                    value={searchQuery}\n                    onChange={(e) => setSearchQuery(e.target.value)}\n                    onBlur={handleSearchBlur}\n                    className=\"pl-10 w-full transition-all duration-300 ease-in-out\"\n                  />\n                </div>\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n      <div className=\"p-4\">\n        <Button\n          variant=\"outline\"\n          className=\"w-full mb-4\"\n          onClick={listItems}\n          disabled={isButtonDisabled}\n        >\n          {buttonText}\n        </Button>\n        {clearItems && (\n          <Button\n            variant=\"outline\"\n            className=\"w-full mb-4\"\n            onClick={clearItems}\n            disabled={items.length === 0}\n          >\n            Clear\n          </Button>\n        )}\n        <div className=\"space-y-2 overflow-y-auto max-h-96\">\n          {filteredItems.map((item, index) => (\n            <div\n              key={index}\n              className=\"flex items-center py-2 px-4 rounded hover:bg-gray-50 dark:hover:bg-secondary cursor-pointer\"\n              onClick={() => setSelectedItem(item)}\n            >\n              {renderItem(item)}\n            </div>\n          ))}\n          {filteredItems.length === 0 && searchQuery && items.length > 0 && (\n            <div className=\"text-center py-4 text-muted-foreground\">\n              No items found matching &quot;{searchQuery}&quot;\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default ListPane;\n"
  },
  {
    "path": "client/src/components/MetadataTab.tsx",
    "content": "import React, { useState } from \"react\";\nimport { TabsContent } from \"@/components/ui/tabs\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Trash2, Plus } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  META_NAME_RULES_MESSAGE,\n  META_PREFIX_RULES_MESSAGE,\n  RESERVED_NAMESPACE_MESSAGE,\n  hasValidMetaName,\n  hasValidMetaPrefix,\n  isReservedMetaKey,\n} from \"@/utils/metaUtils\";\n\ninterface MetadataEntry {\n  key: string;\n  value: string;\n}\n\ninterface MetadataTabProps {\n  metadata: Record<string, string>;\n  onMetadataChange: (metadata: Record<string, string>) => void;\n}\n\nconst MetadataTab: React.FC<MetadataTabProps> = ({\n  metadata,\n  onMetadataChange,\n}) => {\n  const [entries, setEntries] = useState<MetadataEntry[]>(() => {\n    return Object.entries(metadata).map(([key, value]) => ({ key, value }));\n  });\n\n  const addEntry = () => {\n    setEntries([...entries, { key: \"\", value: \"\" }]);\n  };\n\n  const removeEntry = (index: number) => {\n    const newEntries = entries.filter((_, i) => i !== index);\n    setEntries(newEntries);\n    updateMetadata(newEntries);\n  };\n\n  const updateEntry = (\n    index: number,\n    field: \"key\" | \"value\",\n    value: string,\n  ) => {\n    const newEntries = [...entries];\n    newEntries[index][field] = value;\n    setEntries(newEntries);\n    updateMetadata(newEntries);\n  };\n\n  const updateMetadata = (newEntries: MetadataEntry[]) => {\n    const metadataObject: Record<string, string> = {};\n    newEntries.forEach(({ key, value }) => {\n      const trimmedKey = key.trim();\n      if (\n        trimmedKey &&\n        value.trim() &&\n        hasValidMetaPrefix(trimmedKey) &&\n        !isReservedMetaKey(trimmedKey) &&\n        hasValidMetaName(trimmedKey)\n      ) {\n        metadataObject[trimmedKey] = value.trim();\n      }\n    });\n    onMetadataChange(metadataObject);\n  };\n\n  return (\n    <TabsContent value=\"metadata\">\n      <div className=\"space-y-4\">\n        <div className=\"flex items-center justify-between\">\n          <div>\n            <h3 className=\"text-lg font-semibold\">Metadata</h3>\n            <p className=\"text-sm text-gray-600 dark:text-gray-400\">\n              Key-value pairs that will be included in all MCP requests\n            </p>\n          </div>\n          <Button onClick={addEntry} size=\"sm\">\n            <Plus className=\"w-4 h-4 mr-2\" />\n            Add Entry\n          </Button>\n        </div>\n\n        <div className=\"space-y-3\">\n          {entries.map((entry, index) => {\n            const trimmedKey = entry.key.trim();\n            const hasInvalidPrefix =\n              trimmedKey !== \"\" && !hasValidMetaPrefix(trimmedKey);\n            const isReservedKey =\n              trimmedKey !== \"\" && isReservedMetaKey(trimmedKey);\n            const hasInvalidName =\n              trimmedKey !== \"\" && !hasValidMetaName(trimmedKey);\n            const validationMessage = hasInvalidPrefix\n              ? META_PREFIX_RULES_MESSAGE\n              : isReservedKey\n                ? RESERVED_NAMESPACE_MESSAGE\n                : hasInvalidName\n                  ? META_NAME_RULES_MESSAGE\n                  : null;\n            return (\n              <div key={index} className=\"space-y-1\">\n                <div className=\"flex items-center space-x-2\">\n                  <div className=\"flex-1\">\n                    <Label htmlFor={`key-${index}`} className=\"sr-only\">\n                      Key\n                    </Label>\n                    <Input\n                      id={`key-${index}`}\n                      placeholder=\"Key\"\n                      value={entry.key}\n                      onChange={(e) =>\n                        updateEntry(index, \"key\", e.target.value)\n                      }\n                      aria-invalid={Boolean(validationMessage)}\n                      className={cn(\n                        validationMessage &&\n                          \"border-red-500 focus-visible:ring-red-500 focus-visible:ring-1\",\n                      )}\n                    />\n                  </div>\n                  <div className=\"flex-1\">\n                    <Label htmlFor={`value-${index}`} className=\"sr-only\">\n                      Value\n                    </Label>\n                    <Input\n                      id={`value-${index}`}\n                      placeholder=\"Value\"\n                      value={entry.value}\n                      onChange={(e) =>\n                        updateEntry(index, \"value\", e.target.value)\n                      }\n                      disabled={Boolean(validationMessage)}\n                    />\n                  </div>\n                  <Button\n                    variant=\"outline\"\n                    size=\"sm\"\n                    onClick={() => removeEntry(index)}\n                  >\n                    <Trash2 className=\"w-4 h-4\" />\n                  </Button>\n                </div>\n                {validationMessage && (\n                  <p className=\"text-xs text-red-600 dark:text-red-400\">\n                    {validationMessage}\n                  </p>\n                )}\n              </div>\n            );\n          })}\n        </div>\n\n        {entries.length === 0 && (\n          <div className=\"text-center py-8\">\n            <p className=\"text-gray-500 dark:text-gray-400 mb-4\">\n              No metadata entries. Click \"Add Entry\" to add key-value pairs.\n            </p>\n          </div>\n        )}\n      </div>\n    </TabsContent>\n  );\n};\n\nexport default MetadataTab;\n"
  },
  {
    "path": "client/src/components/OAuthCallback.tsx",
    "content": "import { useEffect, useRef } from \"react\";\nimport { InspectorOAuthClientProvider } from \"../lib/auth\";\nimport { SESSION_KEYS } from \"../lib/constants\";\nimport { auth } from \"@modelcontextprotocol/sdk/client/auth.js\";\nimport { useToast } from \"@/lib/hooks/useToast\";\nimport {\n  generateOAuthErrorDescription,\n  parseOAuthCallbackParams,\n} from \"@/utils/oauthUtils.ts\";\n\ninterface OAuthCallbackProps {\n  onConnect: (serverUrl: string) => void;\n}\n\nconst OAuthCallback = ({ onConnect }: OAuthCallbackProps) => {\n  const { toast } = useToast();\n  const hasProcessedRef = useRef(false);\n\n  useEffect(() => {\n    const handleCallback = async () => {\n      // Skip if we've already processed this callback\n      if (hasProcessedRef.current) {\n        return;\n      }\n      hasProcessedRef.current = true;\n\n      const notifyError = (description: string) =>\n        void toast({\n          title: \"OAuth Authorization Error\",\n          description,\n          variant: \"destructive\",\n        });\n\n      const params = parseOAuthCallbackParams(window.location.search);\n      if (!params.successful) {\n        return notifyError(generateOAuthErrorDescription(params));\n      }\n\n      const serverUrl = sessionStorage.getItem(SESSION_KEYS.SERVER_URL);\n      if (!serverUrl) {\n        return notifyError(\"Missing Server URL\");\n      }\n\n      let result;\n      try {\n        // Create an auth provider with the current server URL\n        const serverAuthProvider = new InspectorOAuthClientProvider(serverUrl);\n\n        result = await auth(serverAuthProvider, {\n          serverUrl,\n          authorizationCode: params.code,\n        });\n      } catch (error) {\n        console.error(\"OAuth callback error:\", error);\n        return notifyError(`Unexpected error occurred: ${error}`);\n      }\n\n      if (result !== \"AUTHORIZED\") {\n        return notifyError(\n          `Expected to be authorized after providing auth code, got: ${result}`,\n        );\n      }\n\n      // Finally, trigger auto-connect\n      toast({\n        title: \"Success\",\n        description: \"Successfully authenticated with OAuth\",\n        variant: \"default\",\n      });\n      onConnect(serverUrl);\n    };\n\n    handleCallback().finally(() => {\n      window.history.replaceState({}, document.title, \"/\");\n    });\n  }, [toast, onConnect]);\n\n  return (\n    <div className=\"flex items-center justify-center h-screen\">\n      <p className=\"text-lg text-gray-500\">Processing OAuth callback...</p>\n    </div>\n  );\n};\n\nexport default OAuthCallback;\n"
  },
  {
    "path": "client/src/components/OAuthDebugCallback.tsx",
    "content": "import { useEffect } from \"react\";\nimport { SESSION_KEYS } from \"../lib/constants\";\nimport {\n  generateOAuthErrorDescription,\n  parseOAuthCallbackParams,\n} from \"@/utils/oauthUtils.ts\";\nimport { AuthDebuggerState } from \"@/lib/auth-types\";\n\ninterface OAuthCallbackProps {\n  onConnect: ({\n    authorizationCode,\n    errorMsg,\n    restoredState,\n  }: {\n    authorizationCode?: string;\n    errorMsg?: string;\n    restoredState?: AuthDebuggerState;\n  }) => void;\n}\n\nconst OAuthDebugCallback = ({ onConnect }: OAuthCallbackProps) => {\n  useEffect(() => {\n    let isProcessed = false;\n\n    const handleCallback = async () => {\n      // Skip if we've already processed this callback\n      if (isProcessed) {\n        return;\n      }\n      isProcessed = true;\n\n      const params = parseOAuthCallbackParams(window.location.search);\n      if (!params.successful) {\n        const errorMsg = generateOAuthErrorDescription(params);\n        onConnect({ errorMsg });\n        return;\n      }\n\n      const serverUrl = sessionStorage.getItem(SESSION_KEYS.SERVER_URL);\n\n      // Try to restore the auth state\n      const storedState = sessionStorage.getItem(\n        SESSION_KEYS.AUTH_DEBUGGER_STATE,\n      );\n      let restoredState = null;\n      if (storedState) {\n        try {\n          restoredState = JSON.parse(storedState);\n          if (restoredState && typeof restoredState.resource === \"string\") {\n            restoredState.resource = new URL(restoredState.resource);\n          }\n          if (\n            restoredState &&\n            typeof restoredState.authorizationUrl === \"string\"\n          ) {\n            restoredState.authorizationUrl = new URL(\n              restoredState.authorizationUrl,\n            );\n          }\n          // Clean up the stored state\n          sessionStorage.removeItem(SESSION_KEYS.AUTH_DEBUGGER_STATE);\n        } catch (e) {\n          console.error(\"Failed to parse stored auth state:\", e);\n        }\n      }\n\n      // ServerURL isn't set, this can happen if we've opened the\n      // authentication request in a new tab, so we don't have the same\n      // session storage\n      if (!serverUrl) {\n        // If there's no server URL, we're likely in a new tab\n        // Just display the code for manual copying\n        return;\n      }\n\n      if (!params.code) {\n        onConnect({ errorMsg: \"Missing authorization code\" });\n        return;\n      }\n\n      // Instead of storing in sessionStorage, pass the code directly\n      // to the auth state manager through onConnect, along with restored state\n      onConnect({ authorizationCode: params.code, restoredState });\n    };\n\n    handleCallback().finally(() => {\n      // Only redirect if we have the URL set, otherwise assume this was\n      // in a new tab\n      if (sessionStorage.getItem(SESSION_KEYS.SERVER_URL)) {\n        window.history.replaceState({}, document.title, \"/\");\n      }\n    });\n\n    return () => {\n      isProcessed = true;\n    };\n  }, [onConnect]);\n\n  const callbackParams = parseOAuthCallbackParams(window.location.search);\n\n  return (\n    <div className=\"flex items-center justify-center h-screen\">\n      <div className=\"mt-4 p-4 bg-secondary rounded-md max-w-md\">\n        <p className=\"mb-2 text-sm\">\n          Please copy this authorization code and return to the Auth Debugger:\n        </p>\n        <code className=\"block p-2 bg-muted rounded-sm overflow-x-auto text-xs\">\n          {callbackParams.successful && \"code\" in callbackParams\n            ? callbackParams.code\n            : `No code found: ${callbackParams.error}, ${callbackParams.error_description}`}\n        </code>\n        <p className=\"mt-4 text-xs text-muted-foreground\">\n          Close this tab and paste the code in the OAuth flow to complete\n          authentication.\n        </p>\n      </div>\n    </div>\n  );\n};\n\nexport default OAuthDebugCallback;\n"
  },
  {
    "path": "client/src/components/OAuthFlowProgress.tsx",
    "content": "import { AuthDebuggerState, OAuthStep } from \"@/lib/auth-types\";\nimport { CheckCircle2, Circle, ExternalLink } from \"lucide-react\";\nimport { Button } from \"./ui/button\";\nimport { DebugInspectorOAuthClientProvider } from \"@/lib/auth\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { OAuthClientInformation } from \"@modelcontextprotocol/sdk/shared/auth.js\";\nimport { validateRedirectUrl } from \"@/utils/urlValidation\";\nimport { useToast } from \"@/lib/hooks/useToast\";\nimport { getAuthorizationServerMetadataDiscoveryUrl } from \"@/utils/oauthUtils\";\n\ninterface OAuthStepProps {\n  label: string;\n  isComplete: boolean;\n  isCurrent: boolean;\n  error?: Error | null;\n  children?: React.ReactNode;\n}\n\nconst OAuthStepDetails = ({\n  label,\n  isComplete,\n  isCurrent,\n  error,\n  children,\n}: OAuthStepProps) => {\n  return (\n    <div>\n      <div\n        className={`flex items-center p-2 rounded-md ${isCurrent ? \"bg-accent\" : \"\"}`}\n      >\n        {isComplete ? (\n          <CheckCircle2 className=\"h-5 w-5 text-green-500 mr-2\" />\n        ) : (\n          <Circle className=\"h-5 w-5 text-muted-foreground mr-2\" />\n        )}\n        <span className={`${isCurrent ? \"font-medium\" : \"\"}`}>{label}</span>\n      </div>\n\n      {/* Show children if current step or complete and children exist */}\n      {(isCurrent || isComplete) && children && (\n        <div className=\"ml-7 mt-1\">{children}</div>\n      )}\n\n      {/* Display error if current step and an error exists */}\n      {isCurrent && error && (\n        <div className=\"ml-7 mt-2 p-3 border border-red-300 bg-red-50 rounded-md\">\n          <p className=\"text-sm font-medium text-red-700\">Error:</p>\n          <p className=\"text-xs text-red-600 mt-1\">{error.message}</p>\n        </div>\n      )}\n    </div>\n  );\n};\n\ninterface OAuthFlowProgressProps {\n  serverUrl: string;\n  authState: AuthDebuggerState;\n  updateAuthState: (updates: Partial<AuthDebuggerState>) => void;\n  proceedToNextStep: () => Promise<void>;\n}\n\nconst steps: Array<OAuthStep> = [\n  \"metadata_discovery\",\n  \"client_registration\",\n  \"authorization_redirect\",\n  \"authorization_code\",\n  \"token_request\",\n  \"complete\",\n];\n\nexport const OAuthFlowProgress = ({\n  serverUrl,\n  authState,\n  updateAuthState,\n  proceedToNextStep,\n}: OAuthFlowProgressProps) => {\n  const { toast } = useToast();\n  const provider = useMemo(\n    () => new DebugInspectorOAuthClientProvider(serverUrl),\n    [serverUrl],\n  );\n  const [clientInfo, setClientInfo] = useState<OAuthClientInformation | null>(\n    null,\n  );\n  const authorizationServerMetadataDiscoveryUrl = useMemo(() => {\n    if (!authState.authServerUrl) {\n      return null;\n    }\n\n    return getAuthorizationServerMetadataDiscoveryUrl(authState.authServerUrl);\n  }, [authState.authServerUrl]);\n\n  const currentStepIdx = steps.findIndex((s) => s === authState.oauthStep);\n\n  useEffect(() => {\n    const fetchClientInfo = async () => {\n      if (authState.oauthClientInfo) {\n        setClientInfo(authState.oauthClientInfo);\n      } else {\n        try {\n          const info = await provider.clientInformation();\n          if (info) {\n            setClientInfo(info);\n          }\n        } catch (error) {\n          console.error(\"Failed to fetch client information:\", error);\n        }\n      }\n    };\n\n    if (currentStepIdx > steps.indexOf(\"client_registration\")) {\n      fetchClientInfo();\n    }\n  }, [\n    provider,\n    authState.oauthStep,\n    authState.oauthClientInfo,\n    currentStepIdx,\n  ]);\n\n  // Helper to get step props\n  const getStepProps = (stepName: OAuthStep) => ({\n    isComplete:\n      currentStepIdx > steps.indexOf(stepName) ||\n      currentStepIdx === steps.length - 1, // last step is \"complete\"\n    isCurrent: authState.oauthStep === stepName,\n    error: authState.oauthStep === stepName ? authState.latestError : null,\n  });\n\n  return (\n    <div className=\"rounded-md border p-6 space-y-4 mt-4\">\n      <h3 className=\"text-lg font-medium\">OAuth Flow Progress</h3>\n      <p className=\"text-sm text-muted-foreground\">\n        Follow these steps to complete OAuth authentication with the server.\n      </p>\n\n      <div className=\"space-y-3\">\n        <OAuthStepDetails\n          label=\"Metadata Discovery\"\n          {...getStepProps(\"metadata_discovery\")}\n        >\n          {authState.oauthMetadata && (\n            <details className=\"text-xs mt-2\">\n              <summary className=\"cursor-pointer text-muted-foreground font-medium\">\n                OAuth Metadata Sources\n                {!authState.resourceMetadata && \" ℹ️\"}\n              </summary>\n\n              {authState.resourceMetadata && (\n                <div className=\"mt-2\">\n                  <p className=\"font-medium\">Resource Metadata:</p>\n                  <p className=\"text-xs text-muted-foreground\">\n                    From{\" \"}\n                    {\n                      new URL(\n                        \"/.well-known/oauth-protected-resource\",\n                        serverUrl,\n                      ).href\n                    }\n                  </p>\n                  <pre className=\"mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]\">\n                    {JSON.stringify(authState.resourceMetadata, null, 2)}\n                  </pre>\n                </div>\n              )}\n\n              {authState.resourceMetadataError && (\n                <div className=\"mt-2 p-3 border border-blue-300 bg-blue-50 rounded-md\">\n                  <p className=\"text-sm font-medium text-blue-700\">\n                    ℹ️ Problem with resource metadata from{\" \"}\n                    <a\n                      href={\n                        new URL(\n                          \"/.well-known/oauth-protected-resource\",\n                          serverUrl,\n                        ).href\n                      }\n                      target=\"_blank\"\n                      rel=\"noopener noreferrer\"\n                      className=\"text-blue-500 hover:text-blue-700\"\n                    >\n                      {\n                        new URL(\n                          \"/.well-known/oauth-protected-resource\",\n                          serverUrl,\n                        ).href\n                      }\n                    </a>\n                  </p>\n                  <p className=\"text-xs text-blue-600 mt-1\">\n                    Resource metadata was added in the{\" \"}\n                    <a href=\"https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#authorization-server-location\">\n                      2025-06-18 specification update\n                    </a>\n                    <br />\n                    {authState.resourceMetadataError.message}\n                    {authState.resourceMetadataError instanceof TypeError &&\n                      \" (This could indicate the endpoint doesn't exist or does not have CORS configured)\"}\n                  </p>\n                </div>\n              )}\n\n              {authState.oauthMetadata && (\n                <div className=\"mt-2\">\n                  <p className=\"font-medium\">Authorization Server Metadata:</p>\n                  {authState.authServerUrl && (\n                    <p className=\"text-xs text-muted-foreground\">\n                      From {authorizationServerMetadataDiscoveryUrl}\n                    </p>\n                  )}\n                  <pre className=\"mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]\">\n                    {JSON.stringify(authState.oauthMetadata, null, 2)}\n                  </pre>\n                </div>\n              )}\n            </details>\n          )}\n        </OAuthStepDetails>\n\n        <OAuthStepDetails\n          label=\"Client Registration\"\n          {...getStepProps(\"client_registration\")}\n        >\n          {clientInfo && (\n            <details className=\"text-xs mt-2\">\n              <summary className=\"cursor-pointer text-muted-foreground font-medium\">\n                Registered Client Information\n              </summary>\n              <pre className=\"mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]\">\n                {JSON.stringify(clientInfo, null, 2)}\n              </pre>\n            </details>\n          )}\n        </OAuthStepDetails>\n\n        <OAuthStepDetails\n          label=\"Preparing Authorization\"\n          {...getStepProps(\"authorization_redirect\")}\n        >\n          {authState.authorizationUrl && (\n            <div className=\"mt-2 p-3 border rounded-md bg-muted\">\n              <p className=\"font-medium mb-2 text-sm\">Authorization URL:</p>\n              <div className=\"flex items-center gap-2\">\n                <p className=\"text-xs break-all\">\n                  {String(authState.authorizationUrl)}\n                </p>\n                <button\n                  onClick={() => {\n                    try {\n                      validateRedirectUrl(authState.authorizationUrl!);\n                      window.open(\n                        authState.authorizationUrl!,\n                        \"_blank\",\n                        \"noopener noreferrer\",\n                      );\n                    } catch (error) {\n                      toast({\n                        title: \"Invalid URL\",\n                        description:\n                          error instanceof Error\n                            ? error.message\n                            : \"The authorization URL is not valid\",\n                        variant: \"destructive\",\n                      });\n                    }\n                  }}\n                  className=\"flex items-center text-blue-500 hover:text-blue-700\"\n                  aria-label=\"Open authorization URL in new tab\"\n                  title=\"Open authorization URL\"\n                >\n                  <ExternalLink className=\"h-4 w-4\" />\n                </button>\n              </div>\n              <p className=\"text-xs text-muted-foreground mt-2\">\n                Click the link to authorize in your browser. After\n                authorization, you'll be redirected back to continue the flow.\n              </p>\n            </div>\n          )}\n        </OAuthStepDetails>\n\n        <OAuthStepDetails\n          label=\"Request Authorization and acquire authorization code\"\n          {...getStepProps(\"authorization_code\")}\n        >\n          <div className=\"mt-3\">\n            <label\n              htmlFor=\"authCode\"\n              className=\"block text-sm font-medium mb-1\"\n            >\n              Authorization Code\n            </label>\n            <div className=\"flex gap-2\">\n              <input\n                id=\"authCode\"\n                value={authState.authorizationCode}\n                onChange={(e) => {\n                  updateAuthState({\n                    authorizationCode: e.target.value,\n                    validationError: null,\n                  });\n                }}\n                placeholder=\"Enter the code from the authorization server\"\n                className={`flex h-9 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${\n                  authState.validationError ? \"border-red-500\" : \"border-input\"\n                }`}\n              />\n            </div>\n            {authState.validationError && (\n              <p className=\"text-xs text-red-600 mt-1\">\n                {authState.validationError}\n              </p>\n            )}\n            <p className=\"text-xs text-muted-foreground mt-1\">\n              Once you've completed authorization in the link, paste the code\n              here.\n            </p>\n          </div>\n        </OAuthStepDetails>\n\n        <OAuthStepDetails\n          label=\"Token Request\"\n          {...getStepProps(\"token_request\")}\n        >\n          {authState.oauthMetadata && (\n            <details className=\"text-xs mt-2\">\n              <summary className=\"cursor-pointer text-muted-foreground font-medium\">\n                Token Request Details\n              </summary>\n              <div className=\"mt-2 p-2 bg-muted rounded-md\">\n                <p className=\"font-medium\">Token Endpoint:</p>\n                <code className=\"block mt-1 text-xs overflow-x-auto\">\n                  {authState.oauthMetadata.token_endpoint}\n                </code>\n              </div>\n            </details>\n          )}\n        </OAuthStepDetails>\n\n        <OAuthStepDetails\n          label=\"Authentication Complete\"\n          {...getStepProps(\"complete\")}\n        >\n          {authState.oauthTokens && (\n            <details className=\"text-xs mt-2\">\n              <summary className=\"cursor-pointer text-muted-foreground font-medium\">\n                Access Tokens\n              </summary>\n              <p className=\"mt-1 text-sm\">\n                Authentication successful! You can now use the authenticated\n                connection. These tokens will be used automatically for server\n                requests.\n              </p>\n              <pre className=\"mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]\">\n                {JSON.stringify(authState.oauthTokens, null, 2)}\n              </pre>\n            </details>\n          )}\n        </OAuthStepDetails>\n      </div>\n\n      <div className=\"flex gap-3 mt-4\">\n        {authState.oauthStep !== \"complete\" && (\n          <>\n            <Button\n              onClick={proceedToNextStep}\n              disabled={authState.isInitiatingAuth}\n            >\n              {authState.isInitiatingAuth ? \"Processing...\" : \"Continue\"}\n            </Button>\n          </>\n        )}\n\n        {authState.oauthStep === \"authorization_redirect\" &&\n          authState.authorizationUrl && (\n            <Button\n              variant=\"outline\"\n              onClick={() => {\n                try {\n                  validateRedirectUrl(authState.authorizationUrl!);\n                  window.open(authState.authorizationUrl!, \"_blank\");\n                } catch (error) {\n                  toast({\n                    title: \"Invalid URL\",\n                    description:\n                      error instanceof Error\n                        ? error.message\n                        : \"The authorization URL is not valid\",\n                    variant: \"destructive\",\n                  });\n                }\n              }}\n            >\n              Open in New Tab\n            </Button>\n          )}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/components/PingTab.tsx",
    "content": "import { TabsContent } from \"@/components/ui/tabs\";\nimport { Button } from \"@/components/ui/button\";\n\nconst PingTab = ({ onPingClick }: { onPingClick: () => void }) => {\n  return (\n    <TabsContent value=\"ping\">\n      <div className=\"grid grid-cols-2 gap-4\">\n        <div className=\"col-span-2 flex justify-center items-center\">\n          <Button\n            onClick={onPingClick}\n            className=\"font-bold py-6 px-12 rounded-full\"\n          >\n            Ping Server\n          </Button>\n        </div>\n      </div>\n    </TabsContent>\n  );\n};\n\nexport default PingTab;\n"
  },
  {
    "path": "client/src/components/PromptsTab.tsx",
    "content": "import { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\nimport { Button } from \"@/components/ui/button\";\nimport { Combobox } from \"@/components/ui/combobox\";\nimport { Label } from \"@/components/ui/label\";\nimport { TabsContent } from \"@/components/ui/tabs\";\n\nimport {\n  ListPromptsResult,\n  PromptReference,\n  ResourceReference,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { AlertCircle, ChevronRight } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport ListPane from \"./ListPane\";\nimport { useCompletionState } from \"@/lib/hooks/useCompletionState\";\nimport JsonView from \"./JsonView\";\nimport IconDisplay, { WithIcons } from \"./IconDisplay\";\n\nexport type Prompt = {\n  name: string;\n  description?: string;\n  arguments?: {\n    name: string;\n    description?: string;\n    required?: boolean;\n  }[];\n  icons?: {\n    src: string;\n    mimeType?: string;\n    sizes?: string[];\n    theme?: \"light\" | \"dark\";\n  }[];\n};\n\nconst PromptsTab = ({\n  prompts,\n  listPrompts,\n  clearPrompts,\n  getPrompt,\n  selectedPrompt,\n  setSelectedPrompt,\n  handleCompletion,\n  completionsSupported,\n  promptContent,\n  nextCursor,\n  error,\n}: {\n  prompts: Prompt[];\n  listPrompts: () => void;\n  clearPrompts: () => void;\n  getPrompt: (name: string, args: Record<string, string>) => void;\n  selectedPrompt: Prompt | null;\n  setSelectedPrompt: (prompt: Prompt | null) => void;\n  handleCompletion: (\n    ref: PromptReference | ResourceReference,\n    argName: string,\n    value: string,\n    context?: Record<string, string>,\n  ) => Promise<string[]>;\n  completionsSupported: boolean;\n  promptContent: string;\n  nextCursor: ListPromptsResult[\"nextCursor\"];\n  error: string | null;\n}) => {\n  const [promptArgs, setPromptArgs] = useState<Record<string, string>>({});\n  const { completions, clearCompletions, requestCompletions } =\n    useCompletionState(handleCompletion, completionsSupported);\n\n  useEffect(() => {\n    clearCompletions();\n  }, [clearCompletions, selectedPrompt]);\n\n  const triggerCompletions = (argName: string, value: string) => {\n    if (selectedPrompt) {\n      requestCompletions(\n        {\n          type: \"ref/prompt\",\n          name: selectedPrompt.name,\n        },\n        argName,\n        value,\n        promptArgs,\n      );\n    }\n  };\n\n  const handleInputChange = async (argName: string, value: string) => {\n    setPromptArgs((prev) => ({ ...prev, [argName]: value }));\n    triggerCompletions(argName, value);\n  };\n\n  const handleFocus = async (argName: string) => {\n    const currentValue = promptArgs[argName] || \"\";\n    triggerCompletions(argName, currentValue);\n  };\n\n  const handleGetPrompt = () => {\n    if (selectedPrompt) {\n      getPrompt(selectedPrompt.name, promptArgs);\n    }\n  };\n\n  return (\n    <TabsContent value=\"prompts\">\n      <div className=\"grid grid-cols-2 gap-4\">\n        <ListPane\n          items={prompts}\n          listItems={listPrompts}\n          clearItems={() => {\n            clearPrompts();\n            setSelectedPrompt(null);\n          }}\n          setSelectedItem={(prompt) => {\n            setSelectedPrompt(prompt);\n            setPromptArgs({});\n          }}\n          renderItem={(prompt) => (\n            <div className=\"flex items-start w-full gap-2\">\n              <div className=\"flex-shrink-0 mt-1\">\n                <IconDisplay icons={prompt.icons} size=\"sm\" />\n              </div>\n              <div className=\"flex flex-col flex-1 min-w-0\">\n                <span className=\"truncate\">{prompt.name}</span>\n                <span className=\"text-sm text-gray-500 text-left line-clamp-2\">\n                  {prompt.description}\n                </span>\n              </div>\n              <ChevronRight className=\"w-4 h-4 flex-shrink-0 text-gray-400 mt-1\" />\n            </div>\n          )}\n          title=\"Prompts\"\n          buttonText={nextCursor ? \"List More Prompts\" : \"List Prompts\"}\n          isButtonDisabled={!nextCursor && prompts.length > 0}\n        />\n\n        <div className=\"bg-card border border-border rounded-lg shadow\">\n          <div className=\"p-4 border-b border-gray-200 dark:border-border\">\n            <div className=\"flex items-center gap-2\">\n              {selectedPrompt && (\n                <IconDisplay\n                  icons={(selectedPrompt as WithIcons).icons}\n                  size=\"md\"\n                />\n              )}\n              <h3 className=\"font-semibold\">\n                {selectedPrompt ? selectedPrompt.name : \"Select a prompt\"}\n              </h3>\n            </div>\n          </div>\n          <div className=\"p-4\">\n            {error ? (\n              <Alert variant=\"destructive\">\n                <AlertCircle className=\"h-4 w-4\" />\n                <AlertTitle>Error</AlertTitle>\n                <AlertDescription className=\"break-all\">\n                  {error}\n                </AlertDescription>\n              </Alert>\n            ) : selectedPrompt ? (\n              <div className=\"space-y-4\">\n                {selectedPrompt.description && (\n                  <p className=\"text-sm text-gray-600 dark:text-gray-400\">\n                    {selectedPrompt.description}\n                  </p>\n                )}\n                {selectedPrompt.arguments?.map((arg) => (\n                  <div key={arg.name}>\n                    <Label htmlFor={arg.name}>{arg.name}</Label>\n                    <Combobox\n                      id={arg.name}\n                      placeholder={`Enter ${arg.name}`}\n                      value={promptArgs[arg.name] || \"\"}\n                      onChange={(value) => handleInputChange(arg.name, value)}\n                      onInputChange={(value) =>\n                        handleInputChange(arg.name, value)\n                      }\n                      onFocus={() => handleFocus(arg.name)}\n                      options={completions[arg.name] || []}\n                    />\n\n                    {arg.description && (\n                      <p className=\"text-xs text-gray-500 mt-1\">\n                        {arg.description}\n                        {arg.required && (\n                          <span className=\"text-xs mt-1 ml-1\">(Required)</span>\n                        )}\n                      </p>\n                    )}\n                  </div>\n                ))}\n                <Button onClick={handleGetPrompt} className=\"w-full\">\n                  Get Prompt\n                </Button>\n                {promptContent && (\n                  <JsonView data={promptContent} withCopyButton={false} />\n                )}\n              </div>\n            ) : (\n              <Alert>\n                <AlertDescription>\n                  Select a prompt from the list to view and use it\n                </AlertDescription>\n              </Alert>\n            )}\n          </div>\n        </div>\n      </div>\n    </TabsContent>\n  );\n};\n\nexport default PromptsTab;\n"
  },
  {
    "path": "client/src/components/ResourceLinkView.tsx",
    "content": "import { useState, useCallback, useMemo, memo } from \"react\";\nimport JsonView from \"./JsonView\";\n\ninterface ResourceLinkViewProps {\n  uri: string;\n  name?: string;\n  description?: string;\n  mimeType?: string;\n  resourceContent: string;\n  onReadResource?: (uri: string) => void;\n}\n\nconst ResourceLinkView = memo(\n  ({\n    uri,\n    name,\n    description,\n    mimeType,\n    resourceContent,\n    onReadResource,\n  }: ResourceLinkViewProps) => {\n    const [{ expanded, loading }, setState] = useState({\n      expanded: false,\n      loading: false,\n    });\n\n    const expandedContent = useMemo(\n      () =>\n        expanded && resourceContent ? (\n          <div className=\"mt-2\">\n            <div className=\"flex justify-between items-center mb-1\">\n              <span className=\"font-semibold text-green-600\">Resource:</span>\n            </div>\n            <JsonView data={resourceContent} className=\"bg-background\" />\n          </div>\n        ) : null,\n      [expanded, resourceContent],\n    );\n\n    const handleClick = useCallback(() => {\n      if (!onReadResource) return;\n      if (!expanded) {\n        setState((prev) => ({ ...prev, expanded: true, loading: true }));\n        onReadResource(uri);\n        setState((prev) => ({ ...prev, loading: false }));\n      } else {\n        setState((prev) => ({ ...prev, expanded: false }));\n      }\n    }, [expanded, onReadResource, uri]);\n\n    const handleKeyDown = useCallback(\n      (e: React.KeyboardEvent) => {\n        if ((e.key === \"Enter\" || e.key === \" \") && onReadResource) {\n          e.preventDefault();\n          handleClick();\n        }\n      },\n      [handleClick, onReadResource],\n    );\n\n    return (\n      <div className=\"text-sm text-foreground bg-secondary py-2 px-3 rounded\">\n        <div\n          className=\"flex justify-between items-center cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1 rounded\"\n          onClick={onReadResource ? handleClick : undefined}\n          onKeyDown={onReadResource ? handleKeyDown : undefined}\n          tabIndex={onReadResource ? 0 : -1}\n          role=\"button\"\n          aria-expanded={expanded}\n          aria-label={`${expanded ? \"Collapse\" : \"Expand\"} resource ${uri}`}\n        >\n          <div className=\"flex-1 min-w-0\">\n            <div className=\"flex items-start justify-between gap-2 mb-1\">\n              <span className=\"text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline px-1 py-0.5 break-all font-mono flex-1 min-w-0\">\n                {uri}\n              </span>\n              <div className=\"flex items-center gap-2 flex-shrink-0\">\n                {mimeType && (\n                  <span className=\"inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200\">\n                    {mimeType}\n                  </span>\n                )}\n                {onReadResource && (\n                  <span className=\"ml-2 flex-shrink-0\" aria-hidden=\"true\">\n                    {loading ? (\n                      <div className=\"w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin\" />\n                    ) : (\n                      <span>{expanded ? \"▼\" : \"▶\"}</span>\n                    )}\n                  </span>\n                )}\n              </div>\n            </div>\n            {name && (\n              <div className=\"font-semibold text-sm text-gray-900 dark:text-gray-100 mb-1\">\n                {name}\n              </div>\n            )}\n            {description && (\n              <p className=\"text-sm text-gray-600 dark:text-gray-300 leading-relaxed\">\n                {description}\n              </p>\n            )}\n          </div>\n        </div>\n        {expandedContent}\n      </div>\n    );\n  },\n);\n\nexport default ResourceLinkView;\n"
  },
  {
    "path": "client/src/components/ResourcesTab.tsx",
    "content": "import { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\nimport { Button } from \"@/components/ui/button\";\nimport { Label } from \"@/components/ui/label\";\nimport { Combobox } from \"@/components/ui/combobox\";\nimport { TabsContent } from \"@/components/ui/tabs\";\nimport {\n  ListResourcesResult,\n  Resource,\n  ResourceTemplate,\n  ListResourceTemplatesResult,\n  ResourceReference,\n  PromptReference,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { AlertCircle, ChevronRight, FileText, RefreshCw } from \"lucide-react\";\nimport ListPane from \"./ListPane\";\nimport { useEffect, useState } from \"react\";\nimport { useCompletionState } from \"@/lib/hooks/useCompletionState\";\nimport JsonView from \"./JsonView\";\nimport { UriTemplate } from \"@modelcontextprotocol/sdk/shared/uriTemplate.js\";\nimport IconDisplay, { WithIcons } from \"./IconDisplay\";\n\nconst ResourcesTab = ({\n  resources,\n  resourceTemplates,\n  listResources,\n  clearResources,\n  listResourceTemplates,\n  clearResourceTemplates,\n  readResource,\n  selectedResource,\n  setSelectedResource,\n  resourceSubscriptionsSupported,\n  resourceSubscriptions,\n  subscribeToResource,\n  unsubscribeFromResource,\n  handleCompletion,\n  completionsSupported,\n  resourceContent,\n  nextCursor,\n  nextTemplateCursor,\n  error,\n}: {\n  resources: Resource[];\n  resourceTemplates: ResourceTemplate[];\n  listResources: () => void;\n  clearResources: () => void;\n  listResourceTemplates: () => void;\n  clearResourceTemplates: () => void;\n  readResource: (uri: string) => void;\n  selectedResource: Resource | null;\n  setSelectedResource: (resource: Resource | null) => void;\n  handleCompletion: (\n    ref: ResourceReference | PromptReference,\n    argName: string,\n    value: string,\n    context?: Record<string, string>,\n  ) => Promise<string[]>;\n  completionsSupported: boolean;\n  resourceContent: string;\n  nextCursor: ListResourcesResult[\"nextCursor\"];\n  nextTemplateCursor: ListResourceTemplatesResult[\"nextCursor\"];\n  error: string | null;\n  resourceSubscriptionsSupported: boolean;\n  resourceSubscriptions: Set<string>;\n  subscribeToResource: (uri: string) => void;\n  unsubscribeFromResource: (uri: string) => void;\n}) => {\n  const [selectedTemplate, setSelectedTemplate] =\n    useState<ResourceTemplate | null>(null);\n  const [templateValues, setTemplateValues] = useState<Record<string, string>>(\n    {},\n  );\n\n  const { completions, clearCompletions, requestCompletions } =\n    useCompletionState(handleCompletion, completionsSupported);\n\n  useEffect(() => {\n    clearCompletions();\n  }, [clearCompletions]);\n\n  const fillTemplate = (\n    template: string,\n    values: Record<string, string>,\n  ): string => {\n    return new UriTemplate(template).expand(values);\n  };\n\n  const handleTemplateValueChange = async (key: string, value: string) => {\n    setTemplateValues((prev) => ({ ...prev, [key]: value }));\n\n    if (selectedTemplate?.uriTemplate) {\n      requestCompletions(\n        {\n          type: \"ref/resource\",\n          uri: selectedTemplate.uriTemplate,\n        },\n        key,\n        value,\n        templateValues,\n      );\n    }\n  };\n\n  const handleReadTemplateResource = () => {\n    if (selectedTemplate) {\n      const uri = fillTemplate(selectedTemplate.uriTemplate, templateValues);\n      readResource(uri);\n      // We don't have the full Resource object here, so we create a partial one\n      setSelectedResource({ uri, name: uri } as Resource);\n    }\n  };\n\n  return (\n    <TabsContent value=\"resources\">\n      <div className=\"grid grid-cols-3 gap-4\">\n        <ListPane\n          items={resources}\n          listItems={listResources}\n          clearItems={() => {\n            clearResources();\n            // Condition to check if selected resource is not resource template's resource\n            if (!selectedTemplate) {\n              setSelectedResource(null);\n            }\n          }}\n          setSelectedItem={(resource) => {\n            setSelectedResource(resource);\n            readResource(resource.uri);\n            setSelectedTemplate(null);\n          }}\n          renderItem={(resource) => (\n            <div className=\"flex items-center w-full\">\n              <IconDisplay icons={(resource as WithIcons).icons} size=\"sm\" />\n              {!(resource as WithIcons).icons && (\n                <FileText className=\"w-4 h-4 mr-2 flex-shrink-0 text-gray-500\" />\n              )}\n              <span className=\"flex-1 truncate\" title={resource.uri.toString()}>\n                {resource.name}\n              </span>\n              <ChevronRight className=\"w-4 h-4 flex-shrink-0 text-gray-400\" />\n            </div>\n          )}\n          title=\"Resources\"\n          buttonText={nextCursor ? \"List More Resources\" : \"List Resources\"}\n          isButtonDisabled={!nextCursor && resources.length > 0}\n        />\n\n        <ListPane\n          items={resourceTemplates}\n          listItems={listResourceTemplates}\n          clearItems={() => {\n            clearResourceTemplates();\n            // Condition to check if selected resource is resource template's resource\n            if (selectedTemplate) {\n              setSelectedResource(null);\n            }\n            setSelectedTemplate(null);\n          }}\n          setSelectedItem={(template) => {\n            setSelectedTemplate(template);\n            setSelectedResource(null);\n            setTemplateValues({});\n          }}\n          renderItem={(template) => (\n            <div className=\"flex items-center w-full\">\n              <IconDisplay icons={(template as WithIcons).icons} size=\"sm\" />\n              {!(template as WithIcons).icons && (\n                <FileText className=\"w-4 h-4 mr-2 flex-shrink-0 text-gray-500\" />\n              )}\n              <span className=\"flex-1 truncate\" title={template.uriTemplate}>\n                {template.name}\n              </span>\n              <ChevronRight className=\"w-4 h-4 flex-shrink-0 text-gray-400\" />\n            </div>\n          )}\n          title=\"Resource Templates\"\n          buttonText={\n            nextTemplateCursor ? \"List More Templates\" : \"List Templates\"\n          }\n          isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0}\n        />\n\n        <div className=\"bg-card border border-border rounded-lg shadow\">\n          <div className=\"p-4 border-b border-gray-200 dark:border-border flex justify-between items-center\">\n            <div className=\"flex items-center gap-2 truncate\">\n              {(selectedResource || selectedTemplate) && (\n                <IconDisplay\n                  icons={\n                    ((selectedResource || selectedTemplate) as WithIcons).icons\n                  }\n                  size=\"md\"\n                />\n              )}\n              <h3\n                className=\"font-semibold truncate\"\n                title={selectedResource?.name || selectedTemplate?.name}\n              >\n                {selectedResource\n                  ? selectedResource.name\n                  : selectedTemplate\n                    ? selectedTemplate.name\n                    : \"Select a resource or template\"}\n              </h3>\n            </div>\n            {selectedResource && (\n              <div className=\"flex row-auto gap-1 justify-end w-2/5\">\n                {resourceSubscriptionsSupported &&\n                  !resourceSubscriptions.has(selectedResource.uri) && (\n                    <Button\n                      variant=\"outline\"\n                      size=\"sm\"\n                      onClick={() => subscribeToResource(selectedResource.uri)}\n                    >\n                      Subscribe\n                    </Button>\n                  )}\n                {resourceSubscriptionsSupported &&\n                  resourceSubscriptions.has(selectedResource.uri) && (\n                    <Button\n                      variant=\"outline\"\n                      size=\"sm\"\n                      onClick={() =>\n                        unsubscribeFromResource(selectedResource.uri)\n                      }\n                    >\n                      Unsubscribe\n                    </Button>\n                  )}\n                <Button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  onClick={() => readResource(selectedResource.uri)}\n                >\n                  <RefreshCw className=\"w-4 h-4 mr-2\" />\n                  Refresh\n                </Button>\n              </div>\n            )}\n          </div>\n          <div className=\"p-4\">\n            {error ? (\n              <Alert variant=\"destructive\">\n                <AlertCircle className=\"h-4 w-4\" />\n                <AlertTitle>Error</AlertTitle>\n                <AlertDescription className=\"break-all\">\n                  {error}\n                </AlertDescription>\n              </Alert>\n            ) : selectedResource ? (\n              <JsonView\n                data={resourceContent}\n                className=\"bg-gray-50 dark:bg-gray-800 p-4 rounded text-sm overflow-auto max-h-96 text-gray-900 dark:text-gray-100\"\n              />\n            ) : selectedTemplate ? (\n              <div className=\"space-y-4\">\n                <p className=\"text-sm text-gray-600 dark:text-gray-400\">\n                  {selectedTemplate.description}\n                </p>\n                {new UriTemplate(\n                  selectedTemplate.uriTemplate,\n                ).variableNames?.map((key) => {\n                  return (\n                    <div key={key}>\n                      <Label htmlFor={key}>{key}</Label>\n                      <Combobox\n                        id={key}\n                        placeholder={`Enter ${key}`}\n                        value={templateValues[key] || \"\"}\n                        onChange={(value) =>\n                          handleTemplateValueChange(key, value)\n                        }\n                        onInputChange={(value) =>\n                          handleTemplateValueChange(key, value)\n                        }\n                        options={completions[key] || []}\n                      />\n                    </div>\n                  );\n                })}\n                <Button\n                  onClick={handleReadTemplateResource}\n                  disabled={Object.keys(templateValues).length === 0}\n                >\n                  Read Resource\n                </Button>\n              </div>\n            ) : (\n              <Alert>\n                <AlertDescription>\n                  Select a resource or template from the list to view its\n                  contents\n                </AlertDescription>\n              </Alert>\n            )}\n          </div>\n        </div>\n      </div>\n    </TabsContent>\n  );\n};\n\nexport default ResourcesTab;\n"
  },
  {
    "path": "client/src/components/RootsTab.tsx",
    "content": "import { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { TabsContent } from \"@/components/ui/tabs\";\nimport { Root } from \"@modelcontextprotocol/sdk/types.js\";\nimport { Plus, Minus, Save } from \"lucide-react\";\n\nconst RootsTab = ({\n  roots,\n  setRoots,\n  onRootsChange,\n}: {\n  roots: Root[];\n  setRoots: React.Dispatch<React.SetStateAction<Root[]>>;\n  onRootsChange: () => void;\n}) => {\n  const addRoot = () => {\n    setRoots((currentRoots) => [...currentRoots, { uri: \"file://\", name: \"\" }]);\n  };\n\n  const removeRoot = (index: number) => {\n    setRoots((currentRoots) => currentRoots.filter((_, i) => i !== index));\n  };\n\n  const updateRoot = (index: number, field: keyof Root, value: string) => {\n    setRoots((currentRoots) =>\n      currentRoots.map((root, i) =>\n        i === index ? { ...root, [field]: value } : root,\n      ),\n    );\n  };\n\n  const handleSave = () => {\n    onRootsChange();\n  };\n\n  return (\n    <TabsContent value=\"roots\">\n      <div className=\"space-y-4\">\n        <Alert>\n          <AlertDescription>\n            Configure the root directories that the server can access\n          </AlertDescription>\n        </Alert>\n\n        {roots.map((root, index) => (\n          <div key={index} className=\"flex gap-2 items-center\">\n            <Input\n              placeholder=\"file:// URI\"\n              value={root.uri}\n              onChange={(e) => updateRoot(index, \"uri\", e.target.value)}\n              className=\"flex-1\"\n            />\n            <Button\n              variant=\"destructive\"\n              size=\"sm\"\n              onClick={() => removeRoot(index)}\n            >\n              <Minus className=\"h-4 w-4\" />\n            </Button>\n          </div>\n        ))}\n\n        <div className=\"flex gap-2\">\n          <Button variant=\"outline\" onClick={addRoot}>\n            <Plus className=\"h-4 w-4 mr-2\" />\n            Add Root\n          </Button>\n          <Button onClick={handleSave}>\n            <Save className=\"h-4 w-4 mr-2\" />\n            Save Changes\n          </Button>\n        </div>\n      </div>\n    </TabsContent>\n  );\n};\n\nexport default RootsTab;\n"
  },
  {
    "path": "client/src/components/SamplingRequest.tsx",
    "content": "import { Button } from \"@/components/ui/button\";\nimport JsonView from \"./JsonView\";\nimport { useMemo, useState } from \"react\";\nimport {\n  CreateMessageResult,\n  CreateMessageResultSchema,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { PendingRequest } from \"./SamplingTab\";\nimport DynamicJsonForm from \"./DynamicJsonForm\";\nimport { useToast } from \"@/lib/hooks/useToast\";\nimport { JsonSchemaType, JsonValue } from \"@/utils/jsonUtils\";\n\nexport type SamplingRequestProps = {\n  request: PendingRequest;\n  onApprove: (id: number, result: CreateMessageResult) => void;\n  onReject: (id: number) => void;\n};\n\nconst SamplingRequest = ({\n  onApprove,\n  request,\n  onReject,\n}: SamplingRequestProps) => {\n  const { toast } = useToast();\n\n  const [messageResult, setMessageResult] = useState<JsonValue>({\n    model: \"stub-model\",\n    stopReason: \"endTurn\",\n    role: \"assistant\",\n    content: {\n      type: \"text\",\n      text: \"\",\n    },\n  });\n\n  const contentType = (\n    (messageResult as { [key: string]: JsonValue })?.content as {\n      [key: string]: JsonValue;\n    }\n  )?.type;\n\n  const schema = useMemo(() => {\n    const s: JsonSchemaType = {\n      type: \"object\",\n      description: \"Message result\",\n      properties: {\n        model: {\n          type: \"string\",\n          default: \"stub-model\",\n          description: \"model name\",\n        },\n        stopReason: {\n          type: \"string\",\n          default: \"endTurn\",\n          description: \"Stop reason\",\n        },\n        role: {\n          type: \"string\",\n          default: \"assistant\",\n          description: \"Role of the model\",\n        },\n        content: {\n          type: \"object\",\n          properties: {\n            type: {\n              type: \"string\",\n              default: \"text\",\n              description: \"Type of content\",\n            },\n          },\n        },\n      },\n    };\n\n    if (contentType === \"text\" && s.properties) {\n      s.properties.content.properties = {\n        ...s.properties.content.properties,\n        text: {\n          type: \"string\",\n          default: \"\",\n          description: \"text content\",\n        },\n      };\n      setMessageResult((prev) => ({\n        ...(prev as { [key: string]: JsonValue }),\n        content: {\n          type: contentType,\n          text: \"\",\n        },\n      }));\n    } else if (contentType === \"image\" && s.properties) {\n      s.properties.content.properties = {\n        ...s.properties.content.properties,\n        data: {\n          type: \"string\",\n          default: \"\",\n          description: \"Base64 encoded image data\",\n        },\n        mimeType: {\n          type: \"string\",\n          default: \"\",\n          description: \"Mime type of the image\",\n        },\n      };\n      setMessageResult((prev) => ({\n        ...(prev as { [key: string]: JsonValue }),\n        content: {\n          type: contentType,\n          data: \"\",\n          mimeType: \"\",\n        },\n      }));\n    }\n\n    return s;\n  }, [contentType]);\n\n  const handleApprove = (id: number) => {\n    const validationResult = CreateMessageResultSchema.safeParse(messageResult);\n    if (!validationResult.success) {\n      toast({\n        title: \"Error\",\n        description: `There was an error validating the message result: ${validationResult.error.message}`,\n        variant: \"destructive\",\n      });\n      return;\n    }\n\n    onApprove(id, validationResult.data);\n  };\n\n  return (\n    <div\n      data-testid=\"sampling-request\"\n      className=\"flex gap-4 p-4 border rounded-lg space-y-4\"\n    >\n      <div className=\"flex-1 bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-2 rounded\">\n        <JsonView data={JSON.stringify(request.request)} />\n      </div>\n      <form className=\"flex-1 space-y-4\">\n        <div className=\"space-y-2\">\n          <DynamicJsonForm\n            schema={schema}\n            value={messageResult}\n            onChange={(newValue: JsonValue) => {\n              setMessageResult(newValue);\n            }}\n          />\n        </div>\n        <div className=\"flex space-x-2 mt-1\">\n          <Button type=\"button\" onClick={() => handleApprove(request.id)}>\n            Approve\n          </Button>\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            onClick={() => onReject(request.id)}\n          >\n            Reject\n          </Button>\n        </div>\n      </form>\n    </div>\n  );\n};\n\nexport default SamplingRequest;\n"
  },
  {
    "path": "client/src/components/SamplingTab.tsx",
    "content": "import { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport { TabsContent } from \"@/components/ui/tabs\";\nimport {\n  CreateMessageRequest,\n  CreateMessageResult,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport SamplingRequest from \"./SamplingRequest\";\n\nexport type PendingRequest = {\n  id: number;\n  request: CreateMessageRequest;\n  originatingTab?: string;\n};\n\nexport type Props = {\n  pendingRequests: PendingRequest[];\n  onApprove: (id: number, result: CreateMessageResult) => void;\n  onReject: (id: number) => void;\n};\n\nconst SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => {\n  return (\n    <TabsContent value=\"sampling\">\n      <div className=\"h-96\">\n        <Alert>\n          <AlertDescription>\n            When the server requests LLM sampling, requests will appear here for\n            approval.\n          </AlertDescription>\n        </Alert>\n        <div className=\"mt-4 space-y-4\">\n          <h3 className=\"text-lg font-semibold\">Recent Requests</h3>\n          {pendingRequests.map((request) => (\n            <SamplingRequest\n              key={request.id}\n              request={request}\n              onApprove={onApprove}\n              onReject={onReject}\n            />\n          ))}\n          {pendingRequests.length === 0 && (\n            <p className=\"text-gray-500\">No pending requests</p>\n          )}\n        </div>\n      </div>\n    </TabsContent>\n  );\n};\n\nexport default SamplingTab;\n"
  },
  {
    "path": "client/src/components/Sidebar.tsx",
    "content": "import { useState, useCallback } from \"react\";\nimport {\n  Play,\n  ChevronDown,\n  ChevronRight,\n  CircleHelp,\n  Bug,\n  Github,\n  Eye,\n  EyeOff,\n  RotateCcw,\n  Settings,\n  HelpCircle,\n  RefreshCwOff,\n  Copy,\n  CheckCheck,\n  Server,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport {\n  LoggingLevel,\n  LoggingLevelSchema,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { InspectorConfig } from \"@/lib/configurationTypes\";\nimport { ConnectionStatus } from \"@/lib/constants\";\nimport useTheme from \"../lib/hooks/useTheme\";\nimport { version } from \"../../../package.json\";\nimport {\n  Tooltip,\n  TooltipTrigger,\n  TooltipContent,\n} from \"@/components/ui/tooltip\";\nimport CustomHeaders from \"./CustomHeaders\";\nimport { CustomHeaders as CustomHeadersType } from \"@/lib/types/customHeaders\";\nimport { useToast } from \"../lib/hooks/useToast\";\nimport IconDisplay, { WithIcons } from \"./IconDisplay\";\n\ninterface SidebarProps {\n  connectionStatus: ConnectionStatus;\n  transportType: \"stdio\" | \"sse\" | \"streamable-http\";\n  setTransportType: (type: \"stdio\" | \"sse\" | \"streamable-http\") => void;\n  command: string;\n  setCommand: (command: string) => void;\n  args: string;\n  setArgs: (args: string) => void;\n  sseUrl: string;\n  setSseUrl: (url: string) => void;\n  env: Record<string, string>;\n  setEnv: (env: Record<string, string>) => void;\n  // Custom headers support\n  customHeaders: CustomHeadersType;\n  setCustomHeaders: (headers: CustomHeadersType) => void;\n  oauthClientId: string;\n  setOauthClientId: (id: string) => void;\n  oauthClientSecret: string;\n  setOauthClientSecret: (secret: string) => void;\n  oauthScope: string;\n  setOauthScope: (scope: string) => void;\n  onConnect: () => void;\n  onDisconnect: () => void;\n  logLevel: LoggingLevel;\n  sendLogLevelRequest: (level: LoggingLevel) => void;\n  loggingSupported: boolean;\n  config: InspectorConfig;\n  setConfig: (config: InspectorConfig) => void;\n  connectionType: \"direct\" | \"proxy\";\n  setConnectionType: (type: \"direct\" | \"proxy\") => void;\n  serverImplementation?:\n    | (WithIcons & { name?: string; version?: string; websiteUrl?: string })\n    | null;\n}\n\nconst Sidebar = ({\n  connectionStatus,\n  transportType,\n  setTransportType,\n  command,\n  setCommand,\n  args,\n  setArgs,\n  sseUrl,\n  setSseUrl,\n  env,\n  setEnv,\n  customHeaders,\n  setCustomHeaders,\n  oauthClientId,\n  setOauthClientId,\n  oauthClientSecret,\n  setOauthClientSecret,\n  oauthScope,\n  setOauthScope,\n  onConnect,\n  onDisconnect,\n  logLevel,\n  sendLogLevelRequest,\n  loggingSupported,\n  config,\n  setConfig,\n  connectionType,\n  setConnectionType,\n  serverImplementation,\n}: SidebarProps) => {\n  const [theme, setTheme] = useTheme();\n  const [showEnvVars, setShowEnvVars] = useState(false);\n  const [showAuthConfig, setShowAuthConfig] = useState(false);\n  const [showConfig, setShowConfig] = useState(false);\n  const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());\n  const [showClientSecret, setShowClientSecret] = useState(false);\n  const [copiedServerEntry, setCopiedServerEntry] = useState(false);\n  const [copiedServerFile, setCopiedServerFile] = useState(false);\n  const { toast } = useToast();\n\n  const connectionTypeTip =\n    \"Connect to server directly (requires CORS config on server) or via MCP Inspector Proxy\";\n  // Reusable error reporter for copy actions\n  const reportError = useCallback(\n    (error: unknown) => {\n      toast({\n        title: \"Error\",\n        description: `Failed to copy config: ${error instanceof Error ? error.message : String(error)}`,\n        variant: \"destructive\",\n      });\n    },\n    [toast],\n  );\n\n  // Shared utility function to generate server config\n  const generateServerConfig = useCallback(() => {\n    if (transportType === \"stdio\") {\n      return {\n        command,\n        args: args.trim() ? args.split(/\\s+/) : [],\n        env: { ...env },\n      };\n    }\n    if (transportType === \"sse\") {\n      return {\n        type: \"sse\",\n        url: sseUrl,\n        note: \"For SSE connections, add this URL directly in your MCP Client\",\n      };\n    }\n    if (transportType === \"streamable-http\") {\n      return {\n        type: \"streamable-http\",\n        url: sseUrl,\n        note: \"For Streamable HTTP connections, add this URL directly in your MCP Client\",\n      };\n    }\n    return {};\n  }, [transportType, command, args, env, sseUrl]);\n\n  // Memoized config entry generator\n  const generateMCPServerEntry = useCallback(() => {\n    return JSON.stringify(generateServerConfig(), null, 4);\n  }, [generateServerConfig]);\n\n  // Memoized config file generator\n  const generateMCPServerFile = useCallback(() => {\n    return JSON.stringify(\n      {\n        mcpServers: {\n          \"default-server\": generateServerConfig(),\n        },\n      },\n      null,\n      4,\n    );\n  }, [generateServerConfig]);\n\n  // Memoized copy handlers\n  const handleCopyServerEntry = useCallback(() => {\n    try {\n      const configJson = generateMCPServerEntry();\n      navigator.clipboard\n        .writeText(configJson)\n        .then(() => {\n          setCopiedServerEntry(true);\n\n          toast({\n            title: \"Config entry copied\",\n            description:\n              transportType === \"stdio\"\n                ? \"Server configuration has been copied to clipboard. Add this to your mcp.json inside the 'mcpServers' object with your preferred server name.\"\n                : transportType === \"streamable-http\"\n                  ? \"Streamable HTTP URL has been copied. Use this URL directly in your MCP Client.\"\n                  : \"SSE URL has been copied. Use this URL directly in your MCP Client.\",\n          });\n\n          setTimeout(() => {\n            setCopiedServerEntry(false);\n          }, 2000);\n        })\n        .catch((error) => {\n          reportError(error);\n        });\n    } catch (error) {\n      reportError(error);\n    }\n  }, [generateMCPServerEntry, transportType, toast, reportError]);\n\n  const handleCopyServerFile = useCallback(() => {\n    try {\n      const configJson = generateMCPServerFile();\n      navigator.clipboard\n        .writeText(configJson)\n        .then(() => {\n          setCopiedServerFile(true);\n\n          toast({\n            title: \"Servers file copied\",\n            description:\n              \"Servers configuration has been copied to clipboard. Add this to your mcp.json file. Current testing server will be added as 'default-server'\",\n          });\n\n          setTimeout(() => {\n            setCopiedServerFile(false);\n          }, 2000);\n        })\n        .catch((error) => {\n          reportError(error);\n        });\n    } catch (error) {\n      reportError(error);\n    }\n  }, [generateMCPServerFile, toast, reportError]);\n\n  return (\n    <div className=\"bg-card border-r border-border flex flex-col h-full\">\n      <div className=\"flex items-center justify-between p-4 border-b border-gray-200 dark:border-border\">\n        <div className=\"flex items-center\">\n          <h1 className=\"ml-2 text-lg font-semibold\">\n            MCP Inspector v{version}\n          </h1>\n        </div>\n      </div>\n\n      <div className=\"p-4 flex-1 overflow-auto\">\n        <div className=\"space-y-4\">\n          <div className=\"space-y-2\">\n            <label\n              className=\"text-sm font-medium\"\n              htmlFor=\"transport-type-select\"\n            >\n              Transport Type\n            </label>\n            <Select\n              value={transportType}\n              onValueChange={(value: \"stdio\" | \"sse\" | \"streamable-http\") =>\n                setTransportType(value)\n              }\n            >\n              <SelectTrigger id=\"transport-type-select\">\n                <SelectValue placeholder=\"Select transport type\" />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectItem value=\"stdio\">STDIO</SelectItem>\n                <SelectItem value=\"sse\">SSE</SelectItem>\n                <SelectItem value=\"streamable-http\">Streamable HTTP</SelectItem>\n              </SelectContent>\n            </Select>\n          </div>\n\n          {transportType === \"stdio\" ? (\n            <>\n              <div className=\"space-y-2\">\n                <label className=\"text-sm font-medium\" htmlFor=\"command-input\">\n                  Command\n                </label>\n                <Input\n                  id=\"command-input\"\n                  placeholder=\"Command\"\n                  value={command}\n                  onChange={(e) => setCommand(e.target.value)}\n                  onBlur={(e) => setCommand(e.target.value.trim())}\n                  className=\"font-mono\"\n                />\n              </div>\n              <div className=\"space-y-2\">\n                <label\n                  className=\"text-sm font-medium\"\n                  htmlFor=\"arguments-input\"\n                >\n                  Arguments\n                </label>\n                <Input\n                  id=\"arguments-input\"\n                  placeholder=\"Arguments (space-separated)\"\n                  value={args}\n                  onChange={(e) => setArgs(e.target.value)}\n                  className=\"font-mono\"\n                />\n              </div>\n            </>\n          ) : (\n            <>\n              <div className=\"space-y-2\">\n                <label className=\"text-sm font-medium\" htmlFor=\"sse-url-input\">\n                  URL\n                </label>\n                {sseUrl ? (\n                  <Tooltip>\n                    <TooltipTrigger asChild>\n                      <Input\n                        id=\"sse-url-input\"\n                        placeholder=\"URL\"\n                        value={sseUrl}\n                        onChange={(e) => setSseUrl(e.target.value)}\n                        className=\"font-mono\"\n                      />\n                    </TooltipTrigger>\n                    <TooltipContent>{sseUrl}</TooltipContent>\n                  </Tooltip>\n                ) : (\n                  <Input\n                    id=\"sse-url-input\"\n                    placeholder=\"URL\"\n                    value={sseUrl}\n                    onChange={(e) => setSseUrl(e.target.value)}\n                    className=\"font-mono\"\n                  />\n                )}\n              </div>\n\n              {/* Connection Type switch - only visible for non-STDIO transport types */}\n              <Tooltip>\n                <TooltipTrigger asChild>\n                  <div className=\"space-y-2\">\n                    <label\n                      className=\"text-sm font-medium\"\n                      htmlFor=\"connection-type-select\"\n                    >\n                      Connection Type\n                    </label>\n                    <Select\n                      value={connectionType}\n                      onValueChange={(value: \"direct\" | \"proxy\") =>\n                        setConnectionType(value)\n                      }\n                    >\n                      <SelectTrigger id=\"connection-type-select\">\n                        <SelectValue placeholder=\"Select connection type\" />\n                      </SelectTrigger>\n                      <SelectContent>\n                        <SelectItem value=\"proxy\">Via Proxy</SelectItem>\n                        <SelectItem value=\"direct\">Direct</SelectItem>\n                      </SelectContent>\n                    </Select>\n                  </div>\n                </TooltipTrigger>\n                <TooltipContent>{connectionTypeTip}</TooltipContent>\n              </Tooltip>\n            </>\n          )}\n\n          {transportType === \"stdio\" && (\n            <div className=\"space-y-2\">\n              <Button\n                variant=\"outline\"\n                onClick={() => setShowEnvVars(!showEnvVars)}\n                className=\"flex items-center w-full\"\n                data-testid=\"env-vars-button\"\n                aria-expanded={showEnvVars}\n              >\n                {showEnvVars ? (\n                  <ChevronDown className=\"w-4 h-4 mr-2\" />\n                ) : (\n                  <ChevronRight className=\"w-4 h-4 mr-2\" />\n                )}\n                Environment Variables\n              </Button>\n              {showEnvVars && (\n                <div className=\"space-y-2\">\n                  {Object.entries(env).map(([key, value], idx) => (\n                    <div key={idx} className=\"space-y-2 pb-4\">\n                      <div className=\"flex gap-2\">\n                        <Input\n                          aria-label={`Environment variable key ${idx + 1}`}\n                          placeholder=\"Key\"\n                          value={key}\n                          onChange={(e) => {\n                            const newKey = e.target.value;\n                            const newEnv = Object.entries(env).reduce(\n                              (acc, [k, v]) => {\n                                if (k === key) {\n                                  acc[newKey] = value;\n                                } else {\n                                  acc[k] = v;\n                                }\n                                return acc;\n                              },\n                              {} as Record<string, string>,\n                            );\n                            setEnv(newEnv);\n                            setShownEnvVars((prev) => {\n                              const next = new Set(prev);\n                              if (next.has(key)) {\n                                next.delete(key);\n                                next.add(newKey);\n                              }\n                              return next;\n                            });\n                          }}\n                          className=\"font-mono\"\n                        />\n                        <Button\n                          variant=\"destructive\"\n                          size=\"icon\"\n                          className=\"h-9 w-9 p-0 shrink-0\"\n                          onClick={() => {\n                            // eslint-disable-next-line @typescript-eslint/no-unused-vars\n                            const { [key]: _removed, ...rest } = env;\n                            setEnv(rest);\n                          }}\n                        >\n                          ×\n                        </Button>\n                      </div>\n                      <div className=\"flex gap-2\">\n                        <Input\n                          aria-label={`Environment variable value ${idx + 1}`}\n                          type={shownEnvVars.has(key) ? \"text\" : \"password\"}\n                          placeholder=\"Value\"\n                          value={value}\n                          onChange={(e) => {\n                            const newEnv = { ...env };\n                            newEnv[key] = e.target.value;\n                            setEnv(newEnv);\n                          }}\n                          className=\"font-mono\"\n                        />\n                        <Button\n                          variant=\"outline\"\n                          size=\"icon\"\n                          className=\"h-9 w-9 p-0 shrink-0\"\n                          onClick={() => {\n                            setShownEnvVars((prev) => {\n                              const next = new Set(prev);\n                              if (next.has(key)) {\n                                next.delete(key);\n                              } else {\n                                next.add(key);\n                              }\n                              return next;\n                            });\n                          }}\n                          aria-label={\n                            shownEnvVars.has(key) ? \"Hide value\" : \"Show value\"\n                          }\n                          aria-pressed={shownEnvVars.has(key)}\n                          title={\n                            shownEnvVars.has(key) ? \"Hide value\" : \"Show value\"\n                          }\n                        >\n                          {shownEnvVars.has(key) ? (\n                            <Eye className=\"h-4 w-4\" aria-hidden=\"true\" />\n                          ) : (\n                            <EyeOff className=\"h-4 w-4\" aria-hidden=\"true\" />\n                          )}\n                        </Button>\n                      </div>\n                    </div>\n                  ))}\n                  <Button\n                    variant=\"outline\"\n                    className=\"w-full mt-2\"\n                    onClick={() => {\n                      const key = \"\";\n                      const newEnv = { ...env };\n                      newEnv[key] = \"\";\n                      setEnv(newEnv);\n                    }}\n                  >\n                    Add Environment Variable\n                  </Button>\n                </div>\n              )}\n            </div>\n          )}\n\n          {/* Always show both copy buttons for all transport types */}\n          <div className=\"grid grid-cols-2 gap-2 mt-2\">\n            <Tooltip>\n              <TooltipTrigger asChild>\n                <Button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  onClick={handleCopyServerEntry}\n                  className=\"w-full\"\n                >\n                  {copiedServerEntry ? (\n                    <CheckCheck className=\"h-4 w-4 mr-2\" />\n                  ) : (\n                    <Copy className=\"h-4 w-4 mr-2\" />\n                  )}\n                  Server Entry\n                </Button>\n              </TooltipTrigger>\n              <TooltipContent>Copy Server Entry</TooltipContent>\n            </Tooltip>\n            <Tooltip>\n              <TooltipTrigger asChild>\n                <Button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  onClick={handleCopyServerFile}\n                  className=\"w-full\"\n                >\n                  {copiedServerFile ? (\n                    <CheckCheck className=\"h-4 w-4 mr-2\" />\n                  ) : (\n                    <Copy className=\"h-4 w-4 mr-2\" />\n                  )}\n                  Servers File\n                </Button>\n              </TooltipTrigger>\n              <TooltipContent>Copy Servers File</TooltipContent>\n            </Tooltip>\n          </div>\n\n          <div className=\"space-y-2\">\n            <Button\n              variant=\"outline\"\n              onClick={() => setShowAuthConfig(!showAuthConfig)}\n              className=\"flex items-center w-full\"\n              data-testid=\"auth-button\"\n              aria-expanded={showAuthConfig}\n            >\n              {showAuthConfig ? (\n                <ChevronDown className=\"w-4 h-4 mr-2\" />\n              ) : (\n                <ChevronRight className=\"w-4 h-4 mr-2\" />\n              )}\n              Authentication\n            </Button>\n            {showAuthConfig && (\n              <>\n                {/* Custom Headers Section */}\n                <div className=\"p-3 rounded border overflow-hidden\">\n                  <CustomHeaders\n                    headers={customHeaders}\n                    onChange={setCustomHeaders}\n                  />\n                </div>\n                {transportType !== \"stdio\" && (\n                  // OAuth Configuration\n                  <div className=\"space-y-2 p-3  rounded border\">\n                    <h4 className=\"text-sm font-semibold flex items-center\">\n                      OAuth 2.0 Flow\n                    </h4>\n                    <div className=\"space-y-2\">\n                      <label className=\"text-sm font-medium\">Client ID</label>\n                      <Input\n                        placeholder=\"Client ID\"\n                        onChange={(e) => setOauthClientId(e.target.value)}\n                        value={oauthClientId}\n                        data-testid=\"oauth-client-id-input\"\n                        className=\"font-mono\"\n                      />\n                      <label className=\"text-sm font-medium\">\n                        Client Secret\n                      </label>\n                      <div className=\"flex gap-2\">\n                        <Input\n                          type={showClientSecret ? \"text\" : \"password\"}\n                          placeholder=\"Client Secret (optional)\"\n                          onChange={(e) => setOauthClientSecret(e.target.value)}\n                          value={oauthClientSecret}\n                          data-testid=\"oauth-client-secret-input\"\n                          className=\"font-mono\"\n                        />\n                        <Button\n                          variant=\"outline\"\n                          size=\"icon\"\n                          className=\"h-9 w-9 p-0 shrink-0\"\n                          onClick={() => setShowClientSecret(!showClientSecret)}\n                          aria-label={\n                            showClientSecret ? \"Hide secret\" : \"Show secret\"\n                          }\n                          aria-pressed={showClientSecret}\n                          title={\n                            showClientSecret ? \"Hide secret\" : \"Show secret\"\n                          }\n                        >\n                          {showClientSecret ? (\n                            <Eye className=\"h-4 w-4\" aria-hidden=\"true\" />\n                          ) : (\n                            <EyeOff className=\"h-4 w-4\" aria-hidden=\"true\" />\n                          )}\n                        </Button>\n                      </div>\n                      <label className=\"text-sm font-medium\">\n                        Redirect URL\n                      </label>\n                      <Input\n                        readOnly\n                        placeholder=\"Redirect URL\"\n                        value={window.location.origin + \"/oauth/callback\"}\n                        className=\"font-mono\"\n                      />\n                      <label className=\"text-sm font-medium\">Scope</label>\n                      <Input\n                        placeholder=\"Scope (space-separated)\"\n                        onChange={(e) => setOauthScope(e.target.value)}\n                        value={oauthScope}\n                        data-testid=\"oauth-scope-input\"\n                        className=\"font-mono\"\n                      />\n                    </div>\n                  </div>\n                )}\n              </>\n            )}\n          </div>\n          {/* Configuration */}\n          <div className=\"space-y-2\">\n            <Button\n              variant=\"outline\"\n              onClick={() => setShowConfig(!showConfig)}\n              className=\"flex items-center w-full\"\n              data-testid=\"config-button\"\n              aria-expanded={showConfig}\n            >\n              {showConfig ? (\n                <ChevronDown className=\"w-4 h-4 mr-2\" />\n              ) : (\n                <ChevronRight className=\"w-4 h-4 mr-2\" />\n              )}\n              <Settings className=\"w-4 h-4 mr-2\" />\n              Configuration\n            </Button>\n            {showConfig && (\n              <div className=\"space-y-2\">\n                {Object.entries(config).map(([key, configItem]) => {\n                  const configKey = key as keyof InspectorConfig;\n                  return (\n                    <div key={key} className=\"space-y-2\">\n                      <div className=\"flex items-center gap-1\">\n                        <label\n                          className=\"text-sm font-medium text-green-600 break-all\"\n                          htmlFor={`${configKey}-input`}\n                        >\n                          {configItem.label}\n                        </label>\n                        <Tooltip>\n                          <TooltipTrigger asChild>\n                            <HelpCircle className=\"h-4 w-4 text-muted-foreground\" />\n                          </TooltipTrigger>\n                          <TooltipContent>\n                            {configItem.description}\n                          </TooltipContent>\n                        </Tooltip>\n                      </div>\n                      {typeof configItem.value === \"number\" ? (\n                        <Input\n                          id={`${configKey}-input`}\n                          type=\"number\"\n                          data-testid={`${configKey}-input`}\n                          value={configItem.value}\n                          onChange={(e) => {\n                            const newConfig = { ...config };\n                            newConfig[configKey] = {\n                              ...configItem,\n                              value: Number(e.target.value),\n                            };\n                            setConfig(newConfig);\n                          }}\n                          className=\"font-mono\"\n                        />\n                      ) : typeof configItem.value === \"boolean\" ? (\n                        <Select\n                          data-testid={`${configKey}-select`}\n                          value={configItem.value.toString()}\n                          onValueChange={(val) => {\n                            const newConfig = { ...config };\n                            newConfig[configKey] = {\n                              ...configItem,\n                              value: val === \"true\",\n                            };\n                            setConfig(newConfig);\n                          }}\n                        >\n                          <SelectTrigger id={`${configKey}-input`}>\n                            <SelectValue />\n                          </SelectTrigger>\n                          <SelectContent>\n                            <SelectItem value=\"true\">True</SelectItem>\n                            <SelectItem value=\"false\">False</SelectItem>\n                          </SelectContent>\n                        </Select>\n                      ) : (\n                        <Input\n                          id={`${configKey}-input`}\n                          data-testid={`${configKey}-input`}\n                          value={configItem.value}\n                          onChange={(e) => {\n                            const newConfig = { ...config };\n                            newConfig[configKey] = {\n                              ...configItem,\n                              value: e.target.value,\n                            };\n                            setConfig(newConfig);\n                          }}\n                          className=\"font-mono\"\n                        />\n                      )}\n                    </div>\n                  );\n                })}\n              </div>\n            )}\n          </div>\n\n          <div className=\"space-y-2\">\n            {connectionStatus === \"connected\" && (\n              <div className=\"grid grid-cols-2 gap-4\">\n                <Button\n                  data-testid=\"connect-button\"\n                  onClick={() => {\n                    onDisconnect();\n                    onConnect();\n                  }}\n                >\n                  <RotateCcw className=\"w-4 h-4 mr-2\" />\n                  {transportType === \"stdio\" ? \"Restart\" : \"Reconnect\"}\n                </Button>\n                <Button onClick={onDisconnect}>\n                  <RefreshCwOff className=\"w-4 h-4 mr-2\" />\n                  Disconnect\n                </Button>\n              </div>\n            )}\n            {connectionStatus !== \"connected\" && (\n              <Button className=\"w-full\" onClick={onConnect}>\n                <Play className=\"w-4 h-4 mr-2\" />\n                Connect\n              </Button>\n            )}\n\n            <div className=\"flex items-center justify-center space-x-2 mb-4\">\n              <div\n                className={`w-2 h-2 rounded-full ${(() => {\n                  switch (connectionStatus) {\n                    case \"connected\":\n                      return \"bg-green-500\";\n                    case \"error\":\n                      return \"bg-red-500\";\n                    case \"error-connecting-to-proxy\":\n                      return \"bg-red-500\";\n                    default:\n                      return \"bg-gray-500\";\n                  }\n                })()}`}\n              />\n              <span className=\"text-sm text-gray-600 dark:text-gray-400\">\n                {(() => {\n                  switch (connectionStatus) {\n                    case \"connected\":\n                      return \"Connected\";\n                    case \"error\": {\n                      const hasProxyToken = config.MCP_PROXY_AUTH_TOKEN?.value;\n                      if (!hasProxyToken) {\n                        return \"Connection Error - Did you add the proxy session token in Configuration?\";\n                      }\n                      return \"Connection Error - Check if your MCP server is running and proxy token is correct\";\n                    }\n                    case \"error-connecting-to-proxy\":\n                      return \"Error Connecting to MCP Inspector Proxy - Check Console logs\";\n                    default:\n                      return \"Disconnected\";\n                  }\n                })()}\n              </span>\n            </div>\n\n            {connectionStatus === \"connected\" && serverImplementation && (\n              <div className=\"bg-gray-50 dark:bg-gray-900 p-3 rounded-lg mb-4\">\n                <div className=\"flex items-center gap-2 mb-1\">\n                  {(serverImplementation as WithIcons).icons &&\n                  (serverImplementation as WithIcons).icons!.length > 0 ? (\n                    <IconDisplay\n                      icons={(serverImplementation as WithIcons).icons}\n                      size=\"sm\"\n                    />\n                  ) : (\n                    <Server className=\"w-4 h-4 text-gray-500\" />\n                  )}\n                  {(serverImplementation as { websiteUrl?: string })\n                    .websiteUrl ? (\n                    <a\n                      href={\n                        (serverImplementation as { websiteUrl?: string })\n                          .websiteUrl\n                      }\n                      target=\"_blank\"\n                      rel=\"noopener noreferrer\"\n                      className=\"text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline transition-colors\"\n                    >\n                      {serverImplementation.name || \"MCP Server\"}\n                    </a>\n                  ) : (\n                    <span className=\"text-sm font-medium text-gray-800 dark:text-gray-200\">\n                      {serverImplementation.name || \"MCP Server\"}\n                    </span>\n                  )}\n                </div>\n                {serverImplementation.version && (\n                  <div className=\"text-xs text-gray-500 dark:text-gray-400\">\n                    Version: {serverImplementation.version}\n                  </div>\n                )}\n              </div>\n            )}\n\n            {loggingSupported && connectionStatus === \"connected\" && (\n              <div className=\"space-y-2\">\n                <label\n                  className=\"text-sm font-medium\"\n                  htmlFor=\"logging-level-select\"\n                >\n                  Logging Level\n                </label>\n                <Select\n                  value={logLevel}\n                  onValueChange={(value: LoggingLevel) =>\n                    sendLogLevelRequest(value)\n                  }\n                >\n                  <SelectTrigger id=\"logging-level-select\">\n                    <SelectValue placeholder=\"Select logging level\" />\n                  </SelectTrigger>\n                  <SelectContent>\n                    {Object.values(LoggingLevelSchema.enum).map((level) => (\n                      <SelectItem key={level} value={level}>\n                        {level}\n                      </SelectItem>\n                    ))}\n                  </SelectContent>\n                </Select>\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n      <div className=\"p-4 border-t\">\n        <div className=\"flex items-center justify-between\">\n          <Select\n            value={theme}\n            onValueChange={(value: string) =>\n              setTheme(value as \"system\" | \"light\" | \"dark\")\n            }\n          >\n            <SelectTrigger className=\"w-[100px]\" id=\"theme-select\">\n              <SelectValue />\n            </SelectTrigger>\n            <SelectContent>\n              <SelectItem value=\"system\">System</SelectItem>\n              <SelectItem value=\"light\">Light</SelectItem>\n              <SelectItem value=\"dark\">Dark</SelectItem>\n            </SelectContent>\n          </Select>\n\n          <div className=\"flex items-center space-x-2\">\n            <Button variant=\"ghost\" title=\"Inspector Documentation\" asChild>\n              <a\n                href=\"https://modelcontextprotocol.io/docs/tools/inspector\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n              >\n                <CircleHelp className=\"w-4 h-4 text-foreground\" />\n              </a>\n            </Button>\n            <Button variant=\"ghost\" title=\"Debugging Guide\" asChild>\n              <a\n                href=\"https://modelcontextprotocol.io/docs/tools/debugging\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n              >\n                <Bug className=\"w-4 h-4 text-foreground\" />\n              </a>\n            </Button>\n            <Button\n              variant=\"ghost\"\n              title=\"Report bugs or contribute on GitHub\"\n              asChild\n            >\n              <a\n                href=\"https://github.com/modelcontextprotocol/inspector\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n              >\n                <Github className=\"w-4 h-4 text-foreground\" />\n              </a>\n            </Button>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default Sidebar;\n"
  },
  {
    "path": "client/src/components/TasksTab.tsx",
    "content": "import { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\nimport { Button } from \"@/components/ui/button\";\nimport { TabsContent } from \"@/components/ui/tabs\";\nimport { Task } from \"@modelcontextprotocol/sdk/types.js\";\nimport {\n  AlertCircle,\n  RefreshCw,\n  XCircle,\n  Clock,\n  CheckCircle2,\n  AlertTriangle,\n  PlayCircle,\n} from \"lucide-react\";\nimport ListPane from \"./ListPane\";\nimport { useState } from \"react\";\nimport JsonView from \"./JsonView\";\nimport { cn } from \"@/lib/utils\";\n\nconst TaskStatusIcon = ({ status }: { status: Task[\"status\"] }) => {\n  switch (status) {\n    case \"working\":\n      return <Clock className=\"h-4 w-4 animate-pulse text-blue-500\" />;\n    case \"input_required\":\n      return <AlertTriangle className=\"h-4 w-4 text-yellow-500\" />;\n    case \"completed\":\n      return <CheckCircle2 className=\"h-4 w-4 text-green-500\" />;\n    case \"failed\":\n      return <XCircle className=\"h-4 w-4 text-red-500\" />;\n    case \"cancelled\":\n      return <XCircle className=\"h-4 w-4 text-gray-500\" />;\n    default:\n      return <PlayCircle className=\"h-4 w-4\" />;\n  }\n};\n\nconst TasksTab = ({\n  tasks,\n  listTasks,\n  clearTasks,\n  cancelTask,\n  selectedTask,\n  setSelectedTask,\n  error,\n  nextCursor,\n}: {\n  tasks: Task[];\n  listTasks: () => void;\n  clearTasks: () => void;\n  cancelTask: (taskId: string) => Promise<void>;\n  selectedTask: Task | null;\n  setSelectedTask: (task: Task | null) => void;\n  error: string | null;\n  nextCursor?: string;\n}) => {\n  const [isCancelling, setIsCancelling] = useState<string | null>(null);\n\n  const displayedTask = selectedTask\n    ? tasks.find((t) => t.taskId === selectedTask.taskId) || selectedTask\n    : null;\n\n  const handleCancel = async (taskId: string) => {\n    setIsCancelling(taskId);\n    try {\n      await cancelTask(taskId);\n    } finally {\n      setIsCancelling(null);\n    }\n  };\n\n  return (\n    <TabsContent value=\"tasks\" className=\"flex-1 overflow-hidden p-0 m-0\">\n      <div className=\"flex h-full overflow-hidden p-4 gap-4\">\n        <div className=\"w-1/3\">\n          <ListPane\n            title=\"Tasks\"\n            items={tasks}\n            setSelectedItem={setSelectedTask}\n            listItems={listTasks}\n            clearItems={clearTasks}\n            buttonText={nextCursor ? \"List More Tasks\" : \"List Tasks\"}\n            isButtonDisabled={!nextCursor && tasks.length > 0}\n            renderItem={(task) => (\n              <div className=\"flex items-center gap-2 overflow-hidden w-full\">\n                <TaskStatusIcon status={task.status} />\n                <div className=\"flex flex-col overflow-hidden\">\n                  <span className=\"truncate font-medium\">{task.taskId}</span>\n                  <span className=\"truncate text-xs text-muted-foreground\">\n                    {task.status} -{\" \"}\n                    {new Date(task.lastUpdatedAt).toLocaleString()}\n                  </span>\n                </div>\n              </div>\n            )}\n          />\n        </div>\n\n        <div className=\"flex-1 overflow-y-auto p-4 bg-background border border-border rounded-lg\">\n          {error && (\n            <Alert variant=\"destructive\" className=\"mb-4\">\n              <AlertCircle className=\"h-4 w-4\" />\n              <AlertTitle>Error</AlertTitle>\n              <AlertDescription>{error}</AlertDescription>\n            </Alert>\n          )}\n\n          {displayedTask ? (\n            <div className=\"space-y-6\">\n              <div className=\"flex items-center justify-between border-b pb-4\">\n                <div>\n                  <h2 className=\"text-2xl font-bold tracking-tight\">\n                    Task Details\n                  </h2>\n                  <p className=\"text-muted-foreground\">\n                    ID: {displayedTask.taskId}\n                  </p>\n                </div>\n                {(displayedTask.status === \"working\" ||\n                  displayedTask.status === \"input_required\") && (\n                  <Button\n                    variant=\"destructive\"\n                    size=\"sm\"\n                    aria-label={`Cancel task ${displayedTask.taskId}`}\n                    onClick={() => handleCancel(displayedTask.taskId)}\n                    disabled={isCancelling === displayedTask.taskId}\n                  >\n                    {isCancelling === displayedTask.taskId ? (\n                      <RefreshCw className=\"mr-2 h-4 w-4 animate-spin\" />\n                    ) : (\n                      <XCircle className=\"mr-2 h-4 w-4\" />\n                    )}\n                    Cancel Task\n                  </Button>\n                )}\n              </div>\n\n              <div className=\"grid grid-cols-2 gap-4\">\n                <div className=\"rounded-lg border p-3\">\n                  <p className=\"text-sm font-medium text-muted-foreground\">\n                    Status\n                  </p>\n                  <div className=\"mt-1 flex items-center gap-2\">\n                    <TaskStatusIcon status={displayedTask.status} />\n                    <span\n                      className={cn(\n                        \"font-semibold capitalize\",\n                        displayedTask.status === \"working\" && \"text-blue-500\",\n                        displayedTask.status === \"completed\" &&\n                          \"text-green-500\",\n                        displayedTask.status === \"failed\" && \"text-red-500\",\n                        displayedTask.status === \"cancelled\" && \"text-gray-500\",\n                        displayedTask.status === \"input_required\" &&\n                          \"text-yellow-500\",\n                      )}\n                    >\n                      {displayedTask.status.replace(\"_\", \" \")}\n                    </span>\n                  </div>\n                </div>\n                <div className=\"rounded-lg border p-3\">\n                  <p className=\"text-sm font-medium text-muted-foreground\">\n                    Last Updated\n                  </p>\n                  <p className=\"mt-1 font-medium\">\n                    {new Date(displayedTask.lastUpdatedAt).toLocaleString()}\n                  </p>\n                </div>\n                <div className=\"rounded-lg border p-3\">\n                  <p className=\"text-sm font-medium text-muted-foreground\">\n                    Created At\n                  </p>\n                  <p className=\"mt-1 font-medium\">\n                    {new Date(displayedTask.createdAt).toLocaleString()}\n                  </p>\n                </div>\n                <div className=\"rounded-lg border p-3\">\n                  <p className=\"text-sm font-medium text-muted-foreground\">\n                    TTL\n                  </p>\n                  <p className=\"mt-1 font-medium\">\n                    {displayedTask.ttl === null\n                      ? \"Infinite\"\n                      : `${displayedTask.ttl}ms`}\n                  </p>\n                </div>\n              </div>\n\n              {displayedTask.statusMessage && (\n                <div className=\"rounded-lg border p-3\">\n                  <p className=\"text-sm font-medium text-muted-foreground\">\n                    Status Message\n                  </p>\n                  <p className=\"mt-1 whitespace-pre-wrap\">\n                    {displayedTask.statusMessage}\n                  </p>\n                </div>\n              )}\n\n              <div className=\"space-y-2\">\n                <h3 className=\"text-lg font-semibold\">Full Task Object</h3>\n                <div className=\"rounded-md border\">\n                  <JsonView data={displayedTask} />\n                </div>\n              </div>\n            </div>\n          ) : (\n            <div className=\"flex h-full items-center justify-center text-muted-foreground\">\n              <div className=\"text-center\">\n                <Clock className=\"mx-auto mb-4 h-12 w-12 opacity-20\" />\n                <h3 className=\"text-lg font-medium\">No Task Selected</h3>\n                <p>Select a task from the list to view its details.</p>\n                <Button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  className=\"mt-4\"\n                  onClick={listTasks}\n                >\n                  <RefreshCw className=\"mr-2 h-4 w-4\" />\n                  Refresh Tasks\n                </Button>\n              </div>\n            </div>\n          )}\n        </div>\n      </div>\n    </TabsContent>\n  );\n};\n\nexport default TasksTab;\n"
  },
  {
    "path": "client/src/components/ToolResults.tsx",
    "content": "import JsonView from \"./JsonView\";\nimport ResourceLinkView from \"./ResourceLinkView\";\nimport {\n  CallToolResultSchema,\n  CompatibilityCallToolResult,\n  Tool,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { validateToolOutput, hasOutputSchema } from \"@/utils/schemaUtils\";\n\ninterface ToolResultsProps {\n  toolResult: CompatibilityCallToolResult | null;\n  selectedTool: Tool | null;\n  resourceContent: Record<string, string>;\n  onReadResource?: (uri: string) => void;\n  isPollingTask?: boolean;\n}\n\nconst checkContentCompatibility = (\n  structuredContent: unknown,\n  unstructuredContent: Array<{\n    type: string;\n    text?: string;\n    [key: string]: unknown;\n  }>,\n): { hasMatch: boolean; message: string } | null => {\n  // Look for at least one text content block that matches the structured content\n  const textBlocks = unstructuredContent.filter(\n    (block) => block.type === \"text\",\n  );\n\n  if (textBlocks.length === 0) {\n    return null;\n  }\n\n  // Check if any text block contains JSON that matches the structured content\n  for (const textBlock of textBlocks) {\n    const textContent = textBlock.text;\n    if (!textContent) {\n      continue;\n    }\n\n    try {\n      const parsedContent = JSON.parse(textContent);\n      const isEqual =\n        JSON.stringify(parsedContent) === JSON.stringify(structuredContent);\n\n      if (isEqual) {\n        return {\n          hasMatch: true,\n          message: `Structured content matches text block${textBlocks.length > 1 ? \" (multiple blocks)\" : \"\"}${unstructuredContent.length > textBlocks.length ? \" + other content\" : \"\"}`,\n        };\n      }\n    } catch {\n      // Continue to next text block if this one doesn't parse as JSON\n      continue;\n    }\n  }\n\n  return null;\n};\n\nconst ToolResults = ({\n  toolResult,\n  selectedTool,\n  resourceContent,\n  onReadResource,\n  isPollingTask,\n}: ToolResultsProps) => {\n  if (!toolResult) return null;\n\n  if (\"content\" in toolResult) {\n    const parsedResult = CallToolResultSchema.safeParse(toolResult);\n    if (!parsedResult.success) {\n      return (\n        <>\n          <h4 className=\"font-semibold mb-2\">Invalid Tool Result:</h4>\n          <JsonView data={toolResult} />\n          <h4 className=\"font-semibold mb-2\">Errors:</h4>\n          {parsedResult.error.issues.map((issue, idx) => (\n            <JsonView data={issue} key={idx} />\n          ))}\n        </>\n      );\n    }\n    const structuredResult = parsedResult.data;\n    const isError = structuredResult.isError ?? false;\n\n    // Check if this is a running task\n    const relatedTask = structuredResult._meta?.[\n      \"io.modelcontextprotocol/related-task\"\n    ] as { taskId: string } | undefined;\n    const isTaskRunning =\n      isPollingTask ||\n      (!!relatedTask &&\n        structuredResult.content.some(\n          (c) =>\n            c.type === \"text\" &&\n            (c.text?.includes(\"Polling\") || c.text?.includes(\"Task status\")),\n        ));\n\n    let validationResult = null;\n    const toolHasOutputSchema =\n      selectedTool && hasOutputSchema(selectedTool.name);\n\n    if (toolHasOutputSchema) {\n      if (!structuredResult.structuredContent && !isError) {\n        validationResult = {\n          isValid: false,\n          error:\n            \"Tool has an output schema but did not return structured content\",\n        };\n      } else if (structuredResult.structuredContent) {\n        validationResult = validateToolOutput(\n          selectedTool.name,\n          structuredResult.structuredContent,\n        );\n      }\n    }\n\n    let compatibilityResult = null;\n    if (\n      structuredResult.structuredContent &&\n      structuredResult.content.length > 0 &&\n      selectedTool &&\n      hasOutputSchema(selectedTool.name)\n    ) {\n      compatibilityResult = checkContentCompatibility(\n        structuredResult.structuredContent,\n        structuredResult.content,\n      );\n    }\n\n    return (\n      <>\n        <h4 className=\"font-semibold mb-2\">\n          Tool Result:{\" \"}\n          {isError ? (\n            <span className=\"text-red-600 font-semibold\">Error</span>\n          ) : isTaskRunning ? (\n            <span className=\"text-yellow-600 font-semibold\">Task Running</span>\n          ) : (\n            <span className=\"text-green-600 font-semibold\">Success</span>\n          )}\n        </h4>\n        {structuredResult.structuredContent && (\n          <div className=\"mb-4\">\n            <h5 className=\"font-semibold mb-2 text-sm\">Structured Content:</h5>\n            <div className=\"bg-gray-50 dark:bg-gray-900 p-3 rounded-lg\">\n              <JsonView data={structuredResult.structuredContent} />\n              {validationResult && (\n                <div\n                  className={`mt-2 p-2 rounded text-sm ${\n                    validationResult.isValid\n                      ? \"bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200\"\n                      : \"bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200\"\n                  }`}\n                >\n                  {validationResult.isValid ? (\n                    \"✓ Valid according to output schema\"\n                  ) : (\n                    <>✗ Validation Error: {validationResult.error}</>\n                  )}\n                </div>\n              )}\n            </div>\n          </div>\n        )}\n        {structuredResult._meta && (\n          <div className=\"mb-4\">\n            <h5 className=\"font-semibold mb-2 text-sm\">Meta:</h5>\n            <div className=\"bg-gray-50 dark:bg-gray-900 p-3 rounded-lg\">\n              <JsonView data={structuredResult._meta} />\n            </div>\n          </div>\n        )}\n        {!structuredResult.structuredContent &&\n          validationResult &&\n          !validationResult.isValid && (\n            <div className=\"mb-4\">\n              <div className=\"bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 p-2 rounded text-sm\">\n                ✗ Validation Error: {validationResult.error}\n              </div>\n            </div>\n          )}\n        {structuredResult.content.length > 0 && (\n          <div className=\"mb-4\">\n            {structuredResult.structuredContent && (\n              <>\n                <h5 className=\"font-semibold mb-2 text-sm\">\n                  Unstructured Content:\n                </h5>\n                {compatibilityResult?.hasMatch && (\n                  <div className=\"mb-2 p-2 rounded text-sm bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200\">\n                    ✓ {compatibilityResult.message}\n                  </div>\n                )}\n              </>\n            )}\n            {structuredResult.content.map((item, index) => (\n              <div key={index} className=\"mb-2\">\n                {item.type === \"text\" && (\n                  <JsonView data={item.text} isError={isError} />\n                )}\n                {item.type === \"image\" && (\n                  <img\n                    src={`data:${item.mimeType};base64,${item.data}`}\n                    alt=\"Tool result image\"\n                    className=\"max-w-full h-auto\"\n                  />\n                )}\n                {item.type === \"resource\" &&\n                  (item.resource?.mimeType?.startsWith(\"audio/\") &&\n                  \"blob\" in item.resource ? (\n                    <audio\n                      controls\n                      src={`data:${item.resource.mimeType};base64,${item.resource.blob}`}\n                      className=\"w-full\"\n                    >\n                      <p>Your browser does not support audio playback</p>\n                    </audio>\n                  ) : (\n                    <JsonView data={item.resource} />\n                  ))}\n                {item.type === \"resource_link\" && (\n                  <ResourceLinkView\n                    uri={item.uri}\n                    name={item.name}\n                    description={item.description}\n                    mimeType={item.mimeType}\n                    resourceContent={resourceContent[item.uri] || \"\"}\n                    onReadResource={onReadResource}\n                  />\n                )}\n              </div>\n            ))}\n          </div>\n        )}\n      </>\n    );\n  } else if (\"toolResult\" in toolResult) {\n    return (\n      <>\n        <h4 className=\"font-semibold mb-2\">Tool Result (Legacy):</h4>\n        <JsonView data={toolResult.toolResult} />\n      </>\n    );\n  }\n\n  return null;\n};\n\nexport default ToolResults;\n"
  },
  {
    "path": "client/src/components/ToolsTab.tsx",
    "content": "import { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\nimport { Button } from \"@/components/ui/button\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { TabsContent } from \"@/components/ui/tabs\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport DynamicJsonForm, { DynamicJsonFormRef } from \"./DynamicJsonForm\";\nimport type { JsonValue, JsonSchemaType } from \"@/utils/jsonUtils\";\nimport {\n  generateDefaultValue,\n  isPropertyRequired,\n  normalizeUnionType,\n  resolveRef,\n} from \"@/utils/schemaUtils\";\nimport {\n  CompatibilityCallToolResult,\n  ListToolsResult,\n  Tool,\n  ToolAnnotations,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport {\n  Loader2,\n  Send,\n  ChevronDown,\n  ChevronUp,\n  ChevronRight,\n  AlertCircle,\n  Copy,\n  CheckCheck,\n} from \"lucide-react\";\nimport { useEffect, useState, useRef } from \"react\";\nimport ListPane from \"./ListPane\";\nimport JsonView from \"./JsonView\";\nimport ToolResults from \"./ToolResults\";\nimport { useToast } from \"@/lib/hooks/useToast\";\nimport useCopy from \"@/lib/hooks/useCopy\";\nimport IconDisplay, { WithIcons } from \"./IconDisplay\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  META_NAME_RULES_MESSAGE,\n  META_PREFIX_RULES_MESSAGE,\n  RESERVED_NAMESPACE_MESSAGE,\n  hasValidMetaName,\n  hasValidMetaPrefix,\n  isReservedMetaKey,\n} from \"@/utils/metaUtils\";\n\n/**\n * Extended Tool type that includes optional fields used by the inspector.\n */\nexport interface ExtendedTool extends Tool, WithIcons {\n  _meta?: Record<string, unknown>;\n  execution?: {\n    taskSupport?: \"forbidden\" | \"required\" | \"optional\";\n  };\n}\n\n// Type guard to safely detect the optional _meta field without using `any`\nconst hasMeta = (\n  tool: Tool,\n): tool is ExtendedTool & { _meta: Record<string, unknown> } =>\n  typeof (tool as ExtendedTool)._meta !== \"undefined\";\n\n// Returns the execution.taskSupport value for a tool, defaulting to \"forbidden\" per MCP spec\nconst getTaskSupport = (\n  tool: Tool | null,\n): \"forbidden\" | \"required\" | \"optional\" => {\n  if (!tool) return \"forbidden\";\n  const extendedTool = tool as ExtendedTool;\n  const taskSupport = extendedTool.execution?.taskSupport;\n  if (\n    taskSupport === \"forbidden\" ||\n    taskSupport === \"required\" ||\n    taskSupport === \"optional\"\n  ) {\n    return taskSupport;\n  }\n  return \"forbidden\";\n};\n\n// Type guard to safely detect the optional annotations field\nconst hasAnnotations = (\n  tool: Tool,\n): tool is Tool & { annotations: ToolAnnotations } =>\n  typeof (tool as { annotations?: unknown }).annotations !== \"undefined\" &&\n  (tool as { annotations?: unknown }).annotations !== null;\n\n// Helper to render annotation badges\n// Shows all 4 annotation values with their state (true/false/implied default)\nconst AnnotationBadges = ({\n  annotations,\n}: {\n  annotations: ToolAnnotations | undefined;\n}) => {\n  // Spec defaults: readOnlyHint=false, destructiveHint=true, idempotentHint=false, openWorldHint=true\n  const getValueAndImplied = (\n    value: boolean | undefined,\n    defaultValue: boolean,\n  ): { value: boolean; implied: boolean } => ({\n    value: value ?? defaultValue,\n    implied: value === undefined,\n  });\n\n  const readOnly = getValueAndImplied(annotations?.readOnlyHint, false);\n  const destructive = getValueAndImplied(annotations?.destructiveHint, true);\n  const idempotent = getValueAndImplied(annotations?.idempotentHint, false);\n  const openWorld = getValueAndImplied(annotations?.openWorldHint, true);\n\n  // Descriptions from MCP spec\n  const badges = [\n    {\n      label: \"Read-only\",\n      value: readOnly.value,\n      implied: readOnly.implied,\n      description: \"Tool does not modify its environment\",\n    },\n    {\n      label: \"Destructive\",\n      value: destructive.value,\n      implied: destructive.implied,\n      description:\n        \"Tool may perform destructive updates (delete/overwrite data)\",\n    },\n    {\n      label: \"Idempotent\",\n      value: idempotent.value,\n      implied: idempotent.implied,\n      description: \"Calling repeatedly with same args has no additional effect\",\n    },\n    {\n      label: \"Open-world\",\n      value: openWorld.value,\n      implied: openWorld.implied,\n      description:\n        \"Tool may interact with external entities beyond its local environment\",\n    },\n  ];\n\n  return (\n    <div className=\"flex flex-wrap gap-1 mt-2\">\n      {badges.map(({ label, value, implied, description }) => (\n        <span\n          key={label}\n          title={`${description}\\n\\nValue: ${value ? \"Yes\" : \"No\"} (${implied ? \"implied default\" : \"explicitly set\"})`}\n          className={cn(\n            \"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border\",\n            \"bg-slate-100 text-slate-700 border-slate-300 dark:bg-slate-800 dark:text-slate-300 dark:border-slate-600\",\n            implied && \"border-dashed opacity-60\",\n            !implied && \"border-solid\",\n          )}\n        >\n          {value ? \"✓\" : \"✗\"} {label}\n        </span>\n      ))}\n    </div>\n  );\n};\n\nconst ToolsTab = ({\n  tools,\n  listTools,\n  clearTools,\n  callTool,\n  selectedTool,\n  setSelectedTool,\n  toolResult,\n  isPollingTask,\n  nextCursor,\n  error,\n  resourceContent,\n  onReadResource,\n  serverSupportsTaskRequests,\n}: {\n  tools: Tool[];\n  listTools: () => void;\n  clearTools: () => void;\n  callTool: (\n    name: string,\n    params: Record<string, unknown>,\n    metadata?: Record<string, unknown>,\n    runAsTask?: boolean,\n  ) => Promise<CompatibilityCallToolResult>;\n  selectedTool: Tool | null;\n  setSelectedTool: (tool: Tool | null) => void;\n  toolResult: CompatibilityCallToolResult | null;\n  isPollingTask?: boolean;\n  nextCursor: ListToolsResult[\"nextCursor\"];\n  error: string | null;\n  resourceContent: Record<string, string>;\n  onReadResource?: (uri: string) => void;\n  serverSupportsTaskRequests: boolean;\n}) => {\n  const [params, setParams] = useState<Record<string, unknown>>({});\n  const [runAsTask, setRunAsTask] = useState(false);\n  const [isToolRunning, setIsToolRunning] = useState(false);\n  const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false);\n  const [isMetadataExpanded, setIsMetadataExpanded] = useState(false);\n  const [metadataEntries, setMetadataEntries] = useState<\n    { id: string; key: string; value: string }[]\n  >([]);\n  const [hasValidationErrors, setHasValidationErrors] = useState(false);\n  const formRefs = useRef<Record<string, DynamicJsonFormRef | null>>({});\n  const { toast } = useToast();\n  const { copied, setCopied } = useCopy();\n\n  // Function to check if any form has validation errors\n  const checkValidationErrors = (validateChildren: boolean = false) => {\n    const errors = Object.values(formRefs.current).some(\n      (ref) =>\n        ref &&\n        (validateChildren ? !ref.validateJson().isValid : ref.hasJsonError()),\n    );\n    setHasValidationErrors(errors);\n    return errors;\n  };\n\n  useEffect(() => {\n    const params = Object.entries(\n      selectedTool?.inputSchema.properties ?? [],\n    ).map(([key, value]) => {\n      // First resolve any $ref references\n      const resolvedValue = resolveRef(\n        value as JsonSchemaType,\n        selectedTool?.inputSchema as JsonSchemaType,\n      );\n      return [\n        key,\n        generateDefaultValue(\n          resolvedValue,\n          key,\n          selectedTool?.inputSchema as JsonSchemaType,\n        ),\n      ];\n    });\n    setParams(Object.fromEntries(params));\n    const toolTaskSupport = serverSupportsTaskRequests\n      ? getTaskSupport(selectedTool)\n      : \"forbidden\";\n    setRunAsTask(toolTaskSupport === \"required\");\n\n    // Reset validation errors when switching tools\n    setHasValidationErrors(false);\n\n    // Clear form refs for the previous tool\n    formRefs.current = {};\n  }, [selectedTool, serverSupportsTaskRequests]);\n\n  const hasReservedMetadataEntry = metadataEntries.some(({ key }) => {\n    const trimmedKey = key.trim();\n    return trimmedKey !== \"\" && isReservedMetaKey(trimmedKey);\n  });\n\n  const hasInvalidMetaPrefixEntry = metadataEntries.some(({ key }) => {\n    const trimmedKey = key.trim();\n    return trimmedKey !== \"\" && !hasValidMetaPrefix(trimmedKey);\n  });\n\n  const hasInvalidMetaNameEntry = metadataEntries.some(({ key }) => {\n    const trimmedKey = key.trim();\n    return trimmedKey !== \"\" && !hasValidMetaName(trimmedKey);\n  });\n\n  const taskSupport = serverSupportsTaskRequests\n    ? getTaskSupport(selectedTool)\n    : \"forbidden\";\n\n  return (\n    <TabsContent value=\"tools\">\n      <div className=\"grid grid-cols-2 gap-4\">\n        <ListPane\n          items={tools}\n          listItems={listTools}\n          clearItems={() => {\n            clearTools();\n            setSelectedTool(null);\n            setRunAsTask(false);\n          }}\n          setSelectedItem={setSelectedTool}\n          renderItem={(tool) => (\n            <div className=\"flex items-start w-full gap-2\">\n              <div className=\"flex-shrink-0 mt-1\">\n                <IconDisplay icons={(tool as ExtendedTool).icons} size=\"sm\" />\n              </div>\n              <div className=\"flex flex-col flex-1 min-w-0\">\n                <span className=\"truncate\">{tool.title || tool.name}</span>\n                <span className=\"text-sm text-gray-500 text-left line-clamp-2\">\n                  {tool.description}\n                </span>\n              </div>\n              <ChevronRight className=\"w-4 h-4 flex-shrink-0 text-gray-400 mt-1\" />\n            </div>\n          )}\n          title=\"Tools\"\n          buttonText={nextCursor ? \"List More Tools\" : \"List Tools\"}\n          isButtonDisabled={!nextCursor && tools.length > 0}\n        />\n\n        <div className=\"bg-card border border-border rounded-lg shadow\">\n          <div className=\"p-4 border-b border-gray-200 dark:border-border\">\n            <div className=\"flex items-center gap-2\">\n              {selectedTool && (\n                <IconDisplay\n                  icons={(selectedTool as ExtendedTool).icons}\n                  size=\"md\"\n                />\n              )}\n              <h3 className=\"font-semibold\">\n                {selectedTool\n                  ? selectedTool.title || selectedTool.name\n                  : \"Select a tool\"}\n              </h3>\n            </div>\n          </div>\n          <div className=\"p-4\">\n            {selectedTool ? (\n              <div className=\"space-y-4\">\n                {error && (\n                  <Alert variant=\"destructive\">\n                    <AlertCircle className=\"h-4 w-4\" />\n                    <AlertTitle>Error</AlertTitle>\n                    <AlertDescription className=\"break-all\">\n                      {error}\n                    </AlertDescription>\n                  </Alert>\n                )}\n                <p className=\"text-sm text-gray-600 dark:text-gray-400 whitespace-pre-wrap max-h-48 overflow-y-auto\">\n                  {selectedTool.description}\n                </p>\n                <AnnotationBadges\n                  annotations={\n                    hasAnnotations(selectedTool)\n                      ? selectedTool.annotations\n                      : undefined\n                  }\n                />\n                {Object.entries(selectedTool.inputSchema.properties ?? []).map(\n                  ([key, value]) => {\n                    // First resolve any $ref references\n                    const resolvedValue = resolveRef(\n                      value as JsonSchemaType,\n                      selectedTool.inputSchema as JsonSchemaType,\n                    );\n                    const prop = normalizeUnionType(resolvedValue);\n                    const inputSchema =\n                      selectedTool.inputSchema as JsonSchemaType;\n                    const required = isPropertyRequired(key, inputSchema);\n                    return (\n                      <div key={key}>\n                        <div className=\"flex justify-between\">\n                          <Label\n                            htmlFor={key}\n                            className=\"block text-sm font-medium text-gray-700 dark:text-gray-300\"\n                          >\n                            {key}\n                            {required && (\n                              <span className=\"text-red-500 ml-1\">*</span>\n                            )}\n                          </Label>\n                          {prop.nullable ? (\n                            <div className=\"flex items-center space-x-2\">\n                              <Checkbox\n                                id={key}\n                                name={key}\n                                checked={params[key] === null}\n                                onCheckedChange={(checked: boolean) =>\n                                  setParams({\n                                    ...params,\n                                    [key]: checked\n                                      ? null\n                                      : prop.type === \"array\"\n                                        ? undefined\n                                        : prop.default !== null\n                                          ? prop.default\n                                          : prop.type === \"boolean\"\n                                            ? false\n                                            : prop.type === \"string\"\n                                              ? \"\"\n                                              : prop.type === \"number\" ||\n                                                  prop.type === \"integer\"\n                                                ? undefined\n                                                : undefined,\n                                  })\n                                }\n                              />\n                              <label\n                                htmlFor={key}\n                                className=\"text-sm font-medium text-gray-700 dark:text-gray-300\"\n                              >\n                                null\n                              </label>\n                            </div>\n                          ) : null}\n                        </div>\n\n                        <div\n                          role=\"toolinputwrapper\"\n                          className={`${prop.nullable && params[key] === null ? \"pointer-events-none opacity-50\" : \"\"}`}\n                        >\n                          {prop.type === \"boolean\" ? (\n                            <div className=\"flex items-center space-x-2 mt-2\">\n                              <Checkbox\n                                id={key}\n                                name={key}\n                                checked={!!params[key]}\n                                onCheckedChange={(checked: boolean) =>\n                                  setParams({\n                                    ...params,\n                                    [key]: checked,\n                                  })\n                                }\n                              />\n                              <label\n                                htmlFor={key}\n                                className=\"text-sm font-medium text-gray-700 dark:text-gray-300\"\n                              >\n                                {prop.description || \"Toggle this option\"}\n                              </label>\n                            </div>\n                          ) : prop.type === \"string\" && prop.enum ? (\n                            <Select\n                              value={\n                                params[key] === undefined\n                                  ? \"\"\n                                  : String(params[key])\n                              }\n                              onValueChange={(value) => {\n                                if (value === \"\") {\n                                  setParams({\n                                    ...params,\n                                    [key]: undefined,\n                                  });\n                                } else {\n                                  setParams({\n                                    ...params,\n                                    [key]: value,\n                                  });\n                                }\n                              }}\n                            >\n                              <SelectTrigger id={key} className=\"mt-1\">\n                                <SelectValue\n                                  placeholder={\n                                    prop.description || \"Select an option\"\n                                  }\n                                />\n                              </SelectTrigger>\n                              <SelectContent>\n                                {prop.enum.map((option) => (\n                                  <SelectItem key={option} value={option}>\n                                    {option}\n                                  </SelectItem>\n                                ))}\n                              </SelectContent>\n                            </Select>\n                          ) : prop.type === \"string\" ? (\n                            <Textarea\n                              id={key}\n                              name={key}\n                              placeholder={prop.description}\n                              value={\n                                params[key] === undefined\n                                  ? \"\"\n                                  : String(params[key])\n                              }\n                              onChange={(e) => {\n                                const value = e.target.value;\n                                if (value === \"\") {\n                                  // Field cleared - set to undefined\n                                  setParams({\n                                    ...params,\n                                    [key]: undefined,\n                                  });\n                                } else {\n                                  // Field has value - keep as string\n                                  setParams({\n                                    ...params,\n                                    [key]: value,\n                                  });\n                                }\n                              }}\n                              className=\"mt-1\"\n                            />\n                          ) : prop.type === \"object\" ||\n                            prop.type === \"array\" ? (\n                            <div className=\"mt-1\">\n                              <DynamicJsonForm\n                                ref={(ref) => (formRefs.current[key] = ref)}\n                                schema={{\n                                  type: prop.type,\n                                  properties: prop.properties,\n                                  description: prop.description,\n                                  items: prop.items,\n                                }}\n                                value={\n                                  (params[key] as JsonValue) ??\n                                  generateDefaultValue(prop)\n                                }\n                                onChange={(newValue: JsonValue) => {\n                                  setParams({\n                                    ...params,\n                                    [key]: newValue,\n                                  });\n                                  // Check validation after a short delay to allow form to update\n                                  setTimeout(checkValidationErrors, 100);\n                                }}\n                              />\n                            </div>\n                          ) : prop.type === \"number\" ||\n                            prop.type === \"integer\" ? (\n                            <Input\n                              type=\"number\"\n                              id={key}\n                              name={key}\n                              placeholder={prop.description}\n                              value={\n                                params[key] === undefined\n                                  ? \"\"\n                                  : String(params[key])\n                              }\n                              onChange={(e) => {\n                                const value = e.target.value;\n                                if (value === \"\") {\n                                  // Field cleared - set to undefined\n                                  setParams({\n                                    ...params,\n                                    [key]: undefined,\n                                  });\n                                } else {\n                                  // Field has value - try to convert to number, but store input either way\n                                  const num = Number(value);\n                                  if (!isNaN(num)) {\n                                    setParams({\n                                      ...params,\n                                      [key]: num,\n                                    });\n                                  } else {\n                                    // Store invalid input as string - let server validate\n                                    setParams({\n                                      ...params,\n                                      [key]: value,\n                                    });\n                                  }\n                                }\n                              }}\n                              className=\"mt-1\"\n                            />\n                          ) : (\n                            <div className=\"mt-1\">\n                              <DynamicJsonForm\n                                ref={(ref) => (formRefs.current[key] = ref)}\n                                schema={{\n                                  type: prop.type,\n                                  properties: prop.properties,\n                                  description: prop.description,\n                                  items: prop.items,\n                                }}\n                                value={params[key] as JsonValue}\n                                onChange={(newValue: JsonValue) => {\n                                  setParams({\n                                    ...params,\n                                    [key]: newValue,\n                                  });\n                                  // Check validation after a short delay to allow form to update\n                                  setTimeout(checkValidationErrors, 100);\n                                }}\n                              />\n                            </div>\n                          )}\n                        </div>\n                      </div>\n                    );\n                  },\n                )}\n                <div className=\"pb-4\">\n                  <div className=\"flex items-center justify-between mb-2\">\n                    <h4 className=\"text-sm font-semibold\">\n                      Tool-specific Metadata:\n                    </h4>\n                    <Button\n                      size=\"sm\"\n                      variant=\"outline\"\n                      className=\"h-6 px-2\"\n                      onClick={() =>\n                        setMetadataEntries((prev) => [\n                          ...prev,\n                          {\n                            id:\n                              (\n                                globalThis as unknown as {\n                                  crypto?: { randomUUID?: () => string };\n                                }\n                              ).crypto?.randomUUID?.() ||\n                              Math.random().toString(36).slice(2),\n                            key: \"\",\n                            value: \"\",\n                          },\n                        ])\n                      }\n                    >\n                      Add Pair\n                    </Button>\n                  </div>\n                  {metadataEntries.length === 0 ? (\n                    <p className=\"text-xs text-muted-foreground\">\n                      No metadata pairs.\n                    </p>\n                  ) : (\n                    <div className=\"space-y-2\">\n                      {metadataEntries.map((entry, index) => {\n                        const trimmedKey = entry.key.trim();\n                        const hasInvalidPrefix =\n                          trimmedKey !== \"\" && !hasValidMetaPrefix(trimmedKey);\n                        const isReservedKey =\n                          trimmedKey !== \"\" && isReservedMetaKey(trimmedKey);\n                        const hasInvalidName =\n                          trimmedKey !== \"\" && !hasValidMetaName(trimmedKey);\n                        const validationMessage = hasInvalidPrefix\n                          ? META_PREFIX_RULES_MESSAGE\n                          : isReservedKey\n                            ? RESERVED_NAMESPACE_MESSAGE\n                            : hasInvalidName\n                              ? META_NAME_RULES_MESSAGE\n                              : null;\n                        return (\n                          <div key={entry.id} className=\"space-y-1\">\n                            <div className=\"flex items-center gap-2 w-full\">\n                              <Label\n                                htmlFor={`metadata-key-${entry.id}`}\n                                className=\"text-xs shrink-0\"\n                              >\n                                Key\n                              </Label>\n                              <Input\n                                id={`metadata-key-${entry.id}`}\n                                value={entry.key}\n                                placeholder=\"e.g. requestId\"\n                                onChange={(e) => {\n                                  const value = e.target.value;\n                                  setMetadataEntries((prev) =>\n                                    prev.map((m, i) =>\n                                      i === index ? { ...m, key: value } : m,\n                                    ),\n                                  );\n                                }}\n                                className={cn(\n                                  \"h-8 flex-1\",\n                                  validationMessage &&\n                                    \"border-red-500 focus-visible:ring-red-500 focus-visible:ring-1\",\n                                )}\n                                aria-invalid={Boolean(validationMessage)}\n                              />\n                              <Label\n                                htmlFor={`metadata-value-${entry.id}`}\n                                className=\"text-xs shrink-0\"\n                              >\n                                Value\n                              </Label>\n                              <Input\n                                id={`metadata-value-${entry.id}`}\n                                value={entry.value}\n                                placeholder=\"e.g. 12345\"\n                                onChange={(e) => {\n                                  const value = e.target.value;\n                                  setMetadataEntries((prev) =>\n                                    prev.map((m, i) =>\n                                      i === index ? { ...m, value } : m,\n                                    ),\n                                  );\n                                }}\n                                className=\"h-8 flex-1\"\n                                disabled={Boolean(validationMessage)}\n                              />\n                              <Button\n                                size=\"sm\"\n                                variant=\"ghost\"\n                                className=\"h-8 w-8 p-0 ml-auto shrink-0\"\n                                onClick={() =>\n                                  setMetadataEntries((prev) =>\n                                    prev.filter((_, i) => i !== index),\n                                  )\n                                }\n                                aria-label={`Remove meta pair ${index + 1}`}\n                              >\n                                -\n                              </Button>\n                            </div>\n                            {validationMessage && (\n                              <p className=\"text-xs text-red-600 dark:text-red-400\">\n                                {validationMessage}\n                              </p>\n                            )}\n                          </div>\n                        );\n                      })}\n                    </div>\n                  )}\n                  {(hasReservedMetadataEntry ||\n                    hasInvalidMetaPrefixEntry ||\n                    hasInvalidMetaNameEntry) && (\n                    <p className=\"text-xs text-red-600 dark:text-red-400\">\n                      Remove reserved or invalid metadata keys (prefix/name)\n                      before running the tool.\n                    </p>\n                  )}\n                </div>\n                {selectedTool.outputSchema && (\n                  <div className=\"bg-gray-50 dark:bg-gray-900 p-3 rounded-lg\">\n                    <div className=\"flex items-center justify-between mb-2\">\n                      <h4 className=\"text-sm font-semibold\">Output Schema:</h4>\n                      <Button\n                        size=\"sm\"\n                        variant=\"ghost\"\n                        onClick={() =>\n                          setIsOutputSchemaExpanded(!isOutputSchemaExpanded)\n                        }\n                        className=\"h-6 px-2\"\n                      >\n                        {isOutputSchemaExpanded ? (\n                          <>\n                            <ChevronUp className=\"h-3 w-3 mr-1\" />\n                            Collapse\n                          </>\n                        ) : (\n                          <>\n                            <ChevronDown className=\"h-3 w-3 mr-1\" />\n                            Expand\n                          </>\n                        )}\n                      </Button>\n                    </div>\n                    <div\n                      className={`transition-all ${\n                        isOutputSchemaExpanded\n                          ? \"\"\n                          : \"max-h-[8rem] overflow-y-auto\"\n                      }`}\n                    >\n                      <JsonView data={selectedTool.outputSchema} />\n                    </div>\n                  </div>\n                )}\n                {selectedTool &&\n                  hasMeta(selectedTool) &&\n                  selectedTool._meta && (\n                    <div className=\"bg-gray-50 dark:bg-gray-900 p-3 rounded-lg\">\n                      <div className=\"flex items-center justify-between mb-2\">\n                        <h4 className=\"text-sm font-semibold\">Meta:</h4>\n                        <Button\n                          size=\"sm\"\n                          variant=\"ghost\"\n                          onClick={() =>\n                            setIsMetadataExpanded(!isMetadataExpanded)\n                          }\n                          className=\"h-6 px-2\"\n                        >\n                          {isMetadataExpanded ? (\n                            <>\n                              <ChevronUp className=\"h-3 w-3 mr-1\" />\n                              Collapse\n                            </>\n                          ) : (\n                            <>\n                              <ChevronDown className=\"h-3 w-3 mr-1\" />\n                              Expand\n                            </>\n                          )}\n                        </Button>\n                      </div>\n                      <div\n                        className={`transition-all ${\n                          isMetadataExpanded\n                            ? \"\"\n                            : \"max-h-[8rem] overflow-y-auto\"\n                        }`}\n                      >\n                        <JsonView data={selectedTool._meta} />\n                      </div>\n                    </div>\n                  )}\n                {taskSupport !== \"forbidden\" && (\n                  <div className=\"flex items-center space-x-2\">\n                    <Checkbox\n                      id=\"run-as-task\"\n                      checked={runAsTask}\n                      onCheckedChange={(checked: boolean) =>\n                        setRunAsTask(checked)\n                      }\n                      disabled={taskSupport === \"required\"}\n                    />\n                    <Label\n                      htmlFor=\"run-as-task\"\n                      className=\"text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer\"\n                    >\n                      Run as task\n                    </Label>\n                  </div>\n                )}\n                <Button\n                  onClick={async () => {\n                    // Validate JSON inputs before calling tool\n                    if (checkValidationErrors(true)) return;\n\n                    try {\n                      setIsToolRunning(true);\n                      const metadata = metadataEntries.reduce<\n                        Record<string, unknown>\n                      >((acc, { key, value }) => {\n                        const trimmedKey = key.trim();\n                        if (\n                          trimmedKey !== \"\" &&\n                          hasValidMetaPrefix(trimmedKey) &&\n                          !isReservedMetaKey(trimmedKey) &&\n                          hasValidMetaName(trimmedKey)\n                        ) {\n                          acc[trimmedKey] = value;\n                        }\n                        return acc;\n                      }, {});\n                      await callTool(\n                        selectedTool.name,\n                        params,\n                        Object.keys(metadata).length ? metadata : undefined,\n                        runAsTask,\n                      );\n                    } finally {\n                      setIsToolRunning(false);\n                    }\n                  }}\n                  disabled={\n                    isToolRunning ||\n                    isPollingTask ||\n                    hasValidationErrors ||\n                    hasReservedMetadataEntry ||\n                    hasInvalidMetaPrefixEntry ||\n                    hasInvalidMetaNameEntry\n                  }\n                >\n                  {isToolRunning || isPollingTask ? (\n                    <>\n                      <Loader2 className=\"w-4 h-4 mr-2 animate-spin\" />\n                      {isPollingTask ? \"Polling Task...\" : \"Running...\"}\n                    </>\n                  ) : (\n                    <>\n                      <Send className=\"w-4 h-4 mr-2\" />\n                      Run Tool\n                    </>\n                  )}\n                </Button>\n                <div className=\"flex gap-2\">\n                  <Button\n                    onClick={async () => {\n                      try {\n                        navigator.clipboard.writeText(\n                          JSON.stringify(params, null, 2),\n                        );\n                        setCopied(true);\n                      } catch (error) {\n                        toast({\n                          title: \"Error\",\n                          description: `There was an error copying input to the clipboard: ${error instanceof Error ? error.message : String(error)}`,\n                          variant: \"destructive\",\n                        });\n                      }\n                    }}\n                  >\n                    {copied ? (\n                      <CheckCheck className=\"h-4 w-4 mr-2 dark:text-green-700 text-green-600\" />\n                    ) : (\n                      <Copy className=\"h-4 w-4 mr-2\" />\n                    )}\n                    Copy Input\n                  </Button>\n                </div>\n                <ToolResults\n                  toolResult={toolResult}\n                  selectedTool={selectedTool}\n                  resourceContent={resourceContent}\n                  onReadResource={onReadResource}\n                  isPollingTask={isPollingTask}\n                />\n              </div>\n            ) : (\n              <Alert>\n                <AlertDescription>\n                  Select a tool from the list to view its details and run it\n                </AlertDescription>\n              </Alert>\n            )}\n          </div>\n        </div>\n      </div>\n    </TabsContent>\n  );\n};\n\nexport default ToolsTab;\n"
  },
  {
    "path": "client/src/components/__tests__/AppRenderer.test.tsx",
    "content": "import { render, screen, fireEvent, waitFor } from \"@testing-library/react\";\nimport \"@testing-library/jest-dom\";\nimport { describe, it, jest, beforeEach } from \"@jest/globals\";\nimport AppRenderer from \"../AppRenderer\";\nimport {\n  Tool,\n  CompatibilityCallToolResult,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { RequestHandlerExtra } from \"@mcp-ui/client\";\nimport { McpUiMessageResult } from \"@modelcontextprotocol/ext-apps\";\n\ntype BridgeEvent = {\n  type: \"sendToolInput\" | \"sendToolResult\";\n  toolName: string;\n  payload: unknown;\n};\n\ntype MockMcpUiRendererProps = {\n  toolName: string;\n  toolInput?: Record<string, unknown>;\n  toolResult?: CompatibilityCallToolResult;\n  onMessage?: (\n    params: { role: \"user\"; content: { type: \"text\"; text: string }[] },\n    extra: RequestHandlerExtra,\n  ) => Promise<McpUiMessageResult>;\n};\n\nconst mockBridgeEvents: BridgeEvent[] = [];\n\n// Mock the ext-apps module\njest.mock(\"@modelcontextprotocol/ext-apps/app-bridge\", () => ({\n  getToolUiResourceUri: (tool: Tool) => {\n    const meta = (tool as Tool & { _meta?: { ui?: { resourceUri?: string } } })\n      ._meta;\n    return meta?.ui?.resourceUri || null;\n  },\n}));\n\n// Mock toast hook\nconst mockToast = jest.fn();\njest.mock(\"@/lib/hooks/useToast\", () => ({\n  useToast: () => ({\n    toast: mockToast,\n  }),\n}));\n\n// Mock @mcp-ui/client to simulate bridge delivery only after app initialization\njest.mock(\"@mcp-ui/client\", () => {\n  const React = jest.requireActual(\"react\") as typeof import(\"react\");\n\n  return {\n    AppRenderer: ({\n      toolName,\n      toolInput,\n      toolResult,\n      onMessage,\n    }: MockMcpUiRendererProps) => {\n      const [isInitialized, setIsInitialized] = React.useState(false);\n\n      React.useEffect(() => {\n        setIsInitialized(false);\n      }, [toolName]);\n\n      React.useEffect(() => {\n        if (isInitialized && toolInput) {\n          mockBridgeEvents.push({\n            type: \"sendToolInput\",\n            toolName,\n            payload: toolInput,\n          });\n        }\n      }, [isInitialized, toolInput, toolName]);\n\n      React.useEffect(() => {\n        if (isInitialized && toolResult) {\n          mockBridgeEvents.push({\n            type: \"sendToolResult\",\n            toolName,\n            payload: toolResult,\n          });\n        }\n      }, [isInitialized, toolResult, toolName]);\n\n      return (\n        <div data-testid=\"mcp-ui-app-renderer\">\n          <div data-testid=\"tool-name\">{toolName}</div>\n          <div data-testid=\"tool-result\">\n            {JSON.stringify(toolResult ?? null)}\n          </div>\n          <button\n            data-testid=\"initialize-app\"\n            onClick={() => setIsInitialized(true)}\n          >\n            Initialize App\n          </button>\n          <button\n            data-testid=\"trigger-message\"\n            onClick={() =>\n              onMessage?.(\n                {\n                  role: \"user\",\n                  content: [{ type: \"text\", text: \"Test message\" }],\n                },\n                {} as RequestHandlerExtra,\n              )\n            }\n          >\n            Trigger Message\n          </button>\n        </div>\n      );\n    },\n  };\n});\n\ndescribe(\"AppRenderer\", () => {\n  const mockTool: Tool = {\n    name: \"testApp\",\n    description: \"Test app with UI\",\n    inputSchema: {\n      type: \"object\" as const,\n      properties: {},\n    },\n    _meta: {\n      ui: {\n        resourceUri: \"ui://test-app\",\n      },\n    },\n  } as Tool & { _meta?: { ui?: { resourceUri?: string } } };\n\n  const mockMcpClient = {\n    getServerCapabilities: jest.fn().mockReturnValue({}),\n    setNotificationHandler: jest.fn(),\n  } as unknown as Client;\n\n  const defaultProps = {\n    sandboxPath: \"/sandbox\",\n    tool: mockTool,\n    mcpClient: mockMcpClient,\n    toolResult: null,\n  };\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n    mockBridgeEvents.length = 0;\n  });\n\n  it(\"should display waiting state when mcpClient is null\", () => {\n    render(<AppRenderer {...defaultProps} mcpClient={null} />);\n    expect(screen.getByText(/Waiting for MCP client/i)).toBeInTheDocument();\n  });\n\n  it(\"should render McpUiAppRenderer when client is ready\", () => {\n    render(<AppRenderer {...defaultProps} />);\n\n    expect(screen.getByTestId(\"mcp-ui-app-renderer\")).toBeInTheDocument();\n    expect(screen.getByTestId(\"tool-name\")).toHaveTextContent(\"testApp\");\n  });\n\n  it(\"should set minimum height on container\", () => {\n    render(<AppRenderer {...defaultProps} />);\n\n    const container = screen.getByTestId(\"mcp-ui-app-renderer\").parentElement;\n    expect(container).toHaveStyle({ minHeight: \"400px\" });\n  });\n\n  it(\"should show toast when onMessage is triggered\", () => {\n    render(<AppRenderer {...defaultProps} />);\n\n    fireEvent.click(screen.getByTestId(\"trigger-message\"));\n\n    expect(mockToast).toHaveBeenCalledWith({\n      description: \"Test message\",\n    });\n  });\n\n  it(\"should send provided tool input and tool result after app initialization\", async () => {\n    const result: CompatibilityCallToolResult = {\n      content: [{ type: \"text\", text: \"Budget initialized\" }],\n    };\n\n    render(\n      <AppRenderer\n        {...defaultProps}\n        toolInput={{ monthlyBudget: 2500 }}\n        toolResult={result}\n      />,\n    );\n\n    expect(mockBridgeEvents).toHaveLength(0);\n\n    fireEvent.click(screen.getByTestId(\"initialize-app\"));\n\n    await waitFor(() => {\n      const toolEvents = mockBridgeEvents.filter(\n        (event) => event.toolName === \"testApp\",\n      );\n      expect(toolEvents.map((event) => event.type)).toEqual([\n        \"sendToolInput\",\n        \"sendToolResult\",\n      ]);\n    });\n\n    const resultEvent = mockBridgeEvents.find(\n      (event) =>\n        event.toolName === \"testApp\" && event.type === \"sendToolResult\",\n    );\n    expect(resultEvent?.payload).toEqual(result);\n  });\n\n  it(\"should not send tool result event when toolResult is null\", async () => {\n    render(\n      <AppRenderer\n        {...defaultProps}\n        toolInput={{ monthlyBudget: 2500 }}\n        toolResult={null}\n      />,\n    );\n\n    fireEvent.click(screen.getByTestId(\"initialize-app\"));\n\n    await waitFor(() => {\n      const inputEvents = mockBridgeEvents.filter(\n        (event) =>\n          event.toolName === \"testApp\" && event.type === \"sendToolInput\",\n      );\n      expect(inputEvents).toHaveLength(1);\n    });\n\n    const resultEvents = mockBridgeEvents.filter(\n      (event) =>\n        event.toolName === \"testApp\" && event.type === \"sendToolResult\",\n    );\n    expect(resultEvents).toHaveLength(0);\n  });\n\n  it(\"should normalize compatibility wrapper tool results before sending\", async () => {\n    const wrappedResult = {\n      toolResult: {\n        content: [{ type: \"text\", text: \"Wrapped result payload\" }],\n      },\n    } as CompatibilityCallToolResult;\n\n    render(\n      <AppRenderer\n        {...defaultProps}\n        toolInput={{ monthlyBudget: 2500 }}\n        toolResult={wrappedResult}\n      />,\n    );\n\n    fireEvent.click(screen.getByTestId(\"initialize-app\"));\n\n    await waitFor(() => {\n      const resultEvent = mockBridgeEvents.find(\n        (event) =>\n          event.toolName === \"testApp\" && event.type === \"sendToolResult\",\n      );\n      expect(resultEvent).toBeTruthy();\n      expect(resultEvent?.payload).toEqual(wrappedResult.toolResult);\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/components/__tests__/AppsTab.test.tsx",
    "content": "import {\n  render,\n  screen,\n  fireEvent,\n  waitFor,\n  act,\n} from \"@testing-library/react\";\nimport \"@testing-library/jest-dom\";\nimport { describe, it, jest, beforeEach } from \"@jest/globals\";\nimport AppsTab from \"../AppsTab\";\nimport {\n  Tool,\n  CompatibilityCallToolResult,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { Tabs } from \"../ui/tabs\";\n\n// Mock AppRenderer component\njest.mock(\"../AppRenderer\", () => {\n  return function MockAppRenderer({\n    tool,\n    toolInput,\n    toolResult,\n  }: {\n    tool: Tool;\n    toolInput?: Record<string, unknown>;\n    toolResult?: unknown;\n  }) {\n    return (\n      <div data-testid=\"app-renderer\">\n        <div>Tool: {tool.name}</div>\n        <div data-testid=\"tool-input\">{JSON.stringify(toolInput)}</div>\n        <div data-testid=\"tool-result\">\n          {JSON.stringify(toolResult ?? null)}\n        </div>\n      </div>\n    );\n  };\n});\n\ndescribe(\"AppsTab\", () => {\n  const mockAppTool: Tool = {\n    name: \"weatherApp\",\n    description: \"Weather app with UI\",\n    inputSchema: {\n      type: \"object\" as const,\n      properties: {},\n    },\n    _meta: {\n      ui: {\n        resourceUri: \"ui://weather-app\",\n      },\n    },\n  } as Tool & { _meta?: { ui?: { resourceUri?: string } } };\n\n  const mockRegularTool: Tool = {\n    name: \"regularTool\",\n    description: \"Regular tool without UI\",\n    inputSchema: {\n      type: \"object\" as const,\n      properties: {},\n    },\n  };\n\n  const defaultProps = {\n    tools: [],\n    listTools: jest.fn(),\n    callTool: jest.fn(\n      async () =>\n        ({\n          content: [],\n        }) as CompatibilityCallToolResult,\n    ),\n    error: null,\n    mcpClient: null,\n  };\n\n  const renderAppsTab = (props = {}) => {\n    return render(\n      <Tabs defaultValue=\"apps\">\n        <AppsTab {...defaultProps} {...props} />\n      </Tabs>,\n    );\n  };\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n  });\n\n  it(\"should display message when no apps are available\", () => {\n    renderAppsTab();\n\n    expect(screen.getByText(/No MCP Apps available/i)).toBeInTheDocument();\n    expect(screen.getByText(/_meta\\.ui\\.resourceUri/)).toBeInTheDocument();\n  });\n\n  it(\"should filter and display only tools with UI metadata\", () => {\n    renderAppsTab({\n      tools: [mockAppTool, mockRegularTool],\n    });\n\n    // Should show the app tool\n    expect(screen.getByText(\"weatherApp\")).toBeInTheDocument();\n    expect(screen.getByText(\"Weather app with UI\")).toBeInTheDocument();\n\n    // Should not show the regular tool\n    expect(screen.queryByText(\"regularTool\")).not.toBeInTheDocument();\n  });\n\n  it(\"should display multiple app tools in a grid\", () => {\n    const mockAppTool2: Tool = {\n      name: \"calendarApp\",\n      description: \"Calendar app with UI\",\n      inputSchema: {\n        type: \"object\" as const,\n        properties: {},\n      },\n      _meta: {\n        ui: {\n          resourceUri: \"ui://calendar-app\",\n        },\n      },\n    } as Tool & { _meta?: { ui?: { resourceUri?: string } } };\n\n    renderAppsTab({\n      tools: [mockAppTool, mockAppTool2],\n    });\n\n    expect(screen.getByText(\"weatherApp\")).toBeInTheDocument();\n    expect(screen.getByText(\"calendarApp\")).toBeInTheDocument();\n  });\n\n  it(\"should call listTools when refresh button is clicked\", () => {\n    const mockListTools = jest.fn();\n    renderAppsTab({\n      tools: [mockAppTool],\n      listTools: mockListTools,\n    });\n\n    const refreshButton = screen.getByRole(\"button\", { name: /refresh/i });\n    fireEvent.click(refreshButton);\n\n    expect(mockListTools).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"should display error message when error prop is provided\", () => {\n    const errorMessage = \"Failed to fetch tools\";\n    renderAppsTab({\n      error: errorMessage,\n    });\n\n    expect(screen.getByText(errorMessage)).toBeInTheDocument();\n  });\n\n  it(\"should open app renderer when an app card is clicked and Open App button is clicked if fields exist\", async () => {\n    const toolWithFields: Tool = {\n      name: \"fieldsApp\",\n      inputSchema: {\n        type: \"object\",\n        properties: {\n          field1: { type: \"string\" },\n        },\n      },\n      _meta: { ui: { resourceUri: \"ui://fields\" } },\n    } as Tool & { _meta?: { ui?: { resourceUri?: string } } };\n    const mockCallTool = jest.fn(\n      async () =>\n        ({\n          content: [],\n        }) as CompatibilityCallToolResult,\n    );\n\n    renderAppsTab({\n      tools: [toolWithFields],\n      callTool: mockCallTool,\n    });\n\n    const appCard = screen.getByText(\"fieldsApp\").closest(\"div\");\n    expect(appCard).toBeTruthy();\n    fireEvent.click(appCard!);\n\n    // Should see the input form now, not the renderer yet\n    expect(screen.queryByTestId(\"app-renderer\")).not.toBeInTheDocument();\n    expect(screen.getByText(\"App Input\")).toBeInTheDocument();\n\n    // Click Open App button\n    const openAppButton = screen.getByRole(\"button\", { name: /open app/i });\n    fireEvent.click(openAppButton);\n\n    await waitFor(() => {\n      expect(mockCallTool).toHaveBeenCalledWith(\"fieldsApp\", {\n        field1: undefined,\n      });\n    });\n\n    await waitFor(() => {\n      expect(screen.getByTestId(\"app-renderer\")).toBeInTheDocument();\n    });\n    expect(screen.getByText(\"Tool: fieldsApp\")).toBeInTheDocument();\n  });\n\n  it(\"should close app renderer when close button is clicked\", async () => {\n    renderAppsTab({\n      tools: [mockAppTool],\n    });\n\n    // Open the app\n    const appCard = screen.getByText(\"weatherApp\").closest(\"div\");\n    fireEvent.click(appCard!);\n    await waitFor(() => {\n      expect(screen.getByTestId(\"app-renderer\")).toBeInTheDocument();\n    });\n\n    // Close the app (Deselect tool)\n    const closeButton = screen.getByRole(\"button\", { name: /close app/i });\n    fireEvent.click(closeButton);\n\n    // AppRenderer should be removed\n    expect(screen.queryByTestId(\"app-renderer\")).not.toBeInTheDocument();\n  });\n\n  it(\"should handle tool without description\", () => {\n    const toolWithoutDescription: Tool = {\n      name: \"noDescriptionApp\",\n      inputSchema: {\n        type: \"object\" as const,\n        properties: {},\n      },\n      _meta: {\n        ui: {\n          resourceUri: \"ui://no-description-app\",\n        },\n      },\n    } as Tool & { _meta?: { ui?: { resourceUri?: string } } };\n\n    renderAppsTab({\n      tools: [toolWithoutDescription],\n    });\n\n    expect(screen.getByText(\"noDescriptionApp\")).toBeInTheDocument();\n  });\n\n  it(\"should reset selected tool when tools list changes and selected tool is removed\", async () => {\n    const { rerender } = renderAppsTab({\n      tools: [mockAppTool],\n    });\n\n    // Select the app\n    const appCard = screen.getByText(\"weatherApp\").closest(\"div\");\n    fireEvent.click(appCard!);\n    await waitFor(() => {\n      expect(screen.getByTestId(\"app-renderer\")).toBeInTheDocument();\n    });\n\n    // Update tools list to remove the selected tool\n    rerender(\n      <Tabs defaultValue=\"apps\">\n        <AppsTab {...defaultProps} tools={[]} />\n      </Tabs>,\n    );\n\n    // AppRenderer should be removed\n    expect(screen.queryByTestId(\"app-renderer\")).not.toBeInTheDocument();\n  });\n\n  it(\"should maintain selected tool when tools list updates but includes the same tool\", async () => {\n    const { rerender } = renderAppsTab({\n      tools: [mockAppTool],\n    });\n\n    // Select the app\n    const appCard = screen.getByText(\"weatherApp\").closest(\"div\");\n    fireEvent.click(appCard!);\n    await waitFor(() => {\n      expect(screen.getByTestId(\"app-renderer\")).toBeInTheDocument();\n    });\n\n    // Update tools list with the same tool\n    rerender(\n      <Tabs defaultValue=\"apps\">\n        <AppsTab {...defaultProps} tools={[mockAppTool]} />\n      </Tabs>,\n    );\n\n    // AppRenderer should still be rendered\n    expect(screen.getByTestId(\"app-renderer\")).toBeInTheDocument();\n  });\n\n  it(\"should maximize and minimize the app window\", async () => {\n    renderAppsTab({\n      tools: [mockAppTool],\n    });\n\n    // Select the app\n    const appCard = screen.getByText(\"weatherApp\").closest(\"div\");\n    fireEvent.click(appCard!);\n\n    await waitFor(() => {\n      expect(\n        screen.getByRole(\"button\", { name: /maximize/i }),\n      ).toBeInTheDocument();\n    });\n\n    // Initially, ListPane should be visible\n    expect(screen.getByText(\"MCP Apps\")).toBeInTheDocument();\n\n    // Click Maximize\n    const maximizeButton = screen.getByRole(\"button\", { name: /maximize/i });\n    fireEvent.click(maximizeButton);\n\n    // ListPane should be hidden\n    expect(screen.queryByText(\"MCP Apps\")).not.toBeInTheDocument();\n\n    // Click Minimize\n    const minimizeButton = screen.getByRole(\"button\", { name: /minimize/i });\n    fireEvent.click(minimizeButton);\n\n    // ListPane should be visible again\n    expect(screen.getByText(\"MCP Apps\")).toBeInTheDocument();\n  });\n\n  it(\"should reset maximized state when app is closed\", async () => {\n    renderAppsTab({\n      tools: [mockAppTool],\n    });\n\n    // Select the app\n    const appCard = screen.getByText(\"weatherApp\").closest(\"div\");\n    fireEvent.click(appCard!);\n\n    await waitFor(() => {\n      expect(\n        screen.getByRole(\"button\", { name: /maximize/i }),\n      ).toBeInTheDocument();\n    });\n\n    // Maximize\n    fireEvent.click(screen.getByRole(\"button\", { name: /maximize/i }));\n    expect(screen.queryByText(\"MCP Apps\")).not.toBeInTheDocument();\n\n    // Close app (deselect tool)\n    fireEvent.click(screen.getByRole(\"button\", { name: /close app/i }));\n\n    // ListPane should be visible again\n    expect(screen.getByText(\"MCP Apps\")).toBeInTheDocument();\n  });\n\n  it(\"should display app cards in the list\", () => {\n    renderAppsTab({\n      tools: [mockAppTool],\n    });\n\n    const appItem = screen.getByText(\"weatherApp\").closest(\"div\");\n    expect(appItem).toBeTruthy();\n  });\n\n  it(\"should handle empty resourceContentMap\", async () => {\n    renderAppsTab({\n      tools: [mockAppTool],\n    });\n\n    // Open the app\n    const appCard = screen.getByText(\"weatherApp\").closest(\"div\");\n    fireEvent.click(appCard!);\n\n    await waitFor(() => {\n      expect(screen.getByTestId(\"app-renderer\")).toBeInTheDocument();\n    });\n  });\n\n  it(\"should handle various input types and pass them to AppRenderer\", async () => {\n    const toolWithComplexSchema: Tool = {\n      name: \"complexApp\",\n      inputSchema: {\n        type: \"object\",\n        properties: {\n          text: { type: \"string\" },\n          toggle: { type: \"boolean\" },\n          number: { type: \"number\" },\n          choice: { type: \"string\", enum: [\"a\", \"b\"] },\n        },\n      },\n      _meta: { ui: { resourceUri: \"ui://complex\" } },\n    } as Tool & { _meta?: { ui?: { resourceUri?: string } } };\n\n    renderAppsTab({\n      tools: [toolWithComplexSchema],\n    });\n\n    fireEvent.click(screen.getByText(\"complexApp\"));\n\n    // Fill the form\n    fireEvent.change(screen.getByLabelText(\"text\"), {\n      target: { value: \"hello\" },\n    });\n    // Checkboxes are often rendered with a label next to them, let's look for the text\n    const toggleLabel = screen.getByText(/Toggle this option/i);\n    fireEvent.click(toggleLabel);\n    fireEvent.change(screen.getByLabelText(\"number\"), {\n      target: { value: \"42\" },\n    });\n\n    fireEvent.click(screen.getByRole(\"button\", { name: /open app/i }));\n\n    await waitFor(() => {\n      expect(screen.getByTestId(\"app-renderer\")).toBeInTheDocument();\n    });\n\n    const toolInput = JSON.parse(\n      screen.getByTestId(\"tool-input\").textContent || \"{}\",\n    );\n    expect(toolInput.text).toBe(\"hello\");\n    expect(toolInput.toggle).toBe(true);\n    expect(toolInput.number).toBe(42);\n  });\n\n  it(\"should handle nullable fields\", async () => {\n    const toolWithNullable: Tool = {\n      name: \"nullableApp\",\n      inputSchema: {\n        type: \"object\",\n        properties: {\n          nullableField: { type: [\"string\", \"null\"] },\n        },\n      },\n      _meta: { ui: { resourceUri: \"ui://nullable\" } },\n    } as Tool & { _meta?: { ui?: { resourceUri?: string } } };\n\n    renderAppsTab({\n      tools: [toolWithNullable],\n    });\n\n    fireEvent.click(screen.getByText(\"nullableApp\"));\n\n    // Check the 'null' checkbox\n    const nullCheckbox = screen.getByLabelText(\"null\");\n    fireEvent.click(nullCheckbox);\n\n    fireEvent.click(screen.getByRole(\"button\", { name: /open app/i }));\n\n    await waitFor(() => {\n      expect(screen.getByTestId(\"app-renderer\")).toBeInTheDocument();\n    });\n\n    const toolInput = JSON.parse(\n      screen.getByTestId(\"tool-input\").textContent || \"{}\",\n    );\n    expect(toolInput.nullableField).toBe(null);\n  });\n\n  it(\"should allow going back to input form from app renderer\", async () => {\n    const toolWithFields: Tool = {\n      name: \"fieldsApp\",\n      inputSchema: {\n        type: \"object\",\n        properties: {\n          field1: { type: \"string\" },\n        },\n      },\n      _meta: { ui: { resourceUri: \"ui://fields\" } },\n    } as Tool & { _meta?: { ui?: { resourceUri?: string } } };\n\n    renderAppsTab({\n      tools: [toolWithFields],\n    });\n\n    fireEvent.click(screen.getByText(\"fieldsApp\"));\n    fireEvent.click(screen.getByRole(\"button\", { name: /open app/i }));\n\n    await waitFor(() => {\n      expect(screen.getByTestId(\"app-renderer\")).toBeInTheDocument();\n    });\n\n    const backButton = screen.queryByRole(\"button\", { name: /back to input/i });\n    expect(backButton).toBeInTheDocument();\n  });\n\n  it(\"should skip input form if tool has no input fields\", async () => {\n    const toolNoFields: Tool = {\n      name: \"noFieldsApp\",\n      inputSchema: {\n        type: \"object\",\n        properties: {},\n      },\n      _meta: { ui: { resourceUri: \"ui://no-fields\" } },\n    } as Tool & { _meta?: { ui?: { resourceUri?: string } } };\n    const mockCallTool = jest.fn(\n      async () =>\n        ({\n          content: [],\n        }) as CompatibilityCallToolResult,\n    );\n\n    renderAppsTab({\n      tools: [toolNoFields],\n      callTool: mockCallTool,\n    });\n\n    fireEvent.click(screen.getByText(\"noFieldsApp\"));\n\n    await waitFor(() => {\n      expect(mockCallTool).toHaveBeenCalledWith(\"noFieldsApp\", {});\n    });\n\n    await waitFor(() => {\n      expect(screen.getByTestId(\"app-renderer\")).toBeInTheDocument();\n    });\n    expect(screen.getByText(\"Tool: noFieldsApp\")).toBeInTheDocument();\n\n    // Should NOT see the back button\n    expect(\n      screen.queryByRole(\"button\", { name: /back to input/i }),\n    ).not.toBeInTheDocument();\n  });\n\n  it(\"should allow going back to input form from app renderer if fields exist\", async () => {\n    const toolWithFields: Tool = {\n      name: \"fieldsApp\",\n      inputSchema: {\n        type: \"object\",\n        properties: {\n          field1: { type: \"string\" },\n        },\n      },\n      _meta: { ui: { resourceUri: \"ui://fields\" } },\n    } as Tool & { _meta?: { ui?: { resourceUri?: string } } };\n\n    renderAppsTab({\n      tools: [toolWithFields],\n    });\n\n    fireEvent.click(screen.getByText(\"fieldsApp\"));\n\n    // Should see input form first\n    expect(screen.getByText(\"App Input\")).toBeInTheDocument();\n    fireEvent.click(screen.getByRole(\"button\", { name: /open app/i }));\n\n    await waitFor(() => {\n      expect(screen.getByTestId(\"app-renderer\")).toBeInTheDocument();\n    });\n\n    const backButton = screen.getByRole(\"button\", { name: /back to input/i });\n    fireEvent.click(backButton);\n\n    expect(screen.queryByTestId(\"app-renderer\")).not.toBeInTheDocument();\n    expect(screen.getByText(\"App Input\")).toBeInTheDocument();\n  });\n\n  it(\"should auto-render app from prefilled tools tab call without manual click\", async () => {\n    const toolWithFields: Tool = {\n      name: \"prefilledApp\",\n      inputSchema: {\n        type: \"object\",\n        properties: {\n          city: { type: \"string\" },\n        },\n      },\n      _meta: { ui: { resourceUri: \"ui://prefilled\" } },\n    } as Tool & { _meta?: { ui?: { resourceUri?: string } } };\n    const onPrefilledToolCallConsumed = jest.fn();\n    const prefilledResult: CompatibilityCallToolResult = {\n      content: [{ type: \"text\", text: \"weather result\" }],\n    };\n\n    renderAppsTab({\n      tools: [toolWithFields],\n      prefilledToolCall: {\n        id: 42,\n        toolName: \"prefilledApp\",\n        params: { city: \"Lisbon\" },\n        result: prefilledResult,\n      },\n      onPrefilledToolCallConsumed,\n    });\n\n    await waitFor(() => {\n      expect(screen.getByTestId(\"app-renderer\")).toBeInTheDocument();\n      expect(screen.getByText(\"Tool: prefilledApp\")).toBeInTheDocument();\n    });\n\n    const toolInput = JSON.parse(\n      screen.getByTestId(\"tool-input\").textContent || \"{}\",\n    );\n    expect(toolInput).toEqual({ city: \"Lisbon\" });\n    expect(screen.getByTestId(\"tool-result\")).toHaveTextContent(\n      \"weather result\",\n    );\n    expect(onPrefilledToolCallConsumed).toHaveBeenCalledWith(42);\n  });\n\n  it(\"should not auto-render app when no prefilled tool call is provided\", () => {\n    const toolWithFields: Tool = {\n      name: \"manualApp\",\n      inputSchema: {\n        type: \"object\",\n        properties: {\n          city: { type: \"string\" },\n        },\n      },\n      _meta: { ui: { resourceUri: \"ui://manual\" } },\n    } as Tool & { _meta?: { ui?: { resourceUri?: string } } };\n\n    renderAppsTab({\n      tools: [toolWithFields],\n    });\n\n    expect(screen.queryByTestId(\"app-renderer\")).not.toBeInTheDocument();\n    expect(\n      screen.getByText(\"Select an app from the list to get started\"),\n    ).toBeInTheDocument();\n  });\n\n  it(\"should preserve submitted params while opening app\", async () => {\n    const toolWithFields: Tool = {\n      name: \"paramsApp\",\n      inputSchema: {\n        type: \"object\",\n        properties: {\n          query: { type: \"string\" },\n        },\n      },\n      _meta: { ui: { resourceUri: \"ui://params\" } },\n    } as Tool & { _meta?: { ui?: { resourceUri?: string } } };\n\n    let resolveCall: ((result: CompatibilityCallToolResult) => void) | null =\n      null;\n    const mockCallTool = jest.fn(\n      () =>\n        new Promise<CompatibilityCallToolResult>((resolve) => {\n          resolveCall = resolve;\n        }),\n    );\n\n    renderAppsTab({\n      tools: [toolWithFields],\n      callTool: mockCallTool,\n    });\n\n    fireEvent.click(screen.getByText(\"paramsApp\"));\n    fireEvent.change(screen.getByLabelText(\"query\"), {\n      target: { value: \"first value\" },\n    });\n    fireEvent.click(screen.getByRole(\"button\", { name: /open app/i }));\n\n    fireEvent.change(screen.getByLabelText(\"query\"), {\n      target: { value: \"second value\" },\n    });\n\n    expect(resolveCall).toBeTruthy();\n    await act(async () => {\n      resolveCall?.({\n        content: [{ type: \"text\", text: \"done\" }],\n      });\n    });\n\n    await waitFor(() => {\n      expect(screen.getByTestId(\"app-renderer\")).toBeInTheDocument();\n    });\n\n    const toolInput = JSON.parse(\n      screen.getByTestId(\"tool-input\").textContent || \"{}\",\n    );\n    expect(toolInput.query).toBe(\"first value\");\n  });\n\n  it(\"should keep input view when opening fails and recover button state\", async () => {\n    const toolWithFields: Tool = {\n      name: \"failingApp\",\n      inputSchema: {\n        type: \"object\",\n        properties: {\n          query: { type: \"string\" },\n        },\n      },\n      _meta: { ui: { resourceUri: \"ui://failing\" } },\n    } as Tool & { _meta?: { ui?: { resourceUri?: string } } };\n\n    const mockCallTool = jest.fn(\n      async () =>\n        await Promise.reject<CompatibilityCallToolResult>(\n          new Error(\"tool failed\"),\n        ),\n    );\n\n    renderAppsTab({\n      tools: [toolWithFields],\n      callTool: mockCallTool,\n    });\n\n    fireEvent.click(screen.getByText(\"failingApp\"));\n    fireEvent.click(screen.getByRole(\"button\", { name: /open app/i }));\n\n    await waitFor(() => {\n      expect(mockCallTool).toHaveBeenCalledWith(\"failingApp\", {\n        query: undefined,\n      });\n    });\n\n    await waitFor(() => {\n      const openAppButton = screen.getByRole(\"button\", { name: /open app/i });\n      expect(openAppButton).toBeEnabled();\n    });\n\n    expect(screen.getByText(\"App Input\")).toBeInTheDocument();\n    expect(screen.queryByTestId(\"app-renderer\")).not.toBeInTheDocument();\n  });\n\n  it(\"should ignore stale results from older app launches\", async () => {\n    const appA: Tool = {\n      name: \"appA\",\n      inputSchema: {\n        type: \"object\",\n        properties: {},\n      },\n      _meta: { ui: { resourceUri: \"ui://app-a\" } },\n    } as Tool & { _meta?: { ui?: { resourceUri?: string } } };\n    const appB: Tool = {\n      name: \"appB\",\n      inputSchema: {\n        type: \"object\",\n        properties: {},\n      },\n      _meta: { ui: { resourceUri: \"ui://app-b\" } },\n    } as Tool & { _meta?: { ui?: { resourceUri?: string } } };\n\n    const pendingCalls: {\n      name: string;\n      resolve: (result: CompatibilityCallToolResult) => void;\n    }[] = [];\n    const mockCallTool = jest.fn(\n      (name: string) =>\n        new Promise<CompatibilityCallToolResult>((resolve) => {\n          pendingCalls.push({ name, resolve });\n        }),\n    );\n\n    renderAppsTab({\n      tools: [appA, appB],\n      callTool: mockCallTool,\n    });\n\n    fireEvent.click(screen.getByText(\"appA\"));\n    fireEvent.click(screen.getByText(\"appB\"));\n\n    const appBCall = pendingCalls.find((call) => call.name === \"appB\");\n    const appACall = pendingCalls.find((call) => call.name === \"appA\");\n    expect(appBCall).toBeTruthy();\n    expect(appACall).toBeTruthy();\n\n    await act(async () => {\n      appBCall?.resolve({\n        content: [{ type: \"text\", text: \"result from appB\" }],\n      });\n    });\n\n    await waitFor(() => {\n      expect(screen.getByText(\"Tool: appB\")).toBeInTheDocument();\n      expect(screen.getByTestId(\"tool-result\")).toHaveTextContent(\"appB\");\n    });\n\n    await act(async () => {\n      appACall?.resolve({\n        content: [{ type: \"text\", text: \"result from appA\" }],\n      });\n    });\n\n    await waitFor(() => {\n      expect(screen.getByText(\"Tool: appB\")).toBeInTheDocument();\n      expect(screen.getByTestId(\"tool-result\")).toHaveTextContent(\"appB\");\n      expect(screen.getByTestId(\"tool-result\")).not.toHaveTextContent(\"appA\");\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/components/__tests__/AuthDebugger.test.tsx",
    "content": "import {\n  render,\n  screen,\n  fireEvent,\n  waitFor,\n  act,\n} from \"@testing-library/react\";\nimport \"@testing-library/jest-dom\";\nimport { describe, it, beforeEach, jest } from \"@jest/globals\";\nimport AuthDebugger, { AuthDebuggerProps } from \"../AuthDebugger\";\nimport { TooltipProvider } from \"../ui/tooltip\";\nimport { SESSION_KEYS } from \"../../lib/constants\";\n\nconst mockOAuthTokens = {\n  access_token: \"test_access_token\",\n  token_type: \"Bearer\",\n  expires_in: 3600,\n  refresh_token: \"test_refresh_token\",\n  scope: \"test_scope\",\n};\n\nconst mockOAuthMetadata = {\n  issuer: \"https://oauth.example.com\",\n  authorization_endpoint: \"https://oauth.example.com/authorize\",\n  token_endpoint: \"https://oauth.example.com/token\",\n  response_types_supported: [\"code\"],\n  grant_types_supported: [\"authorization_code\"],\n  scopes_supported: [\"read\", \"write\"],\n};\n\nconst mockOAuthClientInfo = {\n  client_id: \"test_client_id\",\n  client_secret: \"test_client_secret\",\n  redirect_uris: [\"http://localhost:3000/oauth/callback/debug\"],\n};\n\n// Mock MCP SDK functions - must be before imports\njest.mock(\"@modelcontextprotocol/sdk/client/auth.js\", () => ({\n  auth: jest.fn(),\n  discoverAuthorizationServerMetadata: jest.fn(),\n  registerClient: jest.fn(),\n  startAuthorization: jest.fn(),\n  exchangeAuthorization: jest.fn(),\n  discoverOAuthProtectedResourceMetadata: jest.fn(),\n  selectResourceURL: jest.fn(),\n}));\n\n// Import the functions to get their types\nimport {\n  discoverAuthorizationServerMetadata,\n  registerClient,\n  startAuthorization,\n  exchangeAuthorization,\n  auth,\n  discoverOAuthProtectedResourceMetadata,\n} from \"@modelcontextprotocol/sdk/client/auth.js\";\nimport { OAuthMetadata } from \"@modelcontextprotocol/sdk/shared/auth.js\";\nimport { EMPTY_DEBUGGER_STATE } from \"../../lib/auth-types\";\n\n// Mock local auth module\njest.mock(\"@/lib/auth\", () => ({\n  DebugInspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({\n    tokens: jest.fn().mockImplementation(() => Promise.resolve(undefined)),\n    clear: jest.fn().mockImplementation(() => {\n      // Mock the real clear() behavior which removes items from sessionStorage\n      sessionStorage.removeItem(\"[https://example.com/mcp] mcp_tokens\");\n      sessionStorage.removeItem(\"[https://example.com/mcp] mcp_client_info\");\n      sessionStorage.removeItem(\n        \"[https://example.com/mcp] mcp_server_metadata\",\n      );\n    }),\n    redirectUrl: \"http://localhost:3000/oauth/callback/debug\",\n    clientMetadata: {\n      redirect_uris: [\"http://localhost:3000/oauth/callback/debug\"],\n      token_endpoint_auth_method: \"none\",\n      grant_types: [\"authorization_code\", \"refresh_token\"],\n      response_types: [\"code\"],\n      client_name: \"MCP Inspector\",\n    },\n    clientInformation: jest.fn().mockImplementation(async () => {\n      const serverUrl = \"https://example.com/mcp\";\n      const preregisteredKey = `[${serverUrl}] ${SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION}`;\n      const preregisteredData = sessionStorage.getItem(preregisteredKey);\n      if (preregisteredData) {\n        return JSON.parse(preregisteredData);\n      }\n      const dynamicKey = `[${serverUrl}] ${SESSION_KEYS.CLIENT_INFORMATION}`;\n      const dynamicData = sessionStorage.getItem(dynamicKey);\n      if (dynamicData) {\n        return JSON.parse(dynamicData);\n      }\n      return undefined;\n    }),\n    saveClientInformation: jest.fn().mockImplementation((clientInfo) => {\n      const serverUrl = \"https://example.com/mcp\";\n      const key = `[${serverUrl}] ${SESSION_KEYS.CLIENT_INFORMATION}`;\n      sessionStorage.setItem(key, JSON.stringify(clientInfo));\n    }),\n    saveTokens: jest.fn(),\n    redirectToAuthorization: jest.fn(),\n    saveCodeVerifier: jest.fn(),\n    codeVerifier: jest.fn(),\n    saveServerMetadata: jest.fn(),\n    getServerMetadata: jest.fn(),\n  })),\n  discoverScopes: jest.fn().mockResolvedValue(\"read write\" as never),\n}));\n\nimport { discoverScopes } from \"../../lib/auth\";\n\n// Type the mocked functions properly\nconst mockDiscoverAuthorizationServerMetadata =\n  discoverAuthorizationServerMetadata as jest.MockedFunction<\n    typeof discoverAuthorizationServerMetadata\n  >;\nconst mockRegisterClient = registerClient as jest.MockedFunction<\n  typeof registerClient\n>;\nconst mockStartAuthorization = startAuthorization as jest.MockedFunction<\n  typeof startAuthorization\n>;\nconst mockExchangeAuthorization = exchangeAuthorization as jest.MockedFunction<\n  typeof exchangeAuthorization\n>;\nconst mockAuth = auth as jest.MockedFunction<typeof auth>;\nconst mockDiscoverOAuthProtectedResourceMetadata =\n  discoverOAuthProtectedResourceMetadata as jest.MockedFunction<\n    typeof discoverOAuthProtectedResourceMetadata\n  >;\nconst mockDiscoverScopes = discoverScopes as jest.MockedFunction<\n  typeof discoverScopes\n>;\n\nconst sessionStorageMock = {\n  getItem: jest.fn(),\n  setItem: jest.fn(),\n  removeItem: jest.fn(),\n  clear: jest.fn(),\n};\nObject.defineProperty(window, \"sessionStorage\", {\n  value: sessionStorageMock,\n});\n\ndescribe(\"AuthDebugger\", () => {\n  const defaultAuthState = EMPTY_DEBUGGER_STATE;\n\n  const defaultProps = {\n    serverUrl: \"https://example.com/mcp\",\n    onBack: jest.fn(),\n    authState: defaultAuthState,\n    updateAuthState: jest.fn(),\n  };\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n    sessionStorageMock.getItem.mockReturnValue(null);\n\n    // Suppress console errors in tests to avoid JSDOM navigation noise\n    jest.spyOn(console, \"error\").mockImplementation(() => {});\n\n    // Set default mock behaviors with complete OAuth metadata\n    mockDiscoverAuthorizationServerMetadata.mockResolvedValue({\n      issuer: \"https://oauth.example.com\",\n      authorization_endpoint: \"https://oauth.example.com/authorize\",\n      token_endpoint: \"https://oauth.example.com/token\",\n      response_types_supported: [\"code\"],\n      grant_types_supported: [\"authorization_code\"],\n      scopes_supported: [\"read\", \"write\"],\n    });\n    mockRegisterClient.mockResolvedValue(mockOAuthClientInfo);\n    mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(\n      new Error(\"No protected resource metadata found\"),\n    );\n    mockStartAuthorization.mockImplementation(async (_sseUrl, options) => {\n      const authUrl = new URL(\"https://oauth.example.com/authorize\");\n\n      if (options.scope) {\n        authUrl.searchParams.set(\"scope\", options.scope);\n      }\n\n      return {\n        authorizationUrl: authUrl,\n        codeVerifier: \"test_verifier\",\n      };\n    });\n    mockExchangeAuthorization.mockResolvedValue(mockOAuthTokens);\n  });\n\n  afterEach(() => {\n    jest.restoreAllMocks();\n  });\n\n  const renderAuthDebugger = (props: Partial<AuthDebuggerProps> = {}) => {\n    const mergedProps = {\n      ...defaultProps,\n      ...props,\n      authState: { ...defaultAuthState, ...(props.authState || {}) },\n    };\n    return render(\n      <TooltipProvider>\n        <AuthDebugger {...mergedProps} />\n      </TooltipProvider>,\n    );\n  };\n\n  describe(\"Initial Rendering\", () => {\n    it(\"should render the component with correct title\", async () => {\n      await act(async () => {\n        renderAuthDebugger();\n      });\n      expect(screen.getByText(\"Authentication Settings\")).toBeInTheDocument();\n    });\n\n    it(\"should call onBack when Back button is clicked\", async () => {\n      const onBack = jest.fn();\n      await act(async () => {\n        renderAuthDebugger({ onBack });\n      });\n      fireEvent.click(screen.getByText(\"Back to Connect\"));\n      expect(onBack).toHaveBeenCalled();\n    });\n  });\n\n  describe(\"OAuth Flow\", () => {\n    it(\"should start OAuth flow when 'Guided OAuth Flow' is clicked\", async () => {\n      await act(async () => {\n        renderAuthDebugger();\n      });\n\n      await act(async () => {\n        fireEvent.click(screen.getByText(\"Guided OAuth Flow\"));\n      });\n\n      expect(screen.getByText(\"OAuth Flow Progress\")).toBeInTheDocument();\n    });\n\n    it(\"should show error when OAuth flow is started without sseUrl\", async () => {\n      const updateAuthState = jest.fn();\n      await act(async () => {\n        renderAuthDebugger({ serverUrl: \"\", updateAuthState });\n      });\n\n      await act(async () => {\n        fireEvent.click(screen.getByText(\"Guided OAuth Flow\"));\n      });\n\n      expect(updateAuthState).toHaveBeenCalledWith({\n        statusMessage: {\n          type: \"error\",\n          message:\n            \"Please enter a server URL in the sidebar before authenticating\",\n        },\n      });\n    });\n\n    it(\"should start quick OAuth flow and properly fetch and save metadata\", async () => {\n      // Setup the auth mock\n      mockAuth.mockResolvedValue(\"AUTHORIZED\");\n\n      const updateAuthState = jest.fn();\n      await act(async () => {\n        renderAuthDebugger({ updateAuthState });\n      });\n\n      await act(async () => {\n        fireEvent.click(screen.getByText(\"Quick OAuth Flow\"));\n      });\n\n      // Should first discover and save OAuth metadata\n      expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledWith(\n        new URL(\"https://example.com/\"),\n      );\n\n      // Check that updateAuthState was called with the right info message\n      expect(updateAuthState).toHaveBeenCalledWith(\n        expect.objectContaining({\n          oauthStep: \"authorization_code\",\n        }),\n      );\n    });\n\n    it(\"should show error when quick OAuth flow fails to discover metadata\", async () => {\n      mockDiscoverAuthorizationServerMetadata.mockRejectedValue(\n        new Error(\"Metadata discovery failed\"),\n      );\n\n      const updateAuthState = jest.fn();\n      await act(async () => {\n        renderAuthDebugger({ updateAuthState });\n      });\n\n      await act(async () => {\n        fireEvent.click(screen.getByText(\"Quick OAuth Flow\"));\n      });\n\n      // Check that updateAuthState was called with an error message\n      expect(updateAuthState).toHaveBeenCalledWith(\n        expect.objectContaining({\n          statusMessage: {\n            type: \"error\",\n            message: expect.stringContaining(\"Failed to start OAuth flow\"),\n          },\n        }),\n      );\n    });\n  });\n\n  describe(\"Session Storage Integration\", () => {\n    it(\"should load OAuth tokens from session storage\", async () => {\n      // Mock the specific key for tokens with server URL\n      sessionStorageMock.getItem.mockImplementation((key) => {\n        if (key === \"[https://example.com] mcp_tokens\") {\n          return JSON.stringify(mockOAuthTokens);\n        }\n        return null;\n      });\n\n      await act(async () => {\n        renderAuthDebugger({\n          authState: {\n            ...defaultAuthState,\n            oauthTokens: mockOAuthTokens,\n          },\n        });\n      });\n\n      await waitFor(() => {\n        expect(screen.getByText(/Access Token:/)).toBeInTheDocument();\n      });\n    });\n\n    it(\"should handle errors loading OAuth tokens from session storage\", async () => {\n      // Mock console to avoid cluttering test output\n      const originalError = console.error;\n      console.error = jest.fn();\n\n      // Mock getItem to return invalid JSON for tokens\n      sessionStorageMock.getItem.mockImplementation((key) => {\n        if (key === \"[https://example.com] mcp_tokens\") {\n          return \"invalid json\";\n        }\n        return null;\n      });\n\n      await act(async () => {\n        renderAuthDebugger();\n      });\n\n      // Component should still render despite the error\n      expect(screen.getByText(\"Authentication Settings\")).toBeInTheDocument();\n\n      // Restore console.error\n      console.error = originalError;\n    });\n  });\n\n  describe(\"OAuth State Management\", () => {\n    it(\"should clear OAuth state when Clear button is clicked\", async () => {\n      const updateAuthState = jest.fn();\n      // Mock the session storage to return tokens for the specific key\n      sessionStorageMock.getItem.mockImplementation((key) => {\n        if (key === \"[https://example.com] mcp_tokens\") {\n          return JSON.stringify(mockOAuthTokens);\n        }\n        return null;\n      });\n\n      await act(async () => {\n        renderAuthDebugger({\n          authState: {\n            ...defaultAuthState,\n            oauthTokens: mockOAuthTokens,\n          },\n          updateAuthState,\n        });\n      });\n\n      await act(async () => {\n        fireEvent.click(screen.getByText(\"Clear OAuth State\"));\n      });\n\n      expect(updateAuthState).toHaveBeenCalledWith({\n        authServerUrl: null,\n        authorizationUrl: null,\n        isInitiatingAuth: false,\n        resourceMetadata: null,\n        resourceMetadataError: null,\n        resource: null,\n        oauthTokens: null,\n        oauthStep: \"metadata_discovery\",\n        latestError: null,\n        oauthClientInfo: null,\n        oauthMetadata: null,\n        authorizationCode: \"\",\n        validationError: null,\n        statusMessage: {\n          type: \"success\",\n          message: \"OAuth tokens cleared successfully\",\n        },\n      });\n\n      // Verify session storage was cleared\n      expect(sessionStorageMock.removeItem).toHaveBeenCalled();\n    });\n  });\n\n  describe(\"OAuth Flow Steps\", () => {\n    it(\"should handle OAuth flow step progression\", async () => {\n      const updateAuthState = jest.fn();\n      await act(async () => {\n        renderAuthDebugger({\n          updateAuthState,\n          authState: {\n            ...defaultAuthState,\n            isInitiatingAuth: false, // Changed to false so button is enabled\n            oauthStep: \"metadata_discovery\",\n          },\n        });\n      });\n\n      // Verify metadata discovery step\n      expect(screen.getByText(\"Metadata Discovery\")).toBeInTheDocument();\n\n      // Click Continue - this should trigger metadata discovery\n      await act(async () => {\n        fireEvent.click(screen.getByText(\"Continue\"));\n      });\n\n      expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledWith(\n        new URL(\"https://example.com/\"),\n      );\n    });\n\n    // Setup helper for OAuth authorization tests\n    const setupAuthorizationUrlTest = async (metadata: OAuthMetadata) => {\n      const updateAuthState = jest.fn();\n\n      // Mock the session storage to return metadata\n      sessionStorageMock.getItem.mockImplementation((key) => {\n        if (key === `[https://example.com] ${SESSION_KEYS.SERVER_METADATA}`) {\n          return JSON.stringify(metadata);\n        }\n        if (\n          key === `[https://example.com] ${SESSION_KEYS.CLIENT_INFORMATION}`\n        ) {\n          return JSON.stringify(mockOAuthClientInfo);\n        }\n        return null;\n      });\n\n      await act(async () => {\n        renderAuthDebugger({\n          updateAuthState,\n          authState: {\n            ...defaultAuthState,\n            isInitiatingAuth: false,\n            oauthStep: \"authorization_redirect\",\n            oauthMetadata: metadata,\n            oauthClientInfo: mockOAuthClientInfo,\n          },\n        });\n      });\n\n      // Click Continue to trigger authorization\n      await act(async () => {\n        fireEvent.click(screen.getByText(\"Continue\"));\n      });\n\n      return updateAuthState;\n    };\n\n    it(\"should include scope in authorization URL when scopes_supported is present\", async () => {\n      const metadataWithScopes = {\n        ...mockOAuthMetadata,\n        scopes_supported: [\"read\", \"write\", \"admin\"],\n      };\n\n      const updateAuthState =\n        await setupAuthorizationUrlTest(metadataWithScopes);\n\n      // Wait for the updateAuthState to be called\n      await waitFor(() => {\n        expect(updateAuthState).toHaveBeenCalledWith(\n          expect.objectContaining({\n            authorizationUrl: expect.objectContaining({\n              href: \"https://oauth.example.com/authorize?scope=read+write\",\n            }),\n          }),\n        );\n      });\n    });\n\n    it(\"should include scope in authorization URL when scopes_supported is not present\", async () => {\n      const updateAuthState =\n        await setupAuthorizationUrlTest(mockOAuthMetadata);\n\n      // Wait for the updateAuthState to be called\n      await waitFor(() => {\n        expect(updateAuthState).toHaveBeenCalledWith(\n          expect.objectContaining({\n            authorizationUrl: expect.objectContaining({\n              href: \"https://oauth.example.com/authorize?scope=read+write\",\n            }),\n          }),\n        );\n      });\n    });\n\n    it(\"should omit scope from authorization URL when discoverScopes returns undefined\", async () => {\n      // Mock discoverScopes to return undefined (no scopes available)\n      mockDiscoverScopes.mockResolvedValueOnce(undefined);\n\n      const updateAuthState =\n        await setupAuthorizationUrlTest(mockOAuthMetadata);\n\n      // Wait for the updateAuthState to be called\n      await waitFor(() => {\n        expect(updateAuthState).toHaveBeenCalledWith(\n          expect.objectContaining({\n            authorizationUrl: expect.not.stringContaining(\"scope=\"),\n          }),\n        );\n      });\n    });\n  });\n\n  describe(\"Client Registration behavior\", () => {\n    it(\"uses preregistered (static) client information without calling DCR\", async () => {\n      const preregClientInfo = {\n        client_id: \"static_client_id\",\n        client_secret: \"static_client_secret\",\n        redirect_uris: [\"http://localhost:3000/oauth/callback/debug\"],\n      };\n\n      // Return preregistered client info for the server-specific key\n      sessionStorageMock.getItem.mockImplementation((key) => {\n        if (\n          key ===\n          `[${defaultProps.serverUrl}] ${SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION}`\n        ) {\n          return JSON.stringify(preregClientInfo);\n        }\n        return null;\n      });\n\n      const updateAuthState = jest.fn();\n\n      await act(async () => {\n        renderAuthDebugger({\n          updateAuthState,\n          authState: {\n            ...defaultAuthState,\n            isInitiatingAuth: false,\n            oauthStep: \"client_registration\",\n            oauthMetadata: mockOAuthMetadata as unknown as OAuthMetadata,\n          },\n        });\n      });\n\n      // Proceed from client_registration → authorization_redirect\n      await act(async () => {\n        fireEvent.click(screen.getByText(\"Continue\"));\n      });\n\n      // Should NOT attempt dynamic client registration\n      expect(mockRegisterClient).not.toHaveBeenCalled();\n\n      // Should advance with the preregistered client info\n      expect(updateAuthState).toHaveBeenCalledWith(\n        expect.objectContaining({\n          oauthClientInfo: expect.objectContaining({\n            client_id: \"static_client_id\",\n          }),\n          oauthStep: \"authorization_redirect\",\n        }),\n      );\n    });\n\n    it(\"falls back to DCR when no static client information is available\", async () => {\n      // No preregistered or dynamic client info present in session storage\n      sessionStorageMock.getItem.mockImplementation(() => null);\n\n      // DCR returns a new client\n      mockRegisterClient.mockResolvedValueOnce(mockOAuthClientInfo);\n\n      const updateAuthState = jest.fn();\n\n      await act(async () => {\n        renderAuthDebugger({\n          updateAuthState,\n          authState: {\n            ...defaultAuthState,\n            isInitiatingAuth: false,\n            oauthStep: \"client_registration\",\n            oauthMetadata: mockOAuthMetadata as unknown as OAuthMetadata,\n          },\n        });\n      });\n\n      await act(async () => {\n        fireEvent.click(screen.getByText(\"Continue\"));\n      });\n\n      expect(mockRegisterClient).toHaveBeenCalledTimes(1);\n\n      // Should save and advance with the DCR client info\n      expect(updateAuthState).toHaveBeenCalledWith(\n        expect.objectContaining({\n          oauthClientInfo: expect.objectContaining({\n            client_id: \"test_client_id\",\n          }),\n          oauthStep: \"authorization_redirect\",\n        }),\n      );\n\n      // Verify the dynamically registered client info was persisted\n      expect(sessionStorage.setItem).toHaveBeenCalledWith(\n        `[${defaultProps.serverUrl}] ${SESSION_KEYS.CLIENT_INFORMATION}`,\n        expect.any(String),\n      );\n    });\n  });\n\n  describe(\"OAuth State Persistence\", () => {\n    it(\"should store auth state to sessionStorage before redirect in Quick OAuth Flow\", async () => {\n      const updateAuthState = jest.fn();\n\n      // Setup mocks for OAuth flow\n      mockStartAuthorization.mockResolvedValue({\n        authorizationUrl: new URL(\n          \"https://oauth.example.com/authorize?client_id=test_client_id&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Foauth%2Fcallback%2Fdebug\",\n        ),\n        codeVerifier: \"test_verifier\",\n      });\n\n      await act(async () => {\n        renderAuthDebugger({\n          updateAuthState,\n          authState: { ...defaultAuthState },\n        });\n      });\n\n      // Click Quick OAuth Flow\n      await act(async () => {\n        fireEvent.click(screen.getByText(\"Quick OAuth Flow\"));\n      });\n\n      // Wait for the flow to reach the authorization step\n      await waitFor(() => {\n        expect(sessionStorage.setItem).toHaveBeenCalledWith(\n          SESSION_KEYS.AUTH_DEBUGGER_STATE,\n          expect.stringContaining('\"oauthStep\":\"authorization_code\"'),\n        );\n      });\n\n      // Verify the stored state includes all the accumulated data\n      const storedStateCall = (\n        sessionStorage.setItem as jest.Mock\n      ).mock.calls.find((call) => call[0] === SESSION_KEYS.AUTH_DEBUGGER_STATE);\n\n      expect(storedStateCall).toBeDefined();\n      const storedState = JSON.parse(storedStateCall![1] as string);\n\n      expect(storedState).toMatchObject({\n        oauthStep: \"authorization_code\",\n        authorizationUrl: expect.stringMatching(\n          /^https:\\/\\/oauth\\.example\\.com\\/authorize/,\n        ),\n        oauthMetadata: expect.objectContaining({\n          token_endpoint: \"https://oauth.example.com/token\",\n        }),\n        oauthClientInfo: expect.objectContaining({\n          client_id: \"test_client_id\",\n        }),\n      });\n    });\n  });\n\n  describe(\"OAuth Protected Resource Metadata\", () => {\n    it(\"should successfully fetch and display protected resource metadata\", async () => {\n      const updateAuthState = jest.fn();\n      const mockResourceMetadata = {\n        resource: \"https://example.com/mcp\",\n        authorization_servers: [\"https://custom-auth.example.com/mcp/tenant\"],\n        bearer_methods_supported: [\"header\", \"body\"],\n        resource_documentation: \"https://example.com/mcp/docs\",\n        resource_policy_uri: \"https://example.com/mcp/policy\",\n      };\n\n      // Mock successful metadata discovery\n      mockDiscoverOAuthProtectedResourceMetadata.mockResolvedValue(\n        mockResourceMetadata,\n      );\n      mockDiscoverAuthorizationServerMetadata.mockResolvedValue(\n        mockOAuthMetadata,\n      );\n\n      await act(async () => {\n        renderAuthDebugger({\n          updateAuthState,\n          authState: { ...defaultAuthState },\n        });\n      });\n\n      // Click Guided OAuth Flow to start the process\n      await act(async () => {\n        fireEvent.click(screen.getByText(\"Guided OAuth Flow\"));\n      });\n\n      // Verify that the flow started with metadata discovery\n      expect(updateAuthState).toHaveBeenCalledWith({\n        oauthStep: \"metadata_discovery\",\n        authorizationUrl: null,\n        statusMessage: null,\n        latestError: null,\n      });\n\n      // Click Continue to trigger metadata discovery\n      const continueButton = await screen.findByText(\"Continue\");\n      await act(async () => {\n        fireEvent.click(continueButton);\n      });\n\n      // Wait for the metadata to be fetched\n      await waitFor(() => {\n        expect(mockDiscoverOAuthProtectedResourceMetadata).toHaveBeenCalledWith(\n          \"https://example.com/mcp\",\n        );\n      });\n\n      // Verify the state was updated with the resource metadata\n      await waitFor(() => {\n        expect(updateAuthState).toHaveBeenCalledWith(\n          expect.objectContaining({\n            resourceMetadata: mockResourceMetadata,\n            authServerUrl: new URL(\n              \"https://custom-auth.example.com/mcp/tenant\",\n            ),\n            oauthStep: \"client_registration\",\n          }),\n        );\n      });\n\n      expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledWith(\n        new URL(\"https://custom-auth.example.com/mcp/tenant\"),\n      );\n    });\n\n    it(\"should handle protected resource metadata fetch failure gracefully\", async () => {\n      const updateAuthState = jest.fn();\n      const mockError = new Error(\"Failed to fetch resource metadata\");\n\n      // Mock failed metadata discovery\n      mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(mockError);\n      // But OAuth metadata should still work with the original URL\n      mockDiscoverAuthorizationServerMetadata.mockResolvedValue(\n        mockOAuthMetadata,\n      );\n\n      await act(async () => {\n        renderAuthDebugger({\n          updateAuthState,\n          authState: { ...defaultAuthState },\n        });\n      });\n\n      // Click Guided OAuth Flow\n      await act(async () => {\n        fireEvent.click(screen.getByText(\"Guided OAuth Flow\"));\n      });\n\n      // Click Continue to trigger metadata discovery\n      const continueButton = await screen.findByText(\"Continue\");\n      await act(async () => {\n        fireEvent.click(continueButton);\n      });\n\n      // Wait for the metadata fetch to fail\n      await waitFor(() => {\n        expect(mockDiscoverOAuthProtectedResourceMetadata).toHaveBeenCalledWith(\n          \"https://example.com/mcp\",\n        );\n      });\n\n      // Verify the flow continues despite the error\n      await waitFor(() => {\n        expect(updateAuthState).toHaveBeenCalledWith(\n          expect.objectContaining({\n            resourceMetadataError: mockError,\n            // Should use the original server URL as fallback\n            authServerUrl: new URL(\"https://example.com/\"),\n            oauthStep: \"client_registration\",\n          }),\n        );\n      });\n\n      // Verify that regular OAuth metadata discovery was still called\n      expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledWith(\n        new URL(\"https://example.com/\"),\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/components/__tests__/DynamicJsonForm.array.test.tsx",
    "content": "import { render, screen, fireEvent } from \"@testing-library/react\";\nimport \"@testing-library/jest-dom\";\nimport { describe, it, expect, jest } from \"@jest/globals\";\nimport DynamicJsonForm from \"../DynamicJsonForm\";\nimport type { JsonSchemaType } from \"@/utils/jsonUtils\";\n\ndescribe(\"DynamicJsonForm Array Fields\", () => {\n  const renderSimpleArrayForm = (props = {}) => {\n    const defaultProps = {\n      schema: {\n        type: \"array\" as const,\n        description: \"Test array field\",\n        items: {\n          type: \"string\" as const,\n          description: \"Array item\",\n        },\n      } satisfies JsonSchemaType,\n      value: [],\n      onChange: jest.fn(),\n    };\n    return render(<DynamicJsonForm {...defaultProps} {...props} />);\n  };\n\n  const renderComplexArrayForm = (props = {}) => {\n    const defaultProps = {\n      schema: {\n        type: \"array\" as const,\n        description: \"Test complex array field\",\n        items: {\n          type: \"object\" as const,\n          properties: {\n            nested: { type: \"object\" as const },\n          },\n        },\n      } satisfies JsonSchemaType,\n      value: [],\n      onChange: jest.fn(),\n    };\n    return render(<DynamicJsonForm {...defaultProps} {...props} />);\n  };\n\n  describe(\"Simple Array Rendering\", () => {\n    it(\"should render form fields for simple array items\", () => {\n      renderSimpleArrayForm({ value: [\"item1\", \"item2\"] });\n\n      // Should show array description\n      expect(screen.getByText(\"Test array field\")).toBeDefined();\n      expect(screen.getByText(\"Items: Array item\")).toBeDefined();\n\n      // Should show input fields for each item\n      const inputs = screen.getAllByRole(\"textbox\");\n      expect(inputs).toHaveLength(2);\n      expect(inputs[0]).toHaveProperty(\"value\", \"item1\");\n      expect(inputs[1]).toHaveProperty(\"value\", \"item2\");\n\n      // Should show remove buttons\n      const removeButtons = screen.getAllByText(\"Remove\");\n      expect(removeButtons).toHaveLength(2);\n\n      // Should show add button\n      expect(screen.getByText(\"Add Item\")).toBeDefined();\n    });\n\n    it(\"should add new items when Add Item button is clicked\", () => {\n      const onChange = jest.fn();\n      renderSimpleArrayForm({ value: [\"item1\"], onChange });\n\n      const addButton = screen.getByText(\"Add Item\");\n      fireEvent.click(addButton);\n\n      expect(onChange).toHaveBeenCalledWith([\"item1\", \"\"]);\n    });\n\n    it(\"should remove items when Remove button is clicked\", () => {\n      const onChange = jest.fn();\n      renderSimpleArrayForm({ value: [\"item1\", \"item2\"], onChange });\n\n      const removeButtons = screen.getAllByText(\"Remove\");\n      fireEvent.click(removeButtons[0]);\n\n      expect(onChange).toHaveBeenCalledWith([\"item2\"]);\n    });\n\n    it(\"should update item values when input changes\", () => {\n      const onChange = jest.fn();\n      renderSimpleArrayForm({ value: [\"item1\"], onChange });\n\n      const input = screen.getByRole(\"textbox\");\n      fireEvent.change(input, { target: { value: \"updated item\" } });\n\n      expect(onChange).toHaveBeenCalledWith([\"updated item\"]);\n    });\n\n    it(\"should handle empty arrays\", () => {\n      renderSimpleArrayForm({ value: [] });\n\n      // Should show description and add button but no items\n      expect(screen.getByText(\"Test array field\")).toBeDefined();\n      expect(screen.getByText(\"Add Item\")).toBeDefined();\n      expect(screen.queryByText(\"Remove\")).toBeNull();\n    });\n  });\n\n  describe(\"Complex Array Fallback\", () => {\n    it(\"should render JSON editor for complex arrays\", () => {\n      renderComplexArrayForm();\n\n      // Initially renders form view with Switch to JSON button; switch to JSON to see textarea\n      const switchBtn = screen.getByRole(\"button\", { name: /switch to json/i });\n      expect(switchBtn).toBeInTheDocument();\n      fireEvent.click(switchBtn);\n\n      const textarea = screen.getByRole(\"textbox\");\n      expect(textarea).toHaveProperty(\"type\", \"textarea\");\n\n      // Should not show form-specific array controls\n      expect(screen.queryByText(\"Add Item\")).toBeNull();\n      expect(screen.queryByText(\"Remove\")).toBeNull();\n    });\n  });\n\n  describe(\"Array Type Detection\", () => {\n    it(\"should detect string arrays as simple\", () => {\n      const schema = {\n        type: \"array\" as const,\n        items: { type: \"string\" as const },\n      };\n      renderSimpleArrayForm({ schema, value: [\"test\"] });\n\n      // Should render form fields, not JSON editor\n      expect(screen.getByRole(\"textbox\")).not.toHaveProperty(\n        \"type\",\n        \"textarea\",\n      );\n    });\n\n    it(\"should detect number arrays as simple\", () => {\n      const schema = {\n        type: \"array\" as const,\n        items: { type: \"number\" as const },\n      };\n      renderSimpleArrayForm({ schema, value: [1, 2] });\n\n      // Should render form fields (number inputs)\n      const inputs = screen.getAllByRole(\"spinbutton\");\n      expect(inputs).toHaveLength(2);\n    });\n\n    it(\"should detect boolean arrays as simple\", () => {\n      const schema = {\n        type: \"array\" as const,\n        items: { type: \"boolean\" as const },\n      };\n      renderSimpleArrayForm({ schema, value: [true, false] });\n\n      // Should render form fields (checkboxes)\n      const checkboxes = screen.getAllByRole(\"checkbox\");\n      expect(checkboxes).toHaveLength(2);\n    });\n\n    it(\"should detect simple object arrays as simple\", () => {\n      const schema = {\n        type: \"array\" as const,\n        items: {\n          type: \"object\" as const,\n          properties: {\n            name: { type: \"string\" as const },\n            age: { type: \"number\" as const },\n          },\n        },\n      };\n      renderSimpleArrayForm({ schema, value: [{ name: \"John\", age: 30 }] });\n\n      // Should render form fields for simple objects\n      expect(screen.getByText(\"Add Item\")).toBeDefined();\n      expect(screen.getByText(\"Remove\")).toBeDefined();\n    });\n  });\n\n  describe(\"Array with Different Item Types\", () => {\n    it(\"should render multi-select for untitled items.enum with helper text\", () => {\n      const schema: JsonSchemaType = {\n        type: \"array\",\n        title: \"Colors\",\n        description: \"Pick colors\",\n        minItems: 1,\n        maxItems: 3,\n        items: { type: \"string\", enum: [\"red\", \"green\", \"blue\"] },\n      } as JsonSchemaType;\n      const onChange = jest.fn();\n      render(\n        <DynamicJsonForm schema={schema} value={[\"red\"]} onChange={onChange} />,\n      );\n\n      // Description visible\n      expect(screen.getByText(\"Pick colors\")).toBeInTheDocument();\n      // Multi-select present\n      const listbox = screen.getByRole(\"listbox\");\n      expect(listbox).toHaveAttribute(\"multiple\");\n\n      // Helper text shows min/max\n      expect(screen.getByText(/Select at least 1/i)).toBeInTheDocument();\n      expect(screen.getByText(/Select at most 3/i)).toBeInTheDocument();\n\n      // Select another option by toggling option.selected\n      const colorOptions = screen.getAllByRole(\"option\");\n      // options: red, green, blue\n      colorOptions[0].selected = true; // red\n      colorOptions[2].selected = true; // blue\n      fireEvent.change(listbox);\n      expect(onChange).toHaveBeenCalledWith([\"red\", \"blue\"]);\n    });\n\n    it(\"should render titled multi-select for items.anyOf with const/title\", () => {\n      const schema: JsonSchemaType = {\n        type: \"array\",\n        description: \"Pick fish\",\n        items: {\n          anyOf: [\n            { const: \"fish-1\", title: \"Tuna\" },\n            { const: \"fish-2\", title: \"Salmon\" },\n            { const: \"fish-3\", title: \"Trout\" },\n          ],\n        } as unknown as JsonSchemaType,\n      } as unknown as JsonSchemaType;\n      const onChange = jest.fn();\n      render(\n        <DynamicJsonForm\n          schema={schema}\n          value={[\"fish-1\"]}\n          onChange={onChange}\n        />,\n      );\n\n      // Description visible\n      expect(screen.getByText(\"Pick fish\")).toBeInTheDocument();\n\n      const listbox = screen.getByRole(\"listbox\");\n      expect(listbox).toHaveAttribute(\"multiple\");\n\n      // Ensure options have titles as labels\n      const options = screen.getAllByRole(\"option\");\n      expect(options[0]).toHaveProperty(\"textContent\", \"Tuna\");\n      expect(options[1]).toHaveProperty(\"textContent\", \"Salmon\");\n      expect(options[2]).toHaveProperty(\"textContent\", \"Trout\");\n\n      // Select fish-2 and fish-3\n      const fishOptions = screen.getAllByRole(\"option\");\n      // options: Tuna (fish-1), Salmon (fish-2), Trout (fish-3)\n      fishOptions[0].selected = false; // deselect fish-1\n      fishOptions[1].selected = true; // fish-2\n      fishOptions[2].selected = true; // fish-3\n      fireEvent.change(listbox);\n      expect(onChange).toHaveBeenCalledWith([\"fish-2\", \"fish-3\"]);\n    });\n    it(\"should handle integer array items\", () => {\n      const schema = {\n        type: \"array\" as const,\n        items: { type: \"integer\" as const },\n      };\n      const onChange = jest.fn();\n      renderSimpleArrayForm({ schema, value: [1, 2], onChange });\n\n      const inputs = screen.getAllByRole(\"spinbutton\");\n      expect(inputs).toHaveLength(2);\n      expect(inputs[0]).toHaveProperty(\"value\", \"1\");\n      expect(inputs[1]).toHaveProperty(\"value\", \"2\");\n\n      // Test adding new integer item\n      const addButton = screen.getByText(\"Add Item\");\n      fireEvent.click(addButton);\n      expect(onChange).toHaveBeenCalledWith([1, 2, 0]);\n    });\n\n    it(\"should handle boolean array items\", () => {\n      const schema = {\n        type: \"array\" as const,\n        items: { type: \"boolean\" as const },\n      };\n      const onChange = jest.fn();\n      renderSimpleArrayForm({ schema, value: [true, false], onChange });\n\n      const checkboxes = screen.getAllByRole(\"checkbox\");\n      expect(checkboxes).toHaveLength(2);\n      expect(checkboxes[0]).toHaveProperty(\"checked\", true);\n      expect(checkboxes[1]).toHaveProperty(\"checked\", false);\n\n      // Test adding new boolean item\n      const addButton = screen.getByText(\"Add Item\");\n      fireEvent.click(addButton);\n      expect(onChange).toHaveBeenCalledWith([true, false, false]);\n    });\n  });\n\n  describe(\"Array Item Descriptions\", () => {\n    it(\"should show item description when available\", () => {\n      const schema = {\n        type: \"array\" as const,\n        description: \"List of names\",\n        items: {\n          type: \"string\" as const,\n          description: \"Person name\",\n        },\n      };\n      renderSimpleArrayForm({ schema });\n\n      expect(screen.getByText(\"List of names\")).toBeDefined();\n      expect(screen.getByText(\"Items: Person name\")).toBeDefined();\n    });\n\n    it(\"should use item description in add button title\", () => {\n      const schema = {\n        type: \"array\" as const,\n        items: {\n          type: \"string\" as const,\n          description: \"Email address\",\n        },\n      };\n      renderSimpleArrayForm({ schema });\n\n      const addButton = screen.getByText(\"Add Item\");\n      expect(addButton).toHaveProperty(\"title\", \"Add new Email address\");\n    });\n\n    it(\"should use default title when no item description\", () => {\n      const schema = {\n        type: \"array\" as const,\n        items: { type: \"string\" as const },\n      };\n      renderSimpleArrayForm({ schema });\n\n      const addButton = screen.getByText(\"Add Item\");\n      expect(addButton).toHaveProperty(\"title\", \"Add new item\");\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/components/__tests__/DynamicJsonForm.test.tsx",
    "content": "import { render, screen, fireEvent, waitFor } from \"@testing-library/react\";\nimport \"@testing-library/jest-dom\";\nimport { describe, it, expect, jest } from \"@jest/globals\";\nimport { useRef, useState } from \"react\";\nimport DynamicJsonForm, { DynamicJsonFormRef } from \"../DynamicJsonForm\";\nimport type { JsonSchemaType } from \"@/utils/jsonUtils\";\n\ndescribe(\"DynamicJsonForm String Fields\", () => {\n  const renderForm = (props = {}) => {\n    const defaultProps = {\n      schema: {\n        type: \"string\" as const,\n        description: \"Test string field\",\n      } satisfies JsonSchemaType,\n      value: undefined,\n      onChange: jest.fn(),\n    };\n    return render(<DynamicJsonForm {...defaultProps} {...props} />);\n  };\n\n  describe(\"Type Validation\", () => {\n    it(\"should handle numeric input as string type\", () => {\n      const onChange = jest.fn();\n      renderForm({ onChange });\n\n      const input = screen.getByRole(\"textbox\");\n      fireEvent.change(input, { target: { value: \"123321\" } });\n\n      expect(onChange).toHaveBeenCalledWith(\"123321\");\n      // Verify the value is a string, not a number\n      expect(typeof onChange.mock.calls[0][0]).toBe(\"string\");\n    });\n\n    it(\"should render as text input, not number input\", () => {\n      renderForm();\n      const input = screen.getByRole(\"textbox\");\n      expect(input).toHaveProperty(\"type\", \"text\");\n    });\n\n    it(\"should handle a union type of string and null\", () => {\n      const schema: JsonSchemaType = {\n        type: [\"string\", \"null\"],\n        description: \"Test string or null field\",\n      };\n      render(\n        <DynamicJsonForm schema={schema} value={null} onChange={jest.fn()} />,\n      );\n      const input = screen.getByRole(\"textbox\");\n      expect(input).toHaveProperty(\"type\", \"text\");\n    });\n  });\n\n  describe(\"Format Support\", () => {\n    it(\"should render email input for email format\", () => {\n      const schema: JsonSchemaType = {\n        type: \"string\",\n        format: \"email\",\n        description: \"Email address\",\n      };\n      render(<DynamicJsonForm schema={schema} value=\"\" onChange={jest.fn()} />);\n\n      const input = screen.getByRole(\"textbox\");\n      expect(input).toHaveProperty(\"type\", \"email\");\n    });\n\n    it(\"should render url input for uri format\", () => {\n      const schema: JsonSchemaType = {\n        type: \"string\",\n        format: \"uri\",\n        description: \"Website URL\",\n      };\n      render(<DynamicJsonForm schema={schema} value=\"\" onChange={jest.fn()} />);\n\n      const input = screen.getByRole(\"textbox\");\n      expect(input).toHaveProperty(\"type\", \"url\");\n    });\n\n    it(\"should render date input for date format\", () => {\n      const schema: JsonSchemaType = {\n        type: \"string\",\n        format: \"date\",\n        description: \"Birth date\",\n      };\n      render(<DynamicJsonForm schema={schema} value=\"\" onChange={jest.fn()} />);\n\n      const input = screen.getByDisplayValue(\"\");\n      expect(input).toHaveProperty(\"type\", \"date\");\n    });\n\n    it(\"should render datetime-local input for date-time format\", () => {\n      const schema: JsonSchemaType = {\n        type: \"string\",\n        format: \"date-time\",\n        description: \"Event datetime\",\n      };\n      render(<DynamicJsonForm schema={schema} value=\"\" onChange={jest.fn()} />);\n\n      const input = screen.getByDisplayValue(\"\");\n      expect(input).toHaveProperty(\"type\", \"datetime-local\");\n    });\n  });\n\n  describe(\"Enum Support\", () => {\n    it(\"should render select dropdown for enum fields\", () => {\n      const schema: JsonSchemaType = {\n        type: \"string\",\n        enum: [\"option1\", \"option2\", \"option3\"],\n        description: \"Select an option\",\n      };\n      render(<DynamicJsonForm schema={schema} value=\"\" onChange={jest.fn()} />);\n\n      const select = screen.getByRole(\"combobox\");\n      expect(select.tagName).toBe(\"SELECT\");\n\n      const options = screen.getAllByRole(\"option\");\n      expect(options).toHaveLength(4);\n    });\n\n    it(\"should use oneOf with const and title for labeled options\", () => {\n      const schema: JsonSchemaType = {\n        type: \"string\",\n        oneOf: [\n          { const: \"val1\", title: \"Label 1\" },\n          { const: \"val2\", title: \"Label 2\" },\n        ],\n        description: \"Select with labels\",\n      };\n      render(<DynamicJsonForm schema={schema} value=\"\" onChange={jest.fn()} />);\n\n      const options = screen.getAllByRole(\"option\");\n      expect(options[1]).toHaveProperty(\"textContent\", \"Label 1\");\n      expect(options[2]).toHaveProperty(\"textContent\", \"Label 2\");\n    });\n\n    it(\"should call onChange with selected oneOf value\", () => {\n      const onChange = jest.fn();\n      const schema: JsonSchemaType = {\n        type: \"string\",\n        oneOf: [\n          { const: \"option1\", title: \"Option 1\" },\n          { const: \"option2\", title: \"Option 2\" },\n        ],\n        description: \"Select an option\",\n      };\n      render(<DynamicJsonForm schema={schema} value=\"\" onChange={onChange} />);\n\n      const select = screen.getByRole(\"combobox\");\n      fireEvent.change(select, { target: { value: \"option1\" } });\n\n      expect(onChange).toHaveBeenCalledWith(\"option1\");\n    });\n\n    it(\"should call onChange with selected enum value\", () => {\n      const onChange = jest.fn();\n      const schema: JsonSchemaType = {\n        type: \"string\",\n        enum: [\"option1\", \"option2\"],\n        description: \"Select an option\",\n      };\n      render(<DynamicJsonForm schema={schema} value=\"\" onChange={onChange} />);\n\n      const select = screen.getByRole(\"combobox\");\n      fireEvent.change(select, { target: { value: \"option1\" } });\n\n      expect(onChange).toHaveBeenCalledWith(\"option1\");\n    });\n\n    it(\"should render JSON Schema spec compliant oneOf with const for labeled enums\", () => {\n      // Example from JSON Schema spec: labeled enums using oneOf with const\n      const onChange = jest.fn();\n      const schema: JsonSchemaType = {\n        type: \"string\",\n        title: \"Traffic Light\",\n        description: \"Select a traffic light color\",\n        oneOf: [\n          { const: \"red\", title: \"Stop\" },\n          { const: \"amber\", title: \"Caution\" },\n          { const: \"green\", title: \"Go\" },\n        ],\n      };\n      render(<DynamicJsonForm schema={schema} value=\"\" onChange={onChange} />);\n\n      // Should render as a select dropdown\n      const select = screen.getByRole(\"combobox\");\n      expect(select.tagName).toBe(\"SELECT\");\n\n      // Should have options with proper labels\n      const options = screen.getAllByRole(\"option\");\n      expect(options).toHaveLength(4); // 3 options + 1 default \"Select an option...\"\n\n      expect(options[0]).toHaveProperty(\"textContent\", \"Select an option...\");\n      expect(options[1]).toHaveProperty(\"textContent\", \"Stop\");\n      expect(options[2]).toHaveProperty(\"textContent\", \"Caution\");\n      expect(options[3]).toHaveProperty(\"textContent\", \"Go\");\n\n      // Should have proper values\n      expect(options[1]).toHaveProperty(\"value\", \"red\");\n      expect(options[2]).toHaveProperty(\"value\", \"amber\");\n      expect(options[3]).toHaveProperty(\"value\", \"green\");\n\n      // Test onChange behavior\n      fireEvent.change(select, { target: { value: \"amber\" } });\n      expect(onChange).toHaveBeenCalledWith(\"amber\");\n    });\n\n    it(\"should render anyOf with const/title for labeled options and show description\", () => {\n      const onChange = jest.fn();\n      const schema: JsonSchemaType = {\n        type: \"string\",\n        title: \"Heroes\",\n        description: \"Choose a hero\",\n        anyOf: [\n          { const: \"hero-1\", title: \"Superman\" },\n          { const: \"hero-2\", title: \"Batman\" },\n        ],\n      };\n      render(<DynamicJsonForm schema={schema} value=\"\" onChange={onChange} />);\n\n      // Description should be visible above the select\n      expect(screen.getByText(\"Choose a hero\")).toBeInTheDocument();\n      const select = screen.getByRole(\"combobox\");\n      const options = screen.getAllByRole(\"option\");\n      expect(options).toHaveLength(3);\n      expect(options[1]).toHaveProperty(\"textContent\", \"Superman\");\n      expect(options[2]).toHaveProperty(\"textContent\", \"Batman\");\n\n      fireEvent.change(select, { target: { value: \"hero-2\" } });\n      expect(onChange).toHaveBeenCalledWith(\"hero-2\");\n    });\n\n    it(\"should render legacy enum with enumNames as labels\", () => {\n      const onChange = jest.fn();\n      const schema: JsonSchemaType = {\n        type: \"string\",\n        title: \"Pets\",\n        description: \"Choose a pet\",\n        enum: [\"pet-1\", \"pet-2\", \"pet-3\"],\n        enumNames: [\"Cat\", \"Dog\", \"Bird\"],\n      } as unknown as JsonSchemaType; // enumNames is legacy extension\n\n      render(<DynamicJsonForm schema={schema} value=\"\" onChange={onChange} />);\n\n      // Description should be visible above the select\n      expect(screen.getByText(\"Choose a pet\")).toBeInTheDocument();\n      const options = screen.getAllByRole(\"option\");\n      expect(options[1]).toHaveProperty(\"textContent\", \"Cat\");\n      expect(options[2]).toHaveProperty(\"textContent\", \"Dog\");\n      expect(options[3]).toHaveProperty(\"textContent\", \"Bird\");\n\n      const select = screen.getByRole(\"combobox\");\n      fireEvent.change(select, { target: { value: \"pet-2\" } });\n      expect(onChange).toHaveBeenCalledWith(\"pet-2\");\n    });\n  });\n\n  describe(\"Validation Attributes\", () => {\n    it(\"should apply minLength and maxLength\", () => {\n      const schema: JsonSchemaType = {\n        type: \"string\",\n        minLength: 3,\n        maxLength: 10,\n        description: \"Username\",\n      };\n      render(<DynamicJsonForm schema={schema} value=\"\" onChange={jest.fn()} />);\n\n      const input = screen.getByRole(\"textbox\");\n      expect(input).toHaveProperty(\"minLength\", 3);\n      expect(input).toHaveProperty(\"maxLength\", 10);\n    });\n\n    it(\"should apply pattern validation\", () => {\n      const schema: JsonSchemaType = {\n        type: \"string\",\n        pattern: \"^[A-Za-z]+$\",\n        description: \"Letters only\",\n      };\n      render(<DynamicJsonForm schema={schema} value=\"\" onChange={jest.fn()} />);\n\n      const input = screen.getByRole(\"textbox\");\n      expect(input).toHaveProperty(\"pattern\", \"^[A-Za-z]+$\");\n    });\n  });\n});\n\ndescribe(\"DynamicJsonForm Integer Fields\", () => {\n  const renderForm = (props = {}) => {\n    const defaultProps = {\n      schema: {\n        type: \"integer\" as const,\n        description: \"Test integer field\",\n      } satisfies JsonSchemaType,\n      value: undefined,\n      onChange: jest.fn(),\n    };\n    return render(<DynamicJsonForm {...defaultProps} {...props} />);\n  };\n\n  describe(\"Basic Operations\", () => {\n    it(\"should render number input with step=1\", () => {\n      renderForm();\n      const input = screen.getByRole(\"spinbutton\");\n      expect(input).toHaveProperty(\"type\", \"number\");\n      expect(input).toHaveProperty(\"step\", \"1\");\n    });\n\n    it(\"should pass integer values to onChange\", () => {\n      const onChange = jest.fn();\n      renderForm({ onChange });\n\n      const input = screen.getByRole(\"spinbutton\");\n      fireEvent.change(input, { target: { value: \"42\" } });\n\n      expect(onChange).toHaveBeenCalledWith(42);\n      // Verify the value is a number, not a string\n      expect(typeof onChange.mock.calls[0][0]).toBe(\"number\");\n    });\n\n    it(\"should not pass string values to onChange\", () => {\n      const onChange = jest.fn();\n      renderForm({ onChange });\n\n      const input = screen.getByRole(\"spinbutton\");\n      fireEvent.change(input, { target: { value: \"abc\" } });\n\n      expect(onChange).not.toHaveBeenCalled();\n    });\n  });\n\n  describe(\"Validation\", () => {\n    it(\"should apply min and max constraints\", () => {\n      const schema: JsonSchemaType = {\n        type: \"integer\",\n        minimum: 0,\n        maximum: 100,\n        description: \"Age\",\n      };\n      render(\n        <DynamicJsonForm schema={schema} value={0} onChange={jest.fn()} />,\n      );\n\n      const input = screen.getByRole(\"spinbutton\");\n      expect(input).toHaveProperty(\"min\", \"0\");\n      expect(input).toHaveProperty(\"max\", \"100\");\n    });\n\n    it(\"should only accept integer values\", () => {\n      const onChange = jest.fn();\n      const schema: JsonSchemaType = {\n        type: \"integer\",\n        description: \"Count\",\n      };\n      render(<DynamicJsonForm schema={schema} value={0} onChange={onChange} />);\n\n      const input = screen.getByRole(\"spinbutton\");\n      fireEvent.change(input, { target: { value: \"3.14\" } });\n\n      expect(onChange).not.toHaveBeenCalled();\n    });\n  });\n\n  describe(\"Edge Cases\", () => {\n    it(\"should handle non-numeric input by not calling onChange\", () => {\n      const onChange = jest.fn();\n      renderForm({ onChange });\n\n      const input = screen.getByRole(\"spinbutton\");\n      fireEvent.change(input, { target: { value: \"abc\" } });\n\n      expect(onChange).not.toHaveBeenCalled();\n    });\n  });\n});\n\ndescribe(\"DynamicJsonForm Number Fields\", () => {\n  describe(\"Validation\", () => {\n    it(\"should apply min and max constraints\", () => {\n      const schema: JsonSchemaType = {\n        type: \"number\",\n        minimum: 0.5,\n        maximum: 99.9,\n        description: \"Score\",\n      };\n      render(\n        <DynamicJsonForm schema={schema} value={0} onChange={jest.fn()} />,\n      );\n\n      const input = screen.getByRole(\"spinbutton\");\n      expect(input).toHaveProperty(\"min\", \"0.5\");\n      expect(input).toHaveProperty(\"max\", \"99.9\");\n    });\n\n    it(\"should accept decimal values\", () => {\n      const onChange = jest.fn();\n      const schema: JsonSchemaType = {\n        type: \"number\",\n        description: \"Temperature\",\n      };\n      render(<DynamicJsonForm schema={schema} value={0} onChange={onChange} />);\n\n      const input = screen.getByRole(\"spinbutton\");\n      fireEvent.change(input, { target: { value: \"98.6\" } });\n\n      expect(onChange).toHaveBeenCalledWith(98.6);\n    });\n\n    it(\"should preserve decimal zero while typing\", () => {\n      const schema: JsonSchemaType = {\n        type: \"number\",\n        description: \"Coordinate\",\n      };\n\n      const WrappedForm = () => {\n        const [value, setValue] = useState<number>(0);\n        return (\n          <DynamicJsonForm schema={schema} value={value} onChange={setValue} />\n        );\n      };\n\n      render(<WrappedForm />);\n      const input = screen.getByRole(\"spinbutton\") as HTMLInputElement;\n\n      fireEvent.change(input, { target: { value: \"-74.0\" } });\n      expect(input.value).toBe(\"-74.0\");\n\n      fireEvent.change(input, { target: { value: \"-74.01\" } });\n      expect(input.value).toBe(\"-74.01\");\n    });\n  });\n});\n\ndescribe(\"DynamicJsonForm Boolean Fields\", () => {\n  describe(\"Basic Operations\", () => {\n    it(\"should render checkbox for boolean type\", () => {\n      const schema: JsonSchemaType = {\n        type: \"boolean\",\n        description: \"Enable notifications\",\n      };\n      render(\n        <DynamicJsonForm schema={schema} value={false} onChange={jest.fn()} />,\n      );\n\n      const checkbox = screen.getByRole(\"checkbox\");\n      expect(checkbox).toHaveProperty(\"type\", \"checkbox\");\n    });\n\n    it(\"should call onChange with boolean value\", () => {\n      const onChange = jest.fn();\n      const schema: JsonSchemaType = {\n        type: \"boolean\",\n        description: \"Accept terms\",\n      };\n      render(\n        <DynamicJsonForm schema={schema} value={false} onChange={onChange} />,\n      );\n\n      const checkbox = screen.getByRole(\"checkbox\");\n      fireEvent.click(checkbox);\n\n      expect(onChange).toHaveBeenCalledWith(true);\n    });\n\n    it(\"should render boolean field with description\", () => {\n      const schema: JsonSchemaType = {\n        type: \"boolean\",\n        description: \"Enable dark mode\",\n      };\n      render(\n        <DynamicJsonForm schema={schema} value={false} onChange={jest.fn()} />,\n      );\n\n      const checkbox = screen.getByRole(\"checkbox\");\n      expect(checkbox).toHaveProperty(\"checked\", false);\n    });\n  });\n});\n\ndescribe(\"DynamicJsonForm Object Fields\", () => {\n  describe(\"Property Rendering\", () => {\n    it(\"should render input fields for object properties\", () => {\n      const schema: JsonSchemaType = {\n        type: \"object\",\n        properties: {\n          name: {\n            type: \"string\",\n            title: \"Full Name\",\n            description: \"Your full name\",\n          },\n          age: {\n            type: \"integer\",\n            title: \"Age\",\n            description: \"Your age in years\",\n            minimum: 18,\n          },\n          email: {\n            type: \"string\",\n            format: \"email\",\n            title: \"Email\",\n            description: \"Your email address\",\n          },\n        },\n      };\n      render(\n        <DynamicJsonForm schema={schema} value={{}} onChange={jest.fn()} />,\n      );\n\n      const textInputs = screen.getAllByRole(\"textbox\");\n      const numberInput = screen.getByRole(\"spinbutton\");\n\n      expect(textInputs).toHaveLength(2);\n      expect(textInputs[0]).toHaveProperty(\"type\", \"text\");\n      expect(textInputs[1]).toHaveProperty(\"type\", \"email\");\n      expect(numberInput).toHaveProperty(\"type\", \"number\");\n      expect(numberInput).toHaveProperty(\"min\", \"18\");\n    });\n\n    it(\"should handle object field changes correctly\", () => {\n      const onChange = jest.fn();\n      const schema: JsonSchemaType = {\n        type: \"object\",\n        properties: {\n          username: {\n            type: \"string\",\n            description: \"Your username\",\n          },\n        },\n      };\n      render(\n        <DynamicJsonForm schema={schema} value={{}} onChange={onChange} />,\n      );\n\n      const input = screen.getByRole(\"textbox\");\n      fireEvent.change(input, { target: { value: \"testuser\" } });\n\n      expect(onChange).toHaveBeenCalledWith({ username: \"testuser\" });\n    });\n\n    it(\"should handle nested object values correctly\", () => {\n      const onChange = jest.fn();\n      const schema: JsonSchemaType = {\n        type: \"object\",\n        properties: {\n          name: {\n            type: \"string\",\n            title: \"Name\",\n          },\n        },\n      };\n      render(\n        <DynamicJsonForm\n          schema={schema}\n          value={{ name: \"John\" }}\n          onChange={onChange}\n        />,\n      );\n\n      const input = screen.getByDisplayValue(\"John\");\n      fireEvent.change(input, { target: { value: \"Jane\" } });\n\n      expect(onChange).toHaveBeenCalledWith({ name: \"Jane\" });\n    });\n  });\n\n  describe(\"Required Fields\", () => {\n    it(\"should mark required fields with required attribute\", () => {\n      const schema: JsonSchemaType = {\n        type: \"object\",\n        properties: {\n          name: {\n            type: \"string\",\n            title: \"Name\",\n          },\n          email: {\n            type: \"string\",\n            title: \"Email\",\n          },\n        },\n        required: [\"name\"],\n      };\n      render(\n        <DynamicJsonForm schema={schema} value={{}} onChange={jest.fn()} />,\n      );\n\n      const inputs = screen.getAllByRole(\"textbox\");\n      const nameInput = inputs[0];\n      const emailInput = inputs[1];\n\n      expect(nameInput).toHaveProperty(\"required\", true);\n      expect(emailInput).toHaveProperty(\"required\", false);\n    });\n\n    it(\"should mark required fields with required attribute\", () => {\n      const schema: JsonSchemaType = {\n        type: \"object\",\n        properties: {\n          name: {\n            type: \"string\",\n            title: \"Name\",\n          },\n          optional: {\n            type: \"string\",\n            title: \"Optional\",\n          },\n        },\n        required: [\"name\"],\n      };\n      render(\n        <DynamicJsonForm schema={schema} value={{}} onChange={jest.fn()} />,\n      );\n\n      const nameLabel = screen.getByText(\"Name\");\n      const optionalLabel = screen.getByText(\"Optional\");\n\n      const nameInput = nameLabel.closest(\"div\")?.querySelector(\"input\");\n      const optionalInput = optionalLabel\n        .closest(\"div\")\n        ?.querySelector(\"input\");\n\n      expect(nameInput).toHaveProperty(\"required\", true);\n      expect(optionalInput).toHaveProperty(\"required\", false);\n    });\n  });\n});\n\ndescribe(\"DynamicJsonForm Complex Fields\", () => {\n  const renderForm = (props = {}) => {\n    const defaultProps = {\n      schema: {\n        type: \"object\",\n        properties: {\n          // The simplified JsonSchemaType does not accept oneOf fields\n          // But they exist in the more-complete JsonSchema7Type\n          nested: { oneOf: [{ type: \"string\" }, { type: \"integer\" }] },\n        },\n      } as unknown as JsonSchemaType,\n      value: undefined,\n      onChange: jest.fn(),\n    };\n    return render(<DynamicJsonForm {...defaultProps} {...props} />);\n  };\n\n  describe(\"Basic Operations\", () => {\n    it(\"should allow switching to JSON mode and show copy/format buttons\", () => {\n      renderForm();\n\n      // Initially renders as a form with a Switch to JSON button\n      const switchToJson = screen.getByRole(\"button\", {\n        name: /switch to json/i,\n      });\n      expect(switchToJson).toBeInTheDocument();\n\n      // Switch to JSON mode\n      fireEvent.click(switchToJson);\n\n      // Now a textarea and JSON helpers should be visible\n      const input = screen.getByRole(\"textbox\");\n      expect(input).toHaveProperty(\"type\", \"textarea\");\n      const copyButton = screen.getByRole(\"button\", { name: /copy json/i });\n      const formatButton = screen.getByRole(\"button\", { name: /format json/i });\n      expect(copyButton).toBeTruthy();\n      expect(formatButton).toBeTruthy();\n      // And a Switch to Form button should appear\n      expect(\n        screen.getByRole(\"button\", { name: /switch to form/i }),\n      ).toBeInTheDocument();\n    });\n\n    it(\"should pass changed values to onChange in JSON mode\", () => {\n      const onChange = jest.fn();\n      renderForm({ onChange });\n\n      // Switch to JSON mode first\n      fireEvent.click(screen.getByRole(\"button\", { name: /switch to json/i }));\n\n      const input = screen.getByRole(\"textbox\");\n      fireEvent.change(input, {\n        target: { value: `{ \"nested\": \"i am string\" }` },\n      });\n\n      // The onChange handler is debounced when using the JSON view, so we need to wait a little bit\n      return waitFor(() => {\n        expect(onChange).toHaveBeenCalledWith({ nested: \"i am string\" });\n      });\n    });\n  });\n});\n\ndescribe(\"DynamicJsonForm Copy JSON Functionality\", () => {\n  const mockWriteText = jest.fn(() => Promise.resolve());\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n    Object.assign(navigator, {\n      clipboard: {\n        writeText: mockWriteText,\n      },\n    });\n  });\n\n  const renderFormInJsonMode = (props = {}) => {\n    const defaultProps = {\n      schema: {\n        type: \"object\",\n        properties: {\n          nested: { oneOf: [{ type: \"string\" }, { type: \"integer\" }] },\n        },\n      } as unknown as JsonSchemaType,\n      value: { nested: \"test value\" },\n      onChange: jest.fn(),\n    };\n    return render(<DynamicJsonForm {...defaultProps} {...props} />);\n  };\n\n  describe(\"Copy JSON Button\", () => {\n    it(\"should render Copy JSON button when in JSON mode\", () => {\n      renderFormInJsonMode();\n\n      // Switch to JSON mode to reveal copy/format buttons\n      const switchBtn = screen.getByRole(\"button\", { name: /switch to json/i });\n      fireEvent.click(switchBtn);\n\n      const copyButton = screen.getByRole(\"button\", { name: \"Copy JSON\" });\n      expect(copyButton).toBeTruthy();\n    });\n\n    it(\"should not render Copy JSON button when not in JSON mode\", () => {\n      const simpleSchema = {\n        type: \"string\" as const,\n        description: \"Test string field\",\n      };\n\n      render(\n        <DynamicJsonForm\n          schema={simpleSchema}\n          value=\"test\"\n          onChange={jest.fn()}\n        />,\n      );\n\n      const copyButton = screen.queryByRole(\"button\", { name: \"Copy JSON\" });\n      expect(copyButton).toBeNull();\n    });\n\n    it(\"should copy JSON to clipboard when clicked\", async () => {\n      const testValue = { nested: \"test value\", number: 42 };\n\n      renderFormInJsonMode({ value: testValue });\n\n      // Switch to JSON mode first\n      const switchBtn = screen.getByRole(\"button\", { name: /switch to json/i });\n      fireEvent.click(switchBtn);\n\n      const copyButton = screen.getByRole(\"button\", { name: \"Copy JSON\" });\n      fireEvent.click(copyButton);\n\n      await waitFor(() => {\n        expect(mockWriteText).toHaveBeenCalledWith(\n          JSON.stringify(testValue, null, 2),\n        );\n      });\n    });\n  });\n});\n\ndescribe(\"DynamicJsonForm Validation Functionality\", () => {\n  const renderFormWithRef = (props = {}) => {\n    const TestComponent = () => {\n      const formRef = useRef<DynamicJsonFormRef>(null);\n      const defaultProps = {\n        schema: {\n          type: \"object\",\n          properties: {\n            nested: { oneOf: [{ type: \"string\" }, { type: \"integer\" }] },\n          },\n        } as unknown as JsonSchemaType,\n        value: { nested: \"test value\" },\n        onChange: jest.fn(),\n        ref: formRef,\n      };\n\n      return (\n        <div>\n          <DynamicJsonForm {...defaultProps} {...props} />\n          <button\n            onClick={() => {\n              const result = formRef.current?.validateJson();\n              // Add data attributes to make validation result testable\n              const button = document.querySelector(\n                '[data-testid=\"validate-button\"]',\n              ) as HTMLElement;\n              if (button && result) {\n                button.setAttribute(\n                  \"data-validation-valid\",\n                  result.isValid.toString(),\n                );\n                button.setAttribute(\n                  \"data-validation-error\",\n                  result.error || \"\",\n                );\n              }\n            }}\n            data-testid=\"validate-button\"\n          >\n            Validate\n          </button>\n        </div>\n      );\n    };\n\n    return render(<TestComponent />);\n  };\n\n  describe(\"validateJson method\", () => {\n    it(\"should return valid for form mode\", () => {\n      const simpleSchema = {\n        type: \"string\" as const,\n        description: \"Test string field\",\n      };\n\n      const TestComponent = () => {\n        const formRef = useRef<DynamicJsonFormRef>(null);\n\n        return (\n          <div>\n            <DynamicJsonForm\n              ref={formRef}\n              schema={simpleSchema}\n              value=\"test\"\n              onChange={jest.fn()}\n            />\n            <button\n              onClick={() => {\n                const result = formRef.current?.validateJson();\n                const button = document.querySelector(\n                  '[data-testid=\"validate-button\"]',\n                ) as HTMLElement;\n                if (button && result) {\n                  button.setAttribute(\n                    \"data-validation-valid\",\n                    result.isValid.toString(),\n                  );\n                  button.setAttribute(\n                    \"data-validation-error\",\n                    result.error || \"\",\n                  );\n                }\n              }}\n              data-testid=\"validate-button\"\n            >\n              Validate\n            </button>\n          </div>\n        );\n      };\n\n      render(<TestComponent />);\n\n      const validateButton = screen.getByTestId(\"validate-button\");\n      fireEvent.click(validateButton);\n\n      expect(validateButton.getAttribute(\"data-validation-valid\")).toBe(\"true\");\n      expect(validateButton.getAttribute(\"data-validation-error\")).toBe(\"\");\n    });\n\n    it(\"should return valid for valid JSON in JSON mode\", () => {\n      renderFormWithRef();\n\n      // Switch to JSON mode to enable textarea editing/validation\n      const switchBtn = screen.getByRole(\"button\", { name: /switch to json/i });\n      fireEvent.click(switchBtn);\n\n      const validateButton = screen.getByTestId(\"validate-button\");\n      fireEvent.click(validateButton);\n\n      expect(validateButton.getAttribute(\"data-validation-valid\")).toBe(\"true\");\n      expect(validateButton.getAttribute(\"data-validation-error\")).toBe(\"\");\n    });\n\n    it(\"should return invalid for malformed JSON in JSON mode\", async () => {\n      renderFormWithRef();\n\n      // Switch to JSON mode first\n      const switchBtn = screen.getByRole(\"button\", { name: /switch to json/i });\n      fireEvent.click(switchBtn);\n\n      // Enter invalid JSON\n      const textarea = screen.getByRole(\"textbox\");\n      fireEvent.change(textarea, { target: { value: '{ \"invalid\": json }' } });\n\n      // Wait a bit for any debounced updates\n      await waitFor(() => {\n        const validateButton = screen.getByTestId(\"validate-button\");\n        fireEvent.click(validateButton);\n\n        expect(validateButton.getAttribute(\"data-validation-valid\")).toBe(\n          \"false\",\n        );\n        expect(validateButton.getAttribute(\"data-validation-error\")).toContain(\n          \"JSON\",\n        );\n      });\n    });\n\n    it(\"should return valid for empty JSON in JSON mode\", () => {\n      renderFormWithRef();\n\n      // Switch to JSON mode first\n      const switchBtn = screen.getByRole(\"button\", { name: /switch to json/i });\n      fireEvent.click(switchBtn);\n\n      // Clear the textarea\n      const textarea = screen.getByRole(\"textbox\");\n      fireEvent.change(textarea, { target: { value: \"\" } });\n\n      const validateButton = screen.getByTestId(\"validate-button\");\n      fireEvent.click(validateButton);\n\n      expect(validateButton.getAttribute(\"data-validation-valid\")).toBe(\"true\");\n      expect(validateButton.getAttribute(\"data-validation-error\")).toBe(\"\");\n    });\n\n    it(\"should set error state when validation fails\", async () => {\n      renderFormWithRef();\n\n      // Switch to JSON mode first\n      const switchBtn = screen.getByRole(\"button\", { name: /switch to json/i });\n      fireEvent.click(switchBtn);\n\n      // Enter invalid JSON\n      const textarea = screen.getByRole(\"textbox\");\n      fireEvent.change(textarea, {\n        target: { value: '{ \"trailing\": \"comma\", }' },\n      });\n\n      // Trigger validation\n      const validateButton = screen.getByTestId(\"validate-button\");\n      fireEvent.click(validateButton);\n\n      // Check that validation result shows error\n      expect(validateButton.getAttribute(\"data-validation-valid\")).toBe(\n        \"false\",\n      );\n      expect(validateButton.getAttribute(\"data-validation-error\")).toContain(\n        \"JSON\",\n      );\n    });\n  });\n\n  describe(\"forwardRef functionality\", () => {\n    it(\"should expose validateJson method through ref\", () => {\n      const TestComponent = () => {\n        const formRef = useRef<DynamicJsonFormRef>(null);\n\n        return (\n          <div>\n            <DynamicJsonForm\n              ref={formRef}\n              schema={{\n                type: \"object\",\n                properties: {\n                  test: { type: \"string\" },\n                },\n              }}\n              value={{ test: \"value\" }}\n              onChange={jest.fn()}\n            />\n            <button\n              onClick={() => {\n                const hasValidateMethod =\n                  typeof formRef.current?.validateJson === \"function\";\n                const button = document.querySelector(\n                  '[data-testid=\"ref-test-button\"]',\n                ) as HTMLElement;\n                if (button) {\n                  button.setAttribute(\n                    \"data-has-validate-method\",\n                    hasValidateMethod.toString(),\n                  );\n                }\n              }}\n              data-testid=\"ref-test-button\"\n            >\n              Test Ref\n            </button>\n          </div>\n        );\n      };\n\n      render(<TestComponent />);\n\n      const testButton = screen.getByTestId(\"ref-test-button\");\n      fireEvent.click(testButton);\n\n      expect(testButton.getAttribute(\"data-has-validate-method\")).toBe(\"true\");\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/components/__tests__/ElicitationRequest.test.tsx",
    "content": "import { render, screen, fireEvent, act } from \"@testing-library/react\";\nimport \"@testing-library/jest-dom\";\nimport { describe, it, jest, beforeEach, afterEach } from \"@jest/globals\";\nimport ElicitationRequest from \"../ElicitationRequest\";\nimport { PendingElicitationRequest } from \"../ElicitationTab\";\n\njest.mock(\"../DynamicJsonForm\", () => {\n  return function MockDynamicJsonForm({\n    value,\n    onChange,\n  }: {\n    value: unknown;\n    onChange: (value: unknown) => void;\n  }) {\n    return (\n      <div data-testid=\"dynamic-json-form\">\n        <input\n          data-testid=\"form-input\"\n          value={\n            typeof value === \"object\" && value !== null\n              ? JSON.stringify(value)\n              : String(value || \"\")\n          }\n          onChange={(e) => {\n            try {\n              const parsed = JSON.parse(e.target.value);\n              onChange(parsed);\n            } catch {\n              onChange(e.target.value);\n            }\n          }}\n        />\n      </div>\n    );\n  };\n});\n\ndescribe(\"ElicitationRequest\", () => {\n  const mockOnResolve = jest.fn();\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n  });\n\n  afterEach(() => {\n    jest.clearAllMocks();\n  });\n\n  const createMockRequest = (\n    overrides: Partial<PendingElicitationRequest> = {},\n  ): PendingElicitationRequest => ({\n    id: 1,\n    request: {\n      id: 1,\n      message: \"Please provide your information\",\n      requestedSchema: {\n        type: \"object\",\n        properties: {\n          name: { type: \"string\", description: \"Your name\" },\n          email: { type: \"string\", format: \"email\", description: \"Your email\" },\n        },\n        required: [\"name\"],\n      },\n    },\n    ...overrides,\n  });\n\n  const renderElicitationRequest = (\n    request: PendingElicitationRequest = createMockRequest(),\n  ) => {\n    return render(\n      <ElicitationRequest request={request} onResolve={mockOnResolve} />,\n    );\n  };\n\n  describe(\"Rendering\", () => {\n    it(\"should render the component\", () => {\n      renderElicitationRequest();\n      expect(screen.getByTestId(\"elicitation-request\")).toBeInTheDocument();\n    });\n\n    it(\"should display request message\", () => {\n      const message = \"Please provide your GitHub username\";\n      renderElicitationRequest(\n        createMockRequest({\n          request: {\n            id: 1,\n            message,\n            requestedSchema: {\n              type: \"object\",\n              properties: { name: { type: \"string\" } },\n            },\n          },\n        }),\n      );\n      expect(screen.getByText(message)).toBeInTheDocument();\n    });\n\n    it(\"should render all three action buttons\", () => {\n      renderElicitationRequest();\n      expect(\n        screen.getByRole(\"button\", { name: /cancel/i }),\n      ).toBeInTheDocument();\n      expect(\n        screen.getByRole(\"button\", { name: /decline/i }),\n      ).toBeInTheDocument();\n      expect(\n        screen.getByRole(\"button\", { name: /submit/i }),\n      ).toBeInTheDocument();\n    });\n\n    it(\"should render DynamicJsonForm component\", () => {\n      renderElicitationRequest();\n      expect(screen.getByTestId(\"dynamic-json-form\")).toBeInTheDocument();\n    });\n  });\n\n  describe(\"User Interactions\", () => {\n    it(\"should call onResolve with accept action when Submit button is clicked\", async () => {\n      renderElicitationRequest();\n\n      const input = screen.getByTestId(\"form-input\");\n      await act(async () => {\n        fireEvent.change(input, {\n          target: {\n            value: '{\"name\": \"John Doe\", \"email\": \"john@example.com\"}',\n          },\n        });\n      });\n\n      await act(async () => {\n        fireEvent.click(screen.getByRole(\"button\", { name: /submit/i }));\n      });\n\n      expect(mockOnResolve).toHaveBeenCalledWith(1, {\n        action: \"accept\",\n        content: { name: \"John Doe\", email: \"john@example.com\" },\n      });\n    });\n\n    it(\"should call onResolve with decline action when Decline button is clicked\", async () => {\n      renderElicitationRequest();\n\n      await act(async () => {\n        fireEvent.click(screen.getByRole(\"button\", { name: /decline/i }));\n      });\n\n      expect(mockOnResolve).toHaveBeenCalledWith(1, { action: \"decline\" });\n    });\n\n    it(\"should call onResolve with cancel action when Cancel button is clicked\", async () => {\n      renderElicitationRequest();\n\n      await act(async () => {\n        fireEvent.click(screen.getByRole(\"button\", { name: /cancel/i }));\n      });\n\n      expect(mockOnResolve).toHaveBeenCalledWith(1, { action: \"cancel\" });\n    });\n  });\n\n  describe(\"Form Validation\", () => {\n    it(\"should show validation error for missing required fields\", async () => {\n      renderElicitationRequest();\n\n      await act(async () => {\n        fireEvent.click(screen.getByRole(\"button\", { name: /submit/i }));\n      });\n\n      expect(\n        screen.getByText(/Required field missing: name/),\n      ).toBeInTheDocument();\n      expect(mockOnResolve).not.toHaveBeenCalledWith(\n        1,\n        expect.objectContaining({ action: \"accept\" }),\n      );\n    });\n\n    it(\"should show validation error for invalid email format\", async () => {\n      renderElicitationRequest();\n\n      const input = screen.getByTestId(\"form-input\");\n      await act(async () => {\n        fireEvent.change(input, {\n          target: { value: '{\"name\": \"John\", \"email\": \"invalid-email\"}' },\n        });\n      });\n\n      await act(async () => {\n        fireEvent.click(screen.getByRole(\"button\", { name: /submit/i }));\n      });\n\n      expect(\n        screen.getByText(/Invalid email format: email/),\n      ).toBeInTheDocument();\n      expect(mockOnResolve).not.toHaveBeenCalledWith(\n        1,\n        expect.objectContaining({ action: \"accept\" }),\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/components/__tests__/ElicitationTab.test.tsx",
    "content": "import { render, screen } from \"@testing-library/react\";\nimport { Tabs } from \"@/components/ui/tabs\";\nimport ElicitationTab, { PendingElicitationRequest } from \"../ElicitationTab\";\n\ndescribe(\"Elicitation tab\", () => {\n  const mockOnResolve = jest.fn();\n\n  const renderElicitationTab = (pendingRequests: PendingElicitationRequest[]) =>\n    render(\n      <Tabs defaultValue=\"elicitations\">\n        <ElicitationTab\n          pendingRequests={pendingRequests}\n          onResolve={mockOnResolve}\n        />\n      </Tabs>,\n    );\n\n  it(\"should render 'No pending requests' when there are no pending requests\", () => {\n    renderElicitationTab([]);\n    expect(\n      screen.getByText(\n        \"When the server requests information from the user, requests will appear here for response.\",\n      ),\n    ).toBeTruthy();\n    expect(screen.findByText(\"No pending requests\")).toBeTruthy();\n  });\n\n  it(\"should render the correct number of requests\", () => {\n    renderElicitationTab(\n      Array.from({ length: 3 }, (_, i) => ({\n        id: i,\n        request: {\n          id: i,\n          message: `Please provide information ${i}`,\n          requestedSchema: {\n            type: \"object\",\n            properties: {\n              name: {\n                type: \"string\",\n                description: \"Your name\",\n              },\n            },\n            required: [\"name\"],\n          },\n        },\n      })),\n    );\n    expect(screen.getAllByTestId(\"elicitation-request\").length).toBe(3);\n  });\n});\n"
  },
  {
    "path": "client/src/components/__tests__/HistoryAndNotifications.test.tsx",
    "content": "import { render, screen, fireEvent, within } from \"@testing-library/react\";\nimport { useState } from \"react\";\nimport { describe, it, expect, jest } from \"@jest/globals\";\nimport HistoryAndNotifications from \"../HistoryAndNotifications\";\nimport { ServerNotification } from \"@modelcontextprotocol/sdk/types.js\";\n\n// Mock JsonView component\njest.mock(\"../JsonView\", () => {\n  return function JsonView({ data }: { data: string }) {\n    return <div data-testid=\"json-view\">{data}</div>;\n  };\n});\n\ndescribe(\"HistoryAndNotifications\", () => {\n  const mockRequestHistory = [\n    {\n      request: JSON.stringify({ method: \"test/method1\", params: {} }),\n      response: JSON.stringify({ result: \"success\" }),\n    },\n    {\n      request: JSON.stringify({ method: \"test/method2\", params: {} }),\n      response: JSON.stringify({ result: \"success\" }),\n    },\n  ];\n\n  const mockNotifications: ServerNotification[] = [\n    {\n      method: \"notifications/message\",\n      params: {\n        level: \"info\" as const,\n        data: \"First notification\",\n      },\n    },\n    {\n      method: \"notifications/progress\",\n      params: {\n        progressToken: \"test-token\",\n        progress: 50,\n        total: 100,\n      },\n    },\n  ];\n\n  it(\"renders history and notifications sections\", () => {\n    render(\n      <HistoryAndNotifications\n        requestHistory={mockRequestHistory}\n        serverNotifications={mockNotifications}\n      />,\n    );\n\n    expect(screen.getByText(\"History\")).toBeTruthy();\n    expect(screen.getByText(\"Server Notifications\")).toBeTruthy();\n  });\n\n  it(\"displays request history items with correct numbering\", () => {\n    render(\n      <HistoryAndNotifications\n        requestHistory={mockRequestHistory}\n        serverNotifications={[]}\n      />,\n    );\n\n    // Items should be numbered in reverse order (newest first)\n    expect(screen.getByText(\"2. test/method2\")).toBeTruthy();\n    expect(screen.getByText(\"1. test/method1\")).toBeTruthy();\n  });\n\n  it(\"displays server notifications with correct numbering\", () => {\n    render(\n      <HistoryAndNotifications\n        requestHistory={[]}\n        serverNotifications={mockNotifications}\n      />,\n    );\n\n    // Items should be numbered in reverse order (newest first)\n    expect(screen.getByText(\"2. notifications/progress\")).toBeTruthy();\n    expect(screen.getByText(\"1. notifications/message\")).toBeTruthy();\n  });\n\n  it(\"expands and collapses request items when clicked\", () => {\n    render(\n      <HistoryAndNotifications\n        requestHistory={mockRequestHistory}\n        serverNotifications={[]}\n      />,\n    );\n\n    const firstRequestHeader = screen.getByText(\"2. test/method2\");\n\n    // Initially collapsed - should show ▶ arrows (there are multiple)\n    expect(screen.getAllByText(\"▶\")).toHaveLength(2);\n    expect(screen.queryByText(\"Request:\")).toBeNull();\n\n    // Click to expand\n    fireEvent.click(firstRequestHeader);\n\n    // Should now be expanded - one ▼ and one ▶\n    expect(screen.getByText(\"▼\")).toBeTruthy();\n    expect(screen.getAllByText(\"▶\")).toHaveLength(1);\n    expect(screen.getByText(\"Request:\")).toBeTruthy();\n    expect(screen.getByText(\"Response:\")).toBeTruthy();\n  });\n\n  it(\"expands and collapses notification items when clicked\", () => {\n    render(\n      <HistoryAndNotifications\n        requestHistory={[]}\n        serverNotifications={mockNotifications}\n      />,\n    );\n\n    const firstNotificationHeader = screen.getByText(\n      \"2. notifications/progress\",\n    );\n\n    // Initially collapsed\n    expect(screen.getAllByText(\"▶\")).toHaveLength(2);\n    expect(screen.queryByText(\"Details:\")).toBeNull();\n\n    // Click to expand\n    fireEvent.click(firstNotificationHeader);\n\n    // Should now be expanded\n    expect(screen.getByText(\"▼\")).toBeTruthy();\n    expect(screen.getAllByText(\"▶\")).toHaveLength(1);\n    expect(screen.getByText(\"Details:\")).toBeTruthy();\n  });\n\n  it(\"maintains expanded state when new notifications are added\", () => {\n    const { rerender } = render(\n      <HistoryAndNotifications\n        requestHistory={[]}\n        serverNotifications={mockNotifications}\n      />,\n    );\n\n    // Find and expand the older notification (should be \"1. notifications/message\")\n    const olderNotificationHeader = screen.getByText(\n      \"1. notifications/message\",\n    );\n    fireEvent.click(olderNotificationHeader);\n\n    // Verify it's expanded\n    expect(screen.getByText(\"Details:\")).toBeTruthy();\n\n    // Add a new notification at the beginning (simulating real behavior)\n    const newNotifications: ServerNotification[] = [\n      {\n        method: \"notifications/resources/updated\",\n        params: { uri: \"file://test.txt\" },\n      },\n      ...mockNotifications,\n    ];\n\n    // Re-render with new notifications\n    rerender(\n      <HistoryAndNotifications\n        requestHistory={[]}\n        serverNotifications={newNotifications}\n      />,\n    );\n\n    // The original notification should still be expanded\n    // It's now numbered as \"2. notifications/message\" due to the new item\n    expect(screen.getByText(\"3. notifications/progress\")).toBeTruthy();\n    expect(screen.getByText(\"2. notifications/message\")).toBeTruthy();\n    expect(screen.getByText(\"1. notifications/resources/updated\")).toBeTruthy();\n\n    // The originally expanded notification should still show its details\n    expect(screen.getByText(\"Details:\")).toBeTruthy();\n  });\n\n  it(\"maintains expanded state when new requests are added\", () => {\n    const { rerender } = render(\n      <HistoryAndNotifications\n        requestHistory={mockRequestHistory}\n        serverNotifications={[]}\n      />,\n    );\n\n    // Find and expand the older request (should be \"1. test/method1\")\n    const olderRequestHeader = screen.getByText(\"1. test/method1\");\n    fireEvent.click(olderRequestHeader);\n\n    // Verify it's expanded\n    expect(screen.getByText(\"Request:\")).toBeTruthy();\n    expect(screen.getByText(\"Response:\")).toBeTruthy();\n\n    // Add a new request at the beginning\n    const newRequestHistory = [\n      {\n        request: JSON.stringify({ method: \"test/new_method\", params: {} }),\n        response: JSON.stringify({ result: \"new success\" }),\n      },\n      ...mockRequestHistory,\n    ];\n\n    // Re-render with new request history\n    rerender(\n      <HistoryAndNotifications\n        requestHistory={newRequestHistory}\n        serverNotifications={[]}\n      />,\n    );\n\n    // The original request should still be expanded\n    // It's now numbered as \"2. test/method1\" due to the new item\n    expect(screen.getByText(\"3. test/method2\")).toBeTruthy();\n    expect(screen.getByText(\"2. test/method1\")).toBeTruthy();\n    expect(screen.getByText(\"1. test/new_method\")).toBeTruthy();\n\n    // The originally expanded request should still show its details\n    expect(screen.getByText(\"Request:\")).toBeTruthy();\n    expect(screen.getByText(\"Response:\")).toBeTruthy();\n  });\n\n  it(\"displays empty state messages when no data is available\", () => {\n    render(\n      <HistoryAndNotifications requestHistory={[]} serverNotifications={[]} />,\n    );\n\n    expect(screen.getByText(\"No history yet\")).toBeTruthy();\n    expect(screen.getByText(\"No notifications yet\")).toBeTruthy();\n  });\n\n  it(\"clears request history when Clear is clicked\", () => {\n    const Wrapper = () => {\n      const [history, setHistory] = useState(mockRequestHistory);\n      return (\n        <HistoryAndNotifications\n          requestHistory={history}\n          serverNotifications={[]}\n          onClearHistory={() => setHistory([])}\n        />\n      );\n    };\n\n    render(<Wrapper />);\n\n    // Verify items are present initially\n    expect(screen.getByText(\"2. test/method2\")).toBeTruthy();\n    expect(screen.getByText(\"1. test/method1\")).toBeTruthy();\n\n    // Click Clear in History header (scoped by the History heading's container)\n    const historyHeader = screen.getByText(\"History\");\n    const historyHeaderContainer = historyHeader.parentElement as HTMLElement;\n    const historyClearButton = within(historyHeaderContainer).getByRole(\n      \"button\",\n      { name: \"Clear\" },\n    );\n    fireEvent.click(historyClearButton);\n\n    // History should now be empty\n    expect(screen.getByText(\"No history yet\")).toBeTruthy();\n  });\n\n  it(\"clears server notifications when Clear is clicked\", () => {\n    const Wrapper = () => {\n      const [notifications, setNotifications] =\n        useState<ServerNotification[]>(mockNotifications);\n      return (\n        <HistoryAndNotifications\n          requestHistory={[]}\n          serverNotifications={notifications}\n          onClearNotifications={() => setNotifications([])}\n        />\n      );\n    };\n\n    render(<Wrapper />);\n\n    // Verify items are present initially\n    expect(screen.getByText(\"2. notifications/progress\")).toBeTruthy();\n    expect(screen.getByText(\"1. notifications/message\")).toBeTruthy();\n\n    // Click Clear in Server Notifications header (scoped by its heading's container)\n    const notifHeader = screen.getByText(\"Server Notifications\");\n    const notifHeaderContainer = notifHeader.parentElement as HTMLElement;\n    const notifClearButton = within(notifHeaderContainer).getByRole(\"button\", {\n      name: \"Clear\",\n    });\n    fireEvent.click(notifClearButton);\n\n    // Notifications should now be empty\n    expect(screen.getByText(\"No notifications yet\")).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "client/src/components/__tests__/ListPane.test.tsx",
    "content": "import { render, screen, fireEvent, act } from \"@testing-library/react\";\nimport \"@testing-library/jest-dom\";\nimport { describe, it, beforeEach, jest } from \"@jest/globals\";\nimport ListPane from \"../ListPane\";\n\ndescribe(\"ListPane\", () => {\n  const mockItems = [\n    { id: 1, name: \"Tool 1\", description: \"First tool\" },\n    { id: 2, name: \"Tool 2\", description: \"Second tool\" },\n    { id: 3, name: \"Another Tool\", description: \"Third tool\" },\n  ];\n\n  const defaultProps = {\n    items: mockItems,\n    listItems: jest.fn(),\n    setSelectedItem: jest.fn(),\n    renderItem: (item: (typeof mockItems)[0]) => <div>{item.name}</div>,\n    title: \"List tools\",\n    buttonText: \"Load Tools\",\n  };\n\n  const renderListPane = (props = {}) => {\n    return render(<ListPane {...defaultProps} {...props} />);\n  };\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n  });\n\n  describe(\"Rendering\", () => {\n    it(\"should render with title and button\", () => {\n      renderListPane();\n\n      expect(screen.getByText(\"List tools\")).toBeInTheDocument();\n      expect(\n        screen.getByRole(\"button\", { name: \"Load Tools\" }),\n      ).toBeInTheDocument();\n      expect(\n        screen.queryByRole(\"button\", { name: \"Clear\" }),\n      ).not.toBeInTheDocument();\n    });\n\n    it(\"should render Clear button when clearItems prop is provided\", () => {\n      renderListPane({ clearItems: jest.fn() });\n      expect(screen.getByRole(\"button\", { name: \"Clear\" })).toBeInTheDocument();\n    });\n\n    it(\"should render items when provided\", () => {\n      renderListPane();\n\n      expect(screen.getByText(\"Tool 1\")).toBeInTheDocument();\n      expect(screen.getByText(\"Tool 2\")).toBeInTheDocument();\n      expect(screen.getByText(\"Another Tool\")).toBeInTheDocument();\n    });\n\n    it(\"should render empty state when no items\", () => {\n      renderListPane({ items: [] });\n\n      expect(screen.queryByText(\"Tool 1\")).not.toBeInTheDocument();\n      expect(screen.queryByText(\"Tool 2\")).not.toBeInTheDocument();\n    });\n\n    it(\"should render custom item content\", () => {\n      const customRenderItem = (item: (typeof mockItems)[0]) => (\n        <div>\n          <span>{item.name}</span>\n          <small>{item.description}</small>\n        </div>\n      );\n\n      renderListPane({ renderItem: customRenderItem });\n\n      expect(screen.getByText(\"Tool 1\")).toBeInTheDocument();\n      expect(screen.getByText(\"First tool\")).toBeInTheDocument();\n    });\n  });\n\n  describe(\"Search Functionality\", () => {\n    it(\"should show search icon initially\", () => {\n      renderListPane();\n\n      const searchButton = screen.getByRole(\"button\", { name: \"Search\" });\n      expect(searchButton).toBeInTheDocument();\n      expect(searchButton.querySelector(\"svg\")).toBeInTheDocument();\n    });\n\n    it(\"should expand search input when search icon is clicked\", async () => {\n      renderListPane();\n\n      const searchButton = screen.getByRole(\"button\", { name: \"Search\" });\n      await act(async () => {\n        fireEvent.click(searchButton);\n      });\n\n      const searchInput = screen.getByPlaceholderText(\"Search...\");\n      expect(searchInput).toBeInTheDocument();\n\n      // Wait for the setTimeout to complete and focus to be set\n      await act(async () => {\n        await new Promise((resolve) => setTimeout(resolve, 150));\n      });\n\n      expect(searchInput).toHaveFocus();\n    });\n\n    it(\"should filter items based on search query\", async () => {\n      renderListPane();\n\n      const searchButton = screen.getByRole(\"button\", { name: \"Search\" });\n      await act(async () => {\n        fireEvent.click(searchButton);\n      });\n\n      const searchInput = screen.getByPlaceholderText(\"Search...\");\n      await act(async () => {\n        fireEvent.change(searchInput, { target: { value: \"Tool\" } });\n      });\n\n      expect(screen.getByText(\"Tool 1\")).toBeInTheDocument();\n      expect(screen.getByText(\"Tool 2\")).toBeInTheDocument();\n      expect(screen.getByText(\"Another Tool\")).toBeInTheDocument();\n\n      await act(async () => {\n        fireEvent.change(searchInput, { target: { value: \"Another\" } });\n      });\n\n      expect(screen.queryByText(\"Tool 1\")).not.toBeInTheDocument();\n      expect(screen.queryByText(\"Tool 2\")).not.toBeInTheDocument();\n      expect(screen.getByText(\"Another Tool\")).toBeInTheDocument();\n    });\n\n    it(\"should show 'No items found of matching \\\"NonExistent\\\"' when search has no results\", async () => {\n      renderListPane();\n\n      const searchButton = screen.getByRole(\"button\", { name: \"Search\" });\n      await act(async () => {\n        fireEvent.click(searchButton);\n      });\n\n      const searchInput = screen.getByPlaceholderText(\"Search...\");\n\n      await act(async () => {\n        fireEvent.change(searchInput, { target: { value: \"NonExistent\" } });\n      });\n\n      expect(\n        screen.getByText('No items found matching \"NonExistent\"'),\n      ).toBeInTheDocument();\n      expect(screen.queryByText(\"Tool 1\")).not.toBeInTheDocument();\n    });\n\n    it(\"should collapse search when input is empty and loses focus\", async () => {\n      renderListPane();\n\n      const searchButton = screen.getByRole(\"button\", { name: \"Search\" });\n      await act(async () => {\n        fireEvent.click(searchButton);\n      });\n\n      const searchInput = screen.getByPlaceholderText(\"Search...\");\n\n      await act(async () => {\n        fireEvent.change(searchInput, { target: { value: \"test\" } });\n        fireEvent.change(searchInput, { target: { value: \"\" } });\n        fireEvent.blur(searchInput);\n      });\n\n      const searchButtonAfterCollapse = screen.getByRole(\"button\", {\n        name: \"Search\",\n      });\n      expect(searchButtonAfterCollapse).toBeInTheDocument();\n      expect(searchButtonAfterCollapse).not.toHaveClass(\"opacity-0\");\n    });\n\n    it(\"should keep search expanded when input has content and loses focus\", async () => {\n      renderListPane();\n\n      const searchButton = screen.getByRole(\"button\", { name: \"Search\" });\n      await act(async () => {\n        fireEvent.click(searchButton);\n      });\n\n      const searchInput = screen.getByPlaceholderText(\"Search...\");\n      await act(async () => {\n        fireEvent.change(searchInput, { target: { value: \"test\" } });\n        fireEvent.blur(searchInput);\n      });\n\n      expect(screen.getByPlaceholderText(\"Search...\")).toBeInTheDocument();\n    });\n\n    it(\"should search through all item properties (description)\", async () => {\n      renderListPane();\n\n      const searchButton = screen.getByRole(\"button\", { name: \"Search\" });\n      await act(async () => {\n        fireEvent.click(searchButton);\n      });\n\n      const searchInput = screen.getByPlaceholderText(\"Search...\");\n      await act(async () => {\n        fireEvent.change(searchInput, { target: { value: \"First tool\" } });\n      });\n\n      expect(screen.getByText(\"Tool 1\")).toBeInTheDocument();\n      expect(screen.queryByText(\"Tool 2\")).not.toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/components/__tests__/MetadataTab.test.tsx",
    "content": "import { render, screen, fireEvent } from \"@testing-library/react\";\nimport \"@testing-library/jest-dom\";\nimport MetadataTab from \"../MetadataTab\";\nimport { Tabs } from \"@/components/ui/tabs\";\nimport {\n  META_NAME_RULES_MESSAGE,\n  META_PREFIX_RULES_MESSAGE,\n  RESERVED_NAMESPACE_MESSAGE,\n} from \"@/utils/metaUtils\";\n\ndescribe(\"MetadataTab\", () => {\n  const defaultProps = {\n    metadata: {},\n    onMetadataChange: jest.fn(),\n  };\n\n  const renderMetadataTab = (props = {}) => {\n    return render(\n      <Tabs defaultValue=\"metadata\">\n        <MetadataTab {...defaultProps} {...props} />\n      </Tabs>,\n    );\n  };\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n  });\n\n  describe(\"Initial Rendering\", () => {\n    it(\"should render the metadata tab with title and description\", () => {\n      renderMetadataTab();\n\n      expect(screen.getByText(\"Metadata\")).toBeInTheDocument();\n      expect(\n        screen.getByText(\n          \"Key-value pairs that will be included in all MCP requests\",\n        ),\n      ).toBeInTheDocument();\n    });\n\n    it(\"should render Add Entry button\", () => {\n      renderMetadataTab();\n\n      const addButton = screen.getByRole(\"button\", { name: /add entry/i });\n      expect(addButton).toBeInTheDocument();\n    });\n\n    it(\"should show empty state message when no entries exist\", () => {\n      renderMetadataTab();\n\n      expect(\n        screen.getByText(\n          'No metadata entries. Click \"Add Entry\" to add key-value pairs.',\n        ),\n      ).toBeInTheDocument();\n    });\n\n    it(\"should not show empty state message when entries exist\", () => {\n      renderMetadataTab({\n        metadata: { key1: \"value1\" },\n      });\n\n      expect(\n        screen.queryByText(\n          'No metadata entries. Click \"Add Entry\" to add key-value pairs.',\n        ),\n      ).not.toBeInTheDocument();\n    });\n  });\n\n  describe(\"Initial Data Handling\", () => {\n    it(\"should initialize with existing metadata\", () => {\n      const initialMetadata = {\n        API_KEY: \"test-key\",\n        VERSION: \"1.0.0\",\n      };\n\n      renderMetadataTab({ metadata: initialMetadata });\n\n      expect(screen.getByDisplayValue(\"API_KEY\")).toBeInTheDocument();\n      expect(screen.getByDisplayValue(\"test-key\")).toBeInTheDocument();\n      expect(screen.getByDisplayValue(\"VERSION\")).toBeInTheDocument();\n      expect(screen.getByDisplayValue(\"1.0.0\")).toBeInTheDocument();\n    });\n\n    it(\"should render multiple entries in correct order\", () => {\n      const initialMetadata = {\n        FIRST: \"first-value\",\n        SECOND: \"second-value\",\n        THIRD: \"third-value\",\n      };\n\n      renderMetadataTab({ metadata: initialMetadata });\n\n      const keyInputs = screen.getAllByPlaceholderText(\"Key\");\n      const valueInputs = screen.getAllByPlaceholderText(\"Value\");\n\n      expect(keyInputs).toHaveLength(3);\n      expect(valueInputs).toHaveLength(3);\n\n      // Check that entries are rendered in the order they appear in the object\n      const entries = Object.entries(initialMetadata);\n      entries.forEach(([key, value], index) => {\n        expect(keyInputs[index]).toHaveValue(key);\n        expect(valueInputs[index]).toHaveValue(value);\n      });\n    });\n  });\n\n  describe(\"Adding Entries\", () => {\n    it(\"should add a new empty entry when Add Entry button is clicked\", () => {\n      renderMetadataTab();\n\n      const addButton = screen.getByRole(\"button\", { name: /add entry/i });\n      fireEvent.click(addButton);\n\n      const keyInputs = screen.getAllByPlaceholderText(\"Key\");\n      const valueInputs = screen.getAllByPlaceholderText(\"Value\");\n\n      expect(keyInputs).toHaveLength(1);\n      expect(valueInputs).toHaveLength(1);\n      expect(keyInputs[0]).toHaveValue(\"\");\n      expect(valueInputs[0]).toHaveValue(\"\");\n    });\n\n    it(\"should add multiple entries when Add Entry button is clicked multiple times\", () => {\n      renderMetadataTab();\n\n      const addButton = screen.getByRole(\"button\", { name: /add entry/i });\n\n      fireEvent.click(addButton);\n      fireEvent.click(addButton);\n      fireEvent.click(addButton);\n\n      const keyInputs = screen.getAllByPlaceholderText(\"Key\");\n      const valueInputs = screen.getAllByPlaceholderText(\"Value\");\n\n      expect(keyInputs).toHaveLength(3);\n      expect(valueInputs).toHaveLength(3);\n    });\n\n    it(\"should hide empty state message after adding first entry\", () => {\n      renderMetadataTab();\n\n      expect(\n        screen.getByText(\n          'No metadata entries. Click \"Add Entry\" to add key-value pairs.',\n        ),\n      ).toBeInTheDocument();\n\n      const addButton = screen.getByRole(\"button\", { name: /add entry/i });\n      fireEvent.click(addButton);\n\n      expect(\n        screen.queryByText(\n          'No metadata entries. Click \"Add Entry\" to add key-value pairs.',\n        ),\n      ).not.toBeInTheDocument();\n    });\n  });\n\n  describe(\"Removing Entries\", () => {\n    it(\"should render remove button for each entry\", () => {\n      renderMetadataTab({\n        metadata: { key1: \"value1\", key2: \"value2\" },\n      });\n\n      const removeButtons = screen.getAllByRole(\"button\", { name: \"\" }); // Trash icon buttons have no text\n      expect(removeButtons).toHaveLength(2);\n    });\n\n    it(\"should remove entry when remove button is clicked\", () => {\n      const onMetadataChange = jest.fn();\n      renderMetadataTab({\n        metadata: { key1: \"value1\", key2: \"value2\" },\n        onMetadataChange,\n      });\n\n      const removeButtons = screen.getAllByRole(\"button\", { name: \"\" });\n      fireEvent.click(removeButtons[0]);\n\n      expect(onMetadataChange).toHaveBeenCalledWith({ key2: \"value2\" });\n    });\n\n    it(\"should remove correct entry when multiple entries exist\", () => {\n      const onMetadataChange = jest.fn();\n      renderMetadataTab({\n        metadata: {\n          FIRST: \"first-value\",\n          SECOND: \"second-value\",\n          THIRD: \"third-value\",\n        },\n        onMetadataChange,\n      });\n\n      const removeButtons = screen.getAllByRole(\"button\", { name: \"\" });\n      fireEvent.click(removeButtons[1]); // Remove second entry\n\n      expect(onMetadataChange).toHaveBeenCalledWith({\n        FIRST: \"first-value\",\n        THIRD: \"third-value\",\n      });\n    });\n\n    it(\"should show empty state message after removing all entries\", () => {\n      const onMetadataChange = jest.fn();\n      renderMetadataTab({\n        metadata: { key1: \"value1\" },\n        onMetadataChange,\n      });\n\n      const removeButton = screen.getByRole(\"button\", { name: \"\" });\n      fireEvent.click(removeButton);\n\n      expect(onMetadataChange).toHaveBeenCalledWith({});\n    });\n  });\n\n  describe(\"Editing Entries\", () => {\n    it(\"should update key when key input is changed\", () => {\n      const onMetadataChange = jest.fn();\n      renderMetadataTab({\n        metadata: { oldKey: \"value1\" },\n        onMetadataChange,\n      });\n\n      const keyInput = screen.getByDisplayValue(\"oldKey\");\n      fireEvent.change(keyInput, { target: { value: \"newKey\" } });\n\n      expect(onMetadataChange).toHaveBeenCalledWith({ newKey: \"value1\" });\n    });\n\n    it(\"should update value when value input is changed\", () => {\n      const onMetadataChange = jest.fn();\n      renderMetadataTab({\n        metadata: { key1: \"oldValue\" },\n        onMetadataChange,\n      });\n\n      const valueInput = screen.getByDisplayValue(\"oldValue\");\n      fireEvent.change(valueInput, { target: { value: \"newValue\" } });\n\n      expect(onMetadataChange).toHaveBeenCalledWith({ key1: \"newValue\" });\n    });\n\n    it(\"should handle editing multiple entries independently\", () => {\n      const onMetadataChange = jest.fn();\n      renderMetadataTab({\n        metadata: {\n          key1: \"value1\",\n          key2: \"value2\",\n        },\n        onMetadataChange,\n      });\n\n      const keyInputs = screen.getAllByPlaceholderText(\"Key\");\n      const valueInputs = screen.getAllByPlaceholderText(\"Value\");\n\n      // Edit first entry key\n      fireEvent.change(keyInputs[0], { target: { value: \"newKey1\" } });\n      expect(onMetadataChange).toHaveBeenCalledWith({\n        newKey1: \"value1\",\n        key2: \"value2\",\n      });\n\n      // Clear mock to test second edit independently\n      onMetadataChange.mockClear();\n\n      // Edit second entry value\n      fireEvent.change(valueInputs[1], { target: { value: \"newValue2\" } });\n      expect(onMetadataChange).toHaveBeenCalledWith({\n        newKey1: \"value1\",\n        key2: \"newValue2\",\n      });\n    });\n  });\n\n  describe(\"Reserved Metadata Keys\", () => {\n    test.each`\n      description                             | value                             | message                       | shouldDisableValue\n      ${\"reserved keys with prefix\"}          | ${\"modelcontextprotocol.io/flip\"} | ${RESERVED_NAMESPACE_MESSAGE} | ${true}\n      ${\"reserved root without slash\"}        | ${\"modelcontextprotocol.io\"}      | ${RESERVED_NAMESPACE_MESSAGE} | ${true}\n      ${\"nested modelcontextprotocol domain\"} | ${\"api.modelcontextprotocol.org\"} | ${RESERVED_NAMESPACE_MESSAGE} | ${false}\n      ${\"nested mcp domain\"}                  | ${\"tools.mcp.com/path\"}           | ${RESERVED_NAMESPACE_MESSAGE} | ${false}\n      ${\"invalid name segments\"}              | ${\"custom/bad-\"}                  | ${META_NAME_RULES_MESSAGE}    | ${false}\n      ${\"invalid prefix labels\"}              | ${\"1invalid-prefix/value\"}        | ${META_PREFIX_RULES_MESSAGE}  | ${false}\n    `(\n      \"should display an error for $description\",\n      ({ value, message, shouldDisableValue }) => {\n        const onMetadataChange = jest.fn();\n        renderMetadataTab({ onMetadataChange });\n\n        const addButton = screen.getByRole(\"button\", { name: /add entry/i });\n        fireEvent.click(addButton);\n\n        const keyInput = screen.getByPlaceholderText(\"Key\");\n        fireEvent.change(keyInput, { target: { value } });\n\n        const valueInput = screen.getByPlaceholderText(\"Value\");\n        if (shouldDisableValue) {\n          expect(valueInput).toBeDisabled();\n        }\n\n        expect(screen.getByText(message)).toBeInTheDocument();\n        expect(onMetadataChange).toHaveBeenLastCalledWith({});\n      },\n    );\n  });\n\n  describe(\"Data Validation and Trimming\", () => {\n    it(\"should trim whitespace from keys and values\", () => {\n      const onMetadataChange = jest.fn();\n      renderMetadataTab({ onMetadataChange });\n\n      const addButton = screen.getByRole(\"button\", { name: /add entry/i });\n      fireEvent.click(addButton);\n\n      const keyInput = screen.getByPlaceholderText(\"Key\");\n      const valueInput = screen.getByPlaceholderText(\"Value\");\n\n      fireEvent.change(keyInput, { target: { value: \"  trimmedKey  \" } });\n      fireEvent.change(valueInput, { target: { value: \"  trimmedValue  \" } });\n\n      expect(onMetadataChange).toHaveBeenCalledWith({\n        trimmedKey: \"trimmedValue\",\n      });\n    });\n\n    it(\"should exclude entries with empty keys or values after trimming\", () => {\n      const onMetadataChange = jest.fn();\n      renderMetadataTab({ onMetadataChange });\n\n      const addButton = screen.getByRole(\"button\", { name: /add entry/i });\n      fireEvent.click(addButton);\n      fireEvent.click(addButton);\n\n      const keyInputs = screen.getAllByPlaceholderText(\"Key\");\n      const valueInputs = screen.getAllByPlaceholderText(\"Value\");\n\n      // First entry: valid key and value\n      fireEvent.change(keyInputs[0], { target: { value: \"validKey\" } });\n      fireEvent.change(valueInputs[0], { target: { value: \"validValue\" } });\n\n      // Second entry: empty key (should be excluded)\n      fireEvent.change(keyInputs[1], { target: { value: \"\" } });\n      fireEvent.change(valueInputs[1], { target: { value: \"someValue\" } });\n\n      expect(onMetadataChange).toHaveBeenCalledWith({\n        validKey: \"validValue\",\n      });\n    });\n\n    it(\"should exclude entries with whitespace-only keys or values\", () => {\n      const onMetadataChange = jest.fn();\n      renderMetadataTab({ onMetadataChange });\n\n      const addButton = screen.getByRole(\"button\", { name: /add entry/i });\n      fireEvent.click(addButton);\n      fireEvent.click(addButton);\n\n      const keyInputs = screen.getAllByPlaceholderText(\"Key\");\n      const valueInputs = screen.getAllByPlaceholderText(\"Value\");\n\n      // First entry: valid key and value\n      fireEvent.change(keyInputs[0], { target: { value: \"validKey\" } });\n      fireEvent.change(valueInputs[0], { target: { value: \"validValue\" } });\n\n      // Second entry: whitespace-only key (should be excluded)\n      fireEvent.change(keyInputs[1], { target: { value: \"   \" } });\n      fireEvent.change(valueInputs[1], { target: { value: \"someValue\" } });\n\n      expect(onMetadataChange).toHaveBeenCalledWith({\n        validKey: \"validValue\",\n      });\n    });\n\n    it(\"should handle mixed valid and invalid entries\", () => {\n      const onMetadataChange = jest.fn();\n      renderMetadataTab({ onMetadataChange });\n\n      const addButton = screen.getByRole(\"button\", { name: /add entry/i });\n      fireEvent.click(addButton);\n      fireEvent.click(addButton);\n      fireEvent.click(addButton);\n\n      const keyInputs = screen.getAllByPlaceholderText(\"Key\");\n      const valueInputs = screen.getAllByPlaceholderText(\"Value\");\n\n      // First entry: valid\n      fireEvent.change(keyInputs[0], { target: { value: \"key1\" } });\n      fireEvent.change(valueInputs[0], { target: { value: \"value1\" } });\n\n      // Second entry: empty key (invalid)\n      fireEvent.change(keyInputs[1], { target: { value: \"\" } });\n      fireEvent.change(valueInputs[1], { target: { value: \"value2\" } });\n\n      // Third entry: valid\n      fireEvent.change(keyInputs[2], { target: { value: \"key3\" } });\n      fireEvent.change(valueInputs[2], { target: { value: \"value3\" } });\n\n      expect(onMetadataChange).toHaveBeenCalledWith({\n        key1: \"value1\",\n        key3: \"value3\",\n      });\n    });\n  });\n\n  describe(\"Input Accessibility\", () => {\n    it(\"should have proper labels for screen readers\", () => {\n      renderMetadataTab({\n        metadata: { key1: \"value1\" },\n      });\n\n      const keyLabel = screen.getByLabelText(\"Key\", { selector: \"input\" });\n      const valueLabel = screen.getByLabelText(\"Value\", { selector: \"input\" });\n\n      expect(keyLabel).toBeInTheDocument();\n      expect(valueLabel).toBeInTheDocument();\n    });\n\n    it(\"should have unique IDs for each input pair\", () => {\n      renderMetadataTab({\n        metadata: {\n          key1: \"value1\",\n          key2: \"value2\",\n        },\n      });\n\n      const keyInputs = screen.getAllByPlaceholderText(\"Key\");\n      const valueInputs = screen.getAllByPlaceholderText(\"Value\");\n\n      expect(keyInputs[0]).toHaveAttribute(\"id\", \"key-0\");\n      expect(keyInputs[1]).toHaveAttribute(\"id\", \"key-1\");\n      expect(valueInputs[0]).toHaveAttribute(\"id\", \"value-0\");\n      expect(valueInputs[1]).toHaveAttribute(\"id\", \"value-1\");\n    });\n\n    it(\"should have proper placeholder text\", () => {\n      renderMetadataTab();\n\n      const addButton = screen.getByRole(\"button\", { name: /add entry/i });\n      fireEvent.click(addButton);\n\n      const keyInput = screen.getByPlaceholderText(\"Key\");\n      const valueInput = screen.getByPlaceholderText(\"Value\");\n\n      expect(keyInput).toHaveAttribute(\"placeholder\", \"Key\");\n      expect(valueInput).toHaveAttribute(\"placeholder\", \"Value\");\n    });\n  });\n\n  describe(\"Edge Cases\", () => {\n    it(\"should flag invalid names that contain unsupported characters\", () => {\n      const onMetadataChange = jest.fn();\n      renderMetadataTab({ onMetadataChange });\n\n      const addButton = screen.getByRole(\"button\", { name: /add entry/i });\n      fireEvent.click(addButton);\n\n      const keyInput = screen.getByPlaceholderText(\"Key\");\n      const valueInput = screen.getByPlaceholderText(\"Value\");\n\n      fireEvent.change(keyInput, {\n        target: { value: \"key-with-special@chars!\" },\n      });\n      fireEvent.change(valueInput, {\n        target: { value: \"value with spaces & symbols $%^\" },\n      });\n\n      expect(screen.getByText(META_NAME_RULES_MESSAGE)).toBeInTheDocument();\n      expect(onMetadataChange).toHaveBeenLastCalledWith({});\n    });\n\n    it(\"should reject unicode names that do not start with an alphanumeric character\", () => {\n      const onMetadataChange = jest.fn();\n      renderMetadataTab({ onMetadataChange });\n\n      const addButton = screen.getByRole(\"button\", { name: /add entry/i });\n      fireEvent.click(addButton);\n\n      const keyInput = screen.getByPlaceholderText(\"Key\");\n      const valueInput = screen.getByPlaceholderText(\"Value\");\n\n      fireEvent.change(keyInput, { target: { value: \"🔑_key\" } });\n      fireEvent.change(valueInput, { target: { value: \"值_value_🎯\" } });\n\n      expect(screen.getByText(META_NAME_RULES_MESSAGE)).toBeInTheDocument();\n      expect(onMetadataChange).toHaveBeenLastCalledWith({});\n    });\n\n    it(\"should handle very long keys and values\", () => {\n      const onMetadataChange = jest.fn();\n      renderMetadataTab({ onMetadataChange });\n\n      const addButton = screen.getByRole(\"button\", { name: /add entry/i });\n      fireEvent.click(addButton);\n\n      const keyInput = screen.getByPlaceholderText(\"Key\");\n      const valueInput = screen.getByPlaceholderText(\"Value\");\n\n      const longKey = \"A\".repeat(100);\n      const longValue = \"B\".repeat(500);\n\n      fireEvent.change(keyInput, { target: { value: longKey } });\n      fireEvent.change(valueInput, { target: { value: longValue } });\n\n      expect(onMetadataChange).toHaveBeenCalledWith({\n        [longKey]: longValue,\n      });\n    });\n\n    it(\"should handle duplicate keys by keeping the last one\", () => {\n      const onMetadataChange = jest.fn();\n      renderMetadataTab({ onMetadataChange });\n\n      const addButton = screen.getByRole(\"button\", { name: /add entry/i });\n      fireEvent.click(addButton);\n      fireEvent.click(addButton);\n\n      const keyInputs = screen.getAllByPlaceholderText(\"Key\");\n      const valueInputs = screen.getAllByPlaceholderText(\"Value\");\n\n      // Set same key for both entries\n      fireEvent.change(keyInputs[0], { target: { value: \"duplicateKey\" } });\n      fireEvent.change(valueInputs[0], { target: { value: \"firstValue\" } });\n\n      fireEvent.change(keyInputs[1], { target: { value: \"duplicateKey\" } });\n      fireEvent.change(valueInputs[1], { target: { value: \"secondValue\" } });\n\n      // The second value should overwrite the first\n      expect(onMetadataChange).toHaveBeenCalledWith({\n        duplicateKey: \"secondValue\",\n      });\n    });\n  });\n\n  describe(\"Integration with Parent Component\", () => {\n    it(\"should not call onMetadataChange when component mounts with existing data\", () => {\n      const onMetadataChange = jest.fn();\n      renderMetadataTab({\n        metadata: { key1: \"value1\" },\n        onMetadataChange,\n      });\n\n      expect(onMetadataChange).not.toHaveBeenCalled();\n    });\n\n    it(\"should call onMetadataChange only when user makes changes\", () => {\n      const onMetadataChange = jest.fn();\n      renderMetadataTab({\n        metadata: { key1: \"value1\" },\n        onMetadataChange,\n      });\n\n      // Should not be called on mount\n      expect(onMetadataChange).not.toHaveBeenCalled();\n\n      // Should be called when user changes value\n      const valueInput = screen.getByDisplayValue(\"value1\");\n      fireEvent.change(valueInput, { target: { value: \"newValue\" } });\n\n      expect(onMetadataChange).toHaveBeenCalledTimes(1);\n      expect(onMetadataChange).toHaveBeenCalledWith({ key1: \"newValue\" });\n    });\n\n    it(\"should maintain internal state when props change (component doesn't sync with prop changes)\", () => {\n      const { rerender } = renderMetadataTab({\n        metadata: { key1: \"value1\" },\n      });\n\n      expect(screen.getByDisplayValue(\"key1\")).toBeInTheDocument();\n      expect(screen.getByDisplayValue(\"value1\")).toBeInTheDocument();\n\n      // Rerender with different props - component should maintain its internal state\n      // This is the intended behavior since useState initializer only runs once\n      rerender(\n        <Tabs defaultValue=\"metadata\">\n          <MetadataTab\n            metadata={{ key2: \"value2\", key3: \"value3\" }}\n            onMetadataChange={jest.fn()}\n          />\n        </Tabs>,\n      );\n\n      // The component should still show the original values since it maintains internal state\n      expect(screen.getByDisplayValue(\"key1\")).toBeInTheDocument();\n      expect(screen.getByDisplayValue(\"value1\")).toBeInTheDocument();\n      // The new prop values should not be displayed\n      expect(screen.queryByDisplayValue(\"key2\")).not.toBeInTheDocument();\n      expect(screen.queryByDisplayValue(\"value2\")).not.toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/components/__tests__/ResourcesTab.test.tsx",
    "content": "import { render, screen, fireEvent } from \"@testing-library/react\";\nimport \"@testing-library/jest-dom\";\nimport { Tabs } from \"../ui/tabs\";\nimport ResourcesTab from \"../ResourcesTab\";\nimport { ResourceTemplate, Resource } from \"@modelcontextprotocol/sdk/types.js\";\n\n// Mock the hooks and components\njest.mock(\"@/lib/hooks/useCompletionState\", () => ({\n  useCompletionState: () => ({\n    completions: {},\n    clearCompletions: jest.fn(),\n    requestCompletions: jest.fn(),\n  }),\n}));\n\njest.mock(\"../JsonView\", () => {\n  return function MockJsonView({ data }: { data: string }) {\n    return <div data-testid=\"json-view\">{data}</div>;\n  };\n});\n\njest.mock(\"@/components/ui/combobox\", () => ({\n  Combobox: ({\n    id,\n    value,\n    onChange,\n    placeholder,\n  }: {\n    id: string;\n    value: string;\n    onChange: (value: string) => void;\n    placeholder: string;\n  }) => (\n    <input\n      id={id}\n      value={value || \"\"}\n      onChange={(e) => onChange(e.target.value)}\n      placeholder={placeholder}\n      data-testid={`combobox-${id}`}\n    />\n  ),\n}));\n\njest.mock(\"@/components/ui/label\", () => ({\n  Label: ({\n    htmlFor,\n    children,\n  }: {\n    htmlFor: string;\n    children: React.ReactNode;\n  }) => (\n    <label htmlFor={htmlFor} data-testid={`label-${htmlFor}`}>\n      {children}\n    </label>\n  ),\n}));\n\njest.mock(\"@/components/ui/button\", () => ({\n  Button: ({\n    children,\n    onClick,\n    disabled,\n    ...props\n  }: {\n    children: React.ReactNode;\n    onClick?: () => void;\n    disabled?: boolean;\n    [key: string]: unknown;\n  }) => (\n    <button\n      onClick={onClick}\n      disabled={disabled}\n      data-testid=\"button\"\n      {...props}\n    >\n      {children}\n    </button>\n  ),\n}));\n\ndescribe(\"ResourcesTab - Template Query Parameters\", () => {\n  const mockListResources = jest.fn();\n  const mockClearResources = jest.fn();\n  const mockListResourceTemplates = jest.fn();\n  const mockClearResourceTemplates = jest.fn();\n  const mockReadResource = jest.fn();\n  const mockSetSelectedResource = jest.fn();\n  const mockHandleCompletion = jest.fn();\n  const mockSubscribeToResource = jest.fn();\n  const mockUnsubscribeFromResource = jest.fn();\n\n  const mockResourceTemplate: ResourceTemplate = {\n    name: \"Users API\",\n    uriTemplate: \"test://users{?name,limit,offset}\",\n    description: \"Fetch users with optional filtering and pagination\",\n  };\n\n  const mockResource: Resource = {\n    uri: \"test://users?name=john&limit=10&offset=0\",\n    name: \"Users Resource\",\n    description: \"Expanded users resource\",\n  };\n\n  const defaultProps = {\n    resources: [],\n    resourceTemplates: [mockResourceTemplate],\n    listResources: mockListResources,\n    clearResources: mockClearResources,\n    listResourceTemplates: mockListResourceTemplates,\n    clearResourceTemplates: mockClearResourceTemplates,\n    readResource: mockReadResource,\n    selectedResource: null,\n    setSelectedResource: mockSetSelectedResource,\n    handleCompletion: mockHandleCompletion,\n    completionsSupported: true,\n    resourceContent: \"\",\n    nextCursor: undefined,\n    nextTemplateCursor: undefined,\n    error: null,\n    resourceSubscriptionsSupported: false,\n    resourceSubscriptions: new Set<string>(),\n    subscribeToResource: mockSubscribeToResource,\n    unsubscribeFromResource: mockUnsubscribeFromResource,\n  };\n\n  const renderResourcesTab = (props = {}) =>\n    render(\n      <Tabs defaultValue=\"resources\">\n        <ResourcesTab {...defaultProps} {...props} />\n      </Tabs>,\n    );\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n  });\n\n  it(\"should parse and display template variables from URI template\", () => {\n    renderResourcesTab();\n\n    // Click on the resource template to select it\n    fireEvent.click(screen.getByText(\"Users API\"));\n\n    // Check that input fields are rendered for each template variable\n    expect(screen.getByTestId(\"combobox-name\")).toBeInTheDocument();\n    expect(screen.getByTestId(\"combobox-limit\")).toBeInTheDocument();\n    expect(screen.getByTestId(\"combobox-offset\")).toBeInTheDocument();\n  });\n\n  it(\"should display template description when template is selected\", () => {\n    renderResourcesTab();\n\n    // Click on the resource template to select it\n    fireEvent.click(screen.getByText(\"Users API\"));\n\n    expect(\n      screen.getByText(\"Fetch users with optional filtering and pagination\"),\n    ).toBeInTheDocument();\n  });\n\n  it(\"should handle template value changes\", () => {\n    renderResourcesTab();\n\n    // Click on the resource template to select it\n    fireEvent.click(screen.getByText(\"Users API\"));\n\n    // Find and fill template value inputs\n    const nameInput = screen.getByTestId(\"combobox-name\");\n    const limitInput = screen.getByTestId(\"combobox-limit\");\n    const offsetInput = screen.getByTestId(\"combobox-offset\");\n\n    fireEvent.change(nameInput, { target: { value: \"john\" } });\n    fireEvent.change(limitInput, { target: { value: \"10\" } });\n    fireEvent.change(offsetInput, { target: { value: \"0\" } });\n\n    expect(nameInput).toHaveValue(\"john\");\n    expect(limitInput).toHaveValue(\"10\");\n    expect(offsetInput).toHaveValue(\"0\");\n  });\n\n  it(\"should expand template and read resource when Read Resource button is clicked\", async () => {\n    renderResourcesTab();\n\n    // Click on the resource template to select it\n    fireEvent.click(screen.getByText(\"Users API\"));\n\n    // Fill template values\n    const nameInput = screen.getByTestId(\"combobox-name\");\n    const limitInput = screen.getByTestId(\"combobox-limit\");\n    const offsetInput = screen.getByTestId(\"combobox-offset\");\n\n    fireEvent.change(nameInput, { target: { value: \"john\" } });\n    fireEvent.change(limitInput, { target: { value: \"10\" } });\n    fireEvent.change(offsetInput, { target: { value: \"0\" } });\n\n    // Click Read Resource button\n    const readResourceButton = screen.getByText(\"Read Resource\");\n    expect(readResourceButton).not.toBeDisabled();\n\n    fireEvent.click(readResourceButton);\n\n    // Verify that readResource was called with the expanded URI\n    expect(mockReadResource).toHaveBeenCalledWith(\n      \"test://users?name=john&limit=10&offset=0\",\n    );\n\n    // Verify that setSelectedResource was called with the expanded resource\n    expect(mockSetSelectedResource).toHaveBeenCalledWith({\n      uri: \"test://users?name=john&limit=10&offset=0\",\n      name: \"test://users?name=john&limit=10&offset=0\",\n    });\n  });\n\n  it(\"should disable Read Resource button when no template values are provided\", () => {\n    renderResourcesTab();\n\n    // Click on the resource template to select it\n    fireEvent.click(screen.getByText(\"Users API\"));\n\n    // Read Resource button should be disabled when no values are provided\n    const readResourceButton = screen.getByText(\"Read Resource\");\n    expect(readResourceButton).toBeDisabled();\n  });\n\n  it(\"should handle partial template values correctly\", () => {\n    renderResourcesTab();\n\n    // Click on the resource template to select it\n    fireEvent.click(screen.getByText(\"Users API\"));\n\n    // Fill only some template values\n    const nameInput = screen.getByTestId(\"combobox-name\");\n    fireEvent.change(nameInput, { target: { value: \"john\" } });\n\n    // Read Resource button should be enabled with partial values\n    const readResourceButton = screen.getByText(\"Read Resource\");\n    expect(readResourceButton).not.toBeDisabled();\n\n    fireEvent.click(readResourceButton);\n\n    // Should expand with only the provided values\n    expect(mockReadResource).toHaveBeenCalledWith(\"test://users?name=john\");\n  });\n\n  it(\"should handle special characters in template values\", () => {\n    renderResourcesTab();\n\n    // Click on the resource template to select it\n    fireEvent.click(screen.getByText(\"Users API\"));\n\n    // Fill template values with special characters\n    const nameInput = screen.getByTestId(\"combobox-name\");\n    fireEvent.change(nameInput, { target: { value: \"john doe\" } });\n\n    fireEvent.click(screen.getByText(\"Read Resource\"));\n\n    // Should properly encode special characters\n    expect(mockReadResource).toHaveBeenCalledWith(\n      \"test://users?name=john%20doe\",\n    );\n  });\n\n  it(\"should clear template values when switching between templates\", () => {\n    const anotherTemplate: ResourceTemplate = {\n      name: \"Posts API\",\n      uriTemplate: \"test://posts{?author,category}\",\n      description: \"Fetch posts by author and category\",\n    };\n\n    renderResourcesTab({\n      resourceTemplates: [mockResourceTemplate, anotherTemplate],\n    });\n\n    // Select first template and fill values\n    fireEvent.click(screen.getByText(\"Users API\"));\n    const nameInput = screen.getByTestId(\"combobox-name\");\n    fireEvent.change(nameInput, { target: { value: \"john\" } });\n\n    // Switch to second template\n    fireEvent.click(screen.getByText(\"Posts API\"));\n\n    // Should show new template fields and clear previous values\n    expect(screen.getByTestId(\"combobox-author\")).toBeInTheDocument();\n    expect(screen.getByTestId(\"combobox-category\")).toBeInTheDocument();\n    expect(screen.queryByTestId(\"combobox-name\")).not.toBeInTheDocument();\n  });\n\n  it(\"should display resource content when a resource is selected\", () => {\n    const resourceContent = '{\"users\": [{\"id\": 1, \"name\": \"John\"}]}';\n\n    renderResourcesTab({\n      selectedResource: mockResource,\n      resourceContent: resourceContent,\n    });\n\n    expect(screen.getByTestId(\"json-view\")).toBeInTheDocument();\n    expect(screen.getByText(resourceContent)).toBeInTheDocument();\n  });\n\n  it(\"should show alert when no resource or template is selected\", () => {\n    renderResourcesTab();\n\n    expect(\n      screen.getByText(\n        \"Select a resource or template from the list to view its contents\",\n      ),\n    ).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/components/__tests__/Sidebar.test.tsx",
    "content": "import { render, screen, fireEvent, act } from \"@testing-library/react\";\nimport \"@testing-library/jest-dom\";\nimport { describe, it, beforeEach, jest } from \"@jest/globals\";\nimport Sidebar from \"../Sidebar\";\nimport { DEFAULT_INSPECTOR_CONFIG } from \"@/lib/constants\";\nimport { InspectorConfig } from \"@/lib/configurationTypes\";\nimport { TooltipProvider } from \"@/components/ui/tooltip\";\n\n// Mock theme hook\njest.mock(\"../../lib/hooks/useTheme\", () => ({\n  __esModule: true,\n  default: () => [\"light\", jest.fn()],\n}));\n\n// Mock toast hook\nconst mockToast = jest.fn();\njest.mock(\"@/lib/hooks/useToast\", () => ({\n  useToast: () => ({\n    toast: mockToast,\n  }),\n}));\n\n// Mock navigator clipboard\nconst mockClipboardWrite = jest.fn(() => Promise.resolve());\nObject.defineProperty(navigator, \"clipboard\", {\n  value: {\n    writeText: mockClipboardWrite,\n  },\n});\n\n// Setup fake timers\njest.useFakeTimers();\n\ndescribe(\"Sidebar\", () => {\n  const defaultProps = {\n    connectionStatus: \"disconnected\" as const,\n    transportType: \"stdio\" as const,\n    setTransportType: jest.fn(),\n    command: \"\",\n    setCommand: jest.fn(),\n    args: \"\",\n    setArgs: jest.fn(),\n    sseUrl: \"\",\n    setSseUrl: jest.fn(),\n    oauthClientId: \"\",\n    setOauthClientId: jest.fn(),\n    oauthClientSecret: \"\",\n    setOauthClientSecret: jest.fn(),\n    oauthScope: \"\",\n    setOauthScope: jest.fn(),\n    env: {},\n    setEnv: jest.fn(),\n    customHeaders: [],\n    setCustomHeaders: jest.fn(),\n    onConnect: jest.fn(),\n    onDisconnect: jest.fn(),\n    stdErrNotifications: [],\n    clearStdErrNotifications: jest.fn(),\n    logLevel: \"info\" as const,\n    sendLogLevelRequest: jest.fn(),\n    loggingSupported: true,\n    config: DEFAULT_INSPECTOR_CONFIG,\n    setConfig: jest.fn(),\n    connectionType: \"proxy\" as const,\n    setConnectionType: jest.fn(),\n  };\n\n  const renderSidebar = (props = {}) => {\n    return render(\n      <TooltipProvider>\n        <Sidebar {...defaultProps} {...props} />\n      </TooltipProvider>,\n    );\n  };\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n    jest.clearAllTimers();\n  });\n\n  describe(\"Command and arguments\", () => {\n    it(\"should trim whitespace from command input on blur\", () => {\n      const setCommand = jest.fn();\n      renderSidebar({ command: \"  node server.js  \", setCommand });\n\n      const commandInput = screen.getByLabelText(\"Command\");\n\n      fireEvent.blur(commandInput);\n      expect(setCommand).toHaveBeenLastCalledWith(\"node server.js\");\n    });\n\n    it(\"should handle whitespace-only command input on blur\", () => {\n      const setCommand = jest.fn();\n      renderSidebar({ command: \"   \", setCommand });\n\n      const commandInput = screen.getByLabelText(\"Command\");\n\n      fireEvent.blur(commandInput);\n      expect(setCommand).toHaveBeenLastCalledWith(\"\");\n    });\n\n    it(\"should not affect command without surrounding whitespace\", () => {\n      const setCommand = jest.fn();\n      renderSidebar({ command: \"node\", setCommand });\n\n      const commandInput = screen.getByLabelText(\"Command\");\n\n      fireEvent.blur(commandInput);\n      expect(setCommand).toHaveBeenLastCalledWith(\"node\");\n    });\n  });\n\n  describe(\"Environment Variables\", () => {\n    const openEnvVarsSection = () => {\n      const button = screen.getByTestId(\"env-vars-button\");\n      fireEvent.click(button);\n    };\n\n    describe(\"Basic Operations\", () => {\n      it(\"should add a new environment variable\", () => {\n        const setEnv = jest.fn();\n        renderSidebar({ env: {}, setEnv });\n\n        openEnvVarsSection();\n\n        const addButton = screen.getByText(\"Add Environment Variable\");\n        fireEvent.click(addButton);\n\n        expect(setEnv).toHaveBeenCalledWith({ \"\": \"\" });\n      });\n\n      it(\"should remove an environment variable\", () => {\n        const setEnv = jest.fn();\n        const initialEnv = { TEST_KEY: \"test_value\" };\n        renderSidebar({ env: initialEnv, setEnv });\n\n        openEnvVarsSection();\n\n        const removeButton = screen.getByRole(\"button\", { name: \"×\" });\n        fireEvent.click(removeButton);\n\n        expect(setEnv).toHaveBeenCalledWith({});\n      });\n\n      it(\"should update environment variable value\", () => {\n        const setEnv = jest.fn();\n        const initialEnv = { TEST_KEY: \"test_value\" };\n        renderSidebar({ env: initialEnv, setEnv });\n\n        openEnvVarsSection();\n\n        const valueInput = screen.getByDisplayValue(\"test_value\");\n        fireEvent.change(valueInput, { target: { value: \"new_value\" } });\n\n        expect(setEnv).toHaveBeenCalledWith({ TEST_KEY: \"new_value\" });\n      });\n\n      it(\"should toggle value visibility\", () => {\n        const initialEnv = { TEST_KEY: \"test_value\" };\n        renderSidebar({ env: initialEnv });\n\n        openEnvVarsSection();\n\n        const valueInput = screen.getByDisplayValue(\"test_value\");\n        expect(valueInput).toHaveProperty(\"type\", \"password\");\n\n        const toggleButton = screen.getByRole(\"button\", {\n          name: /show value/i,\n        });\n        fireEvent.click(toggleButton);\n\n        expect(valueInput).toHaveProperty(\"type\", \"text\");\n      });\n    });\n\n    describe(\"Key Editing\", () => {\n      it(\"should maintain order when editing first key\", () => {\n        const setEnv = jest.fn();\n        const initialEnv = {\n          FIRST_KEY: \"first_value\",\n          SECOND_KEY: \"second_value\",\n          THIRD_KEY: \"third_value\",\n        };\n        renderSidebar({ env: initialEnv, setEnv });\n\n        openEnvVarsSection();\n\n        const firstKeyInput = screen.getByDisplayValue(\"FIRST_KEY\");\n        fireEvent.change(firstKeyInput, { target: { value: \"NEW_FIRST_KEY\" } });\n\n        expect(setEnv).toHaveBeenCalledWith({\n          NEW_FIRST_KEY: \"first_value\",\n          SECOND_KEY: \"second_value\",\n          THIRD_KEY: \"third_value\",\n        });\n      });\n\n      it(\"should maintain order when editing middle key\", () => {\n        const setEnv = jest.fn();\n        const initialEnv = {\n          FIRST_KEY: \"first_value\",\n          SECOND_KEY: \"second_value\",\n          THIRD_KEY: \"third_value\",\n        };\n        renderSidebar({ env: initialEnv, setEnv });\n\n        openEnvVarsSection();\n\n        const middleKeyInput = screen.getByDisplayValue(\"SECOND_KEY\");\n        fireEvent.change(middleKeyInput, {\n          target: { value: \"NEW_SECOND_KEY\" },\n        });\n\n        expect(setEnv).toHaveBeenCalledWith({\n          FIRST_KEY: \"first_value\",\n          NEW_SECOND_KEY: \"second_value\",\n          THIRD_KEY: \"third_value\",\n        });\n      });\n\n      it(\"should maintain order when editing last key\", () => {\n        const setEnv = jest.fn();\n        const initialEnv = {\n          FIRST_KEY: \"first_value\",\n          SECOND_KEY: \"second_value\",\n          THIRD_KEY: \"third_value\",\n        };\n        renderSidebar({ env: initialEnv, setEnv });\n\n        openEnvVarsSection();\n\n        const lastKeyInput = screen.getByDisplayValue(\"THIRD_KEY\");\n        fireEvent.change(lastKeyInput, { target: { value: \"NEW_THIRD_KEY\" } });\n\n        expect(setEnv).toHaveBeenCalledWith({\n          FIRST_KEY: \"first_value\",\n          SECOND_KEY: \"second_value\",\n          NEW_THIRD_KEY: \"third_value\",\n        });\n      });\n\n      it(\"should maintain order during key editing\", () => {\n        const setEnv = jest.fn();\n        const initialEnv = {\n          KEY1: \"value1\",\n          KEY2: \"value2\",\n        };\n        renderSidebar({ env: initialEnv, setEnv });\n\n        openEnvVarsSection();\n\n        // Type \"NEW_\" one character at a time\n        const key1Input = screen.getByDisplayValue(\"KEY1\");\n        \"NEW_\".split(\"\").forEach((char) => {\n          fireEvent.change(key1Input, {\n            target: { value: char + \"KEY1\".slice(1) },\n          });\n        });\n\n        // Verify the last setEnv call maintains the order\n        const lastCall = setEnv.mock.calls[\n          setEnv.mock.calls.length - 1\n        ][0] as Record<string, string>;\n        const entries = Object.entries(lastCall);\n\n        // The values should stay with their original keys\n        expect(entries[0][1]).toBe(\"value1\"); // First entry should still have value1\n        expect(entries[1][1]).toBe(\"value2\"); // Second entry should still have value2\n      });\n    });\n\n    describe(\"Multiple Operations\", () => {\n      it(\"should maintain state after multiple key edits\", () => {\n        const setEnv = jest.fn();\n        const initialEnv = {\n          FIRST_KEY: \"first_value\",\n          SECOND_KEY: \"second_value\",\n        };\n        const { rerender } = renderSidebar({ env: initialEnv, setEnv });\n\n        openEnvVarsSection();\n\n        // First key edit\n        const firstKeyInput = screen.getByDisplayValue(\"FIRST_KEY\");\n        fireEvent.change(firstKeyInput, { target: { value: \"NEW_FIRST_KEY\" } });\n\n        // Get the updated env from the first setEnv call\n        const updatedEnv = setEnv.mock.calls[0][0] as Record<string, string>;\n\n        // Rerender with the updated env\n        rerender(\n          <TooltipProvider>\n            <Sidebar {...defaultProps} env={updatedEnv} setEnv={setEnv} />\n          </TooltipProvider>,\n        );\n\n        // Second key edit\n        const secondKeyInput = screen.getByDisplayValue(\"SECOND_KEY\");\n        fireEvent.change(secondKeyInput, {\n          target: { value: \"NEW_SECOND_KEY\" },\n        });\n\n        // Verify the final state matches what we expect\n        expect(setEnv).toHaveBeenLastCalledWith({\n          NEW_FIRST_KEY: \"first_value\",\n          NEW_SECOND_KEY: \"second_value\",\n        });\n      });\n\n      it(\"should maintain visibility state after key edit\", () => {\n        const initialEnv = { TEST_KEY: \"test_value\" };\n        const { rerender } = renderSidebar({ env: initialEnv });\n\n        openEnvVarsSection();\n\n        // Show the value\n        const toggleButton = screen.getByRole(\"button\", {\n          name: /show value/i,\n        });\n        fireEvent.click(toggleButton);\n\n        const valueInput = screen.getByDisplayValue(\"test_value\");\n        expect(valueInput).toHaveProperty(\"type\", \"text\");\n\n        // Edit the key\n        const keyInput = screen.getByDisplayValue(\"TEST_KEY\");\n        fireEvent.change(keyInput, { target: { value: \"NEW_KEY\" } });\n\n        // Rerender with updated env\n        rerender(\n          <TooltipProvider>\n            <Sidebar {...defaultProps} env={{ NEW_KEY: \"test_value\" }} />\n          </TooltipProvider>,\n        );\n\n        // Value should still be visible\n        const updatedValueInput = screen.getByDisplayValue(\"test_value\");\n        expect(updatedValueInput).toHaveProperty(\"type\", \"text\");\n      });\n    });\n\n    describe(\"Edge Cases\", () => {\n      it(\"should handle empty key\", () => {\n        const setEnv = jest.fn();\n        const initialEnv = { TEST_KEY: \"test_value\" };\n        renderSidebar({ env: initialEnv, setEnv });\n\n        openEnvVarsSection();\n\n        const keyInput = screen.getByDisplayValue(\"TEST_KEY\");\n        fireEvent.change(keyInput, { target: { value: \"\" } });\n\n        expect(setEnv).toHaveBeenCalledWith({ \"\": \"test_value\" });\n      });\n\n      it(\"should handle special characters in key\", () => {\n        const setEnv = jest.fn();\n        const initialEnv = { TEST_KEY: \"test_value\" };\n        renderSidebar({ env: initialEnv, setEnv });\n\n        openEnvVarsSection();\n\n        const keyInput = screen.getByDisplayValue(\"TEST_KEY\");\n        fireEvent.change(keyInput, { target: { value: \"TEST-KEY@123\" } });\n\n        expect(setEnv).toHaveBeenCalledWith({ \"TEST-KEY@123\": \"test_value\" });\n      });\n\n      it(\"should handle unicode characters in key\", () => {\n        const setEnv = jest.fn();\n        const initialEnv = { TEST_KEY: \"test_value\" };\n        renderSidebar({ env: initialEnv, setEnv });\n\n        openEnvVarsSection();\n\n        const keyInput = screen.getByDisplayValue(\"TEST_KEY\");\n        fireEvent.change(keyInput, { target: { value: \"TEST_🔑\" } });\n\n        expect(setEnv).toHaveBeenCalledWith({ \"TEST_🔑\": \"test_value\" });\n      });\n\n      it(\"should handle a very long key name\", () => {\n        const setEnv = jest.fn();\n        const initialEnv = { TEST_KEY: \"test_value\" };\n        renderSidebar({ env: initialEnv, setEnv });\n\n        openEnvVarsSection();\n\n        const keyInput = screen.getByDisplayValue(\"TEST_KEY\");\n        const longKey = \"A\".repeat(100);\n        fireEvent.change(keyInput, { target: { value: longKey } });\n\n        expect(setEnv).toHaveBeenCalledWith({ [longKey]: \"test_value\" });\n      });\n    });\n  });\n\n  describe(\"Copy Server Features\", () => {\n    const getCopyButtons = () => {\n      return {\n        serverEntry: screen.getByRole(\"button\", { name: /server entry/i }),\n        serversFile: screen.getByRole(\"button\", { name: /servers file/i }),\n      };\n    };\n\n    it(\"should render both copy buttons for all transport types\", () => {\n      [\"stdio\", \"sse\", \"streamable-http\"].forEach((transportType) => {\n        renderSidebar({ transportType });\n        // There should be exactly one Server Entry and one Servers File button per render\n        const serverEntryButtons = screen.getAllByRole(\"button\", {\n          name: /server entry/i,\n        });\n        const serversFileButtons = screen.getAllByRole(\"button\", {\n          name: /servers file/i,\n        });\n        expect(serverEntryButtons).toHaveLength(1);\n        expect(serversFileButtons).toHaveLength(1);\n        // Clean up DOM for next iteration\n        // (Testing Library's render does not auto-unmount in a loop)\n        document.body.innerHTML = \"\";\n      });\n    });\n\n    it(\"should copy server entry configuration to clipboard for STDIO transport\", async () => {\n      const command = \"node\";\n      const args = \"--inspect server.js\";\n      const env = { API_KEY: \"test-key\", DEBUG: \"true\" };\n\n      renderSidebar({\n        transportType: \"stdio\",\n        command,\n        args,\n        env,\n      });\n\n      await act(async () => {\n        const { serverEntry } = getCopyButtons();\n        fireEvent.click(serverEntry);\n        jest.runAllTimers();\n      });\n\n      expect(mockClipboardWrite).toHaveBeenCalledTimes(1);\n      const expectedConfig = JSON.stringify(\n        {\n          command,\n          args: [\"--inspect\", \"server.js\"],\n          env,\n        },\n        null,\n        4,\n      );\n      expect(mockClipboardWrite).toHaveBeenCalledWith(expectedConfig);\n      expect(mockToast).toHaveBeenCalledWith({\n        title: \"Config entry copied\",\n        description:\n          \"Server configuration has been copied to clipboard. Add this to your mcp.json inside the 'mcpServers' object with your preferred server name.\",\n      });\n    });\n\n    it(\"should copy servers file configuration to clipboard for STDIO transport\", async () => {\n      const command = \"node\";\n      const args = \"--inspect server.js\";\n      const env = { API_KEY: \"test-key\", DEBUG: \"true\" };\n\n      renderSidebar({\n        transportType: \"stdio\",\n        command,\n        args,\n        env,\n      });\n\n      await act(async () => {\n        const { serversFile } = getCopyButtons();\n        fireEvent.click(serversFile);\n        jest.runAllTimers();\n      });\n\n      expect(mockClipboardWrite).toHaveBeenCalledTimes(1);\n      const expectedConfig = JSON.stringify(\n        {\n          mcpServers: {\n            \"default-server\": {\n              command,\n              args: [\"--inspect\", \"server.js\"],\n              env,\n            },\n          },\n        },\n        null,\n        4,\n      );\n      expect(mockClipboardWrite).toHaveBeenCalledWith(expectedConfig);\n    });\n\n    it(\"should copy server entry configuration to clipboard for SSE transport\", async () => {\n      const sseUrl = \"http://localhost:3000/events\";\n      renderSidebar({ transportType: \"sse\", sseUrl });\n\n      await act(async () => {\n        const { serverEntry } = getCopyButtons();\n        fireEvent.click(serverEntry);\n        jest.runAllTimers();\n      });\n\n      expect(mockClipboardWrite).toHaveBeenCalledTimes(1);\n      const expectedConfig = JSON.stringify(\n        {\n          type: \"sse\",\n          url: sseUrl,\n          note: \"For SSE connections, add this URL directly in your MCP Client\",\n        },\n        null,\n        4,\n      );\n      expect(mockClipboardWrite).toHaveBeenCalledWith(expectedConfig);\n      expect(mockToast).toHaveBeenCalledWith({\n        title: \"Config entry copied\",\n        description:\n          \"SSE URL has been copied. Use this URL directly in your MCP Client.\",\n      });\n    });\n\n    it(\"should copy servers file configuration to clipboard for SSE transport\", async () => {\n      const sseUrl = \"http://localhost:3000/events\";\n      renderSidebar({ transportType: \"sse\", sseUrl });\n\n      await act(async () => {\n        const { serversFile } = getCopyButtons();\n        fireEvent.click(serversFile);\n        jest.runAllTimers();\n      });\n\n      expect(mockClipboardWrite).toHaveBeenCalledTimes(1);\n      const expectedConfig = JSON.stringify(\n        {\n          mcpServers: {\n            \"default-server\": {\n              type: \"sse\",\n              url: sseUrl,\n              note: \"For SSE connections, add this URL directly in your MCP Client\",\n            },\n          },\n        },\n        null,\n        4,\n      );\n      expect(mockClipboardWrite).toHaveBeenCalledWith(expectedConfig);\n    });\n\n    it(\"should copy server entry configuration to clipboard for streamable-http transport\", async () => {\n      const sseUrl = \"http://localhost:3001/sse\";\n      renderSidebar({ transportType: \"streamable-http\", sseUrl });\n\n      await act(async () => {\n        const { serverEntry } = getCopyButtons();\n        fireEvent.click(serverEntry);\n        jest.runAllTimers();\n      });\n\n      expect(mockClipboardWrite).toHaveBeenCalledTimes(1);\n      const expectedConfig = JSON.stringify(\n        {\n          type: \"streamable-http\",\n          url: sseUrl,\n          note: \"For Streamable HTTP connections, add this URL directly in your MCP Client\",\n        },\n        null,\n        4,\n      );\n      expect(mockClipboardWrite).toHaveBeenCalledWith(expectedConfig);\n      expect(mockToast).toHaveBeenCalledWith({\n        title: \"Config entry copied\",\n        description:\n          \"Streamable HTTP URL has been copied. Use this URL directly in your MCP Client.\",\n      });\n    });\n\n    it(\"should copy servers file configuration to clipboard for streamable-http transport\", async () => {\n      const sseUrl = \"http://localhost:3001/sse\";\n      renderSidebar({ transportType: \"streamable-http\", sseUrl });\n\n      await act(async () => {\n        const { serversFile } = getCopyButtons();\n        fireEvent.click(serversFile);\n        jest.runAllTimers();\n      });\n\n      expect(mockClipboardWrite).toHaveBeenCalledTimes(1);\n      const expectedConfig = JSON.stringify(\n        {\n          mcpServers: {\n            \"default-server\": {\n              type: \"streamable-http\",\n              url: sseUrl,\n              note: \"For Streamable HTTP connections, add this URL directly in your MCP Client\",\n            },\n          },\n        },\n        null,\n        4,\n      );\n      expect(mockClipboardWrite).toHaveBeenCalledWith(expectedConfig);\n    });\n\n    it(\"should handle empty args in STDIO transport\", async () => {\n      const command = \"python\";\n      const args = \"\";\n\n      renderSidebar({\n        transportType: \"stdio\",\n        command,\n        args,\n      });\n\n      await act(async () => {\n        const { serverEntry } = getCopyButtons();\n        fireEvent.click(serverEntry);\n        jest.runAllTimers();\n      });\n\n      expect(mockClipboardWrite).toHaveBeenCalledTimes(1);\n      const expectedConfig = JSON.stringify(\n        {\n          command,\n          args: [],\n          env: {},\n        },\n        null,\n        4,\n      );\n      expect(mockClipboardWrite).toHaveBeenCalledWith(expectedConfig);\n    });\n  });\n\n  describe(\"Authentication\", () => {\n    const openAuthSection = () => {\n      const button = screen.getByTestId(\"auth-button\");\n      fireEvent.click(button);\n    };\n\n    it(\"should update bearer token via custom headers\", async () => {\n      const setCustomHeaders = jest.fn();\n      renderSidebar({\n        customHeaders: [],\n        setCustomHeaders,\n        transportType: \"sse\", // Set transport type to SSE\n      });\n\n      openAuthSection();\n\n      // Add a new header\n      const addButton = screen.getByTestId(\"add-header-button\");\n      fireEvent.click(addButton);\n\n      // Verify that setCustomHeaders was called to add an empty header\n      expect(setCustomHeaders).toHaveBeenCalledWith([\n        {\n          name: \"\",\n          value: \"\",\n          enabled: true,\n        },\n      ]);\n    });\n\n    it(\"should update header name via custom headers\", () => {\n      const setCustomHeaders = jest.fn();\n      renderSidebar({\n        customHeaders: [\n          {\n            name: \"Authorization\",\n            value: \"Bearer token123\",\n            enabled: true,\n          },\n        ],\n        setCustomHeaders,\n        transportType: \"sse\",\n      });\n\n      openAuthSection();\n\n      const headerNameInput = screen.getByTestId(\"header-name-input-0\");\n      fireEvent.change(headerNameInput, { target: { value: \"X-Custom-Auth\" } });\n\n      expect(setCustomHeaders).toHaveBeenCalledWith([\n        {\n          name: \"X-Custom-Auth\",\n          value: \"Bearer token123\",\n          enabled: true,\n        },\n      ]);\n    });\n\n    it(\"should clear bearer token via custom headers\", () => {\n      const setCustomHeaders = jest.fn();\n      renderSidebar({\n        customHeaders: [\n          {\n            name: \"Authorization\",\n            value: \"Bearer existing_token\",\n            enabled: true,\n          },\n        ],\n        setCustomHeaders,\n        transportType: \"sse\", // Set transport type to SSE\n      });\n\n      openAuthSection();\n\n      const headerValueInput = screen.getByTestId(\"header-value-input-0\");\n      fireEvent.change(headerValueInput, { target: { value: \"\" } });\n\n      expect(setCustomHeaders).toHaveBeenCalledWith([\n        {\n          name: \"Authorization\",\n          value: \"\",\n          enabled: true,\n        },\n      ]);\n    });\n\n    it(\"should properly render header value input as password field\", () => {\n      const { rerender } = renderSidebar({\n        customHeaders: [\n          {\n            name: \"Authorization\",\n            value: \"Bearer existing_token\",\n            enabled: true,\n          },\n        ],\n        transportType: \"sse\", // Set transport type to SSE\n      });\n\n      openAuthSection();\n\n      // Token input should be a password field\n      const tokenInput = screen.getByTestId(\"header-value-input-0\");\n      expect(tokenInput).toHaveProperty(\"type\", \"password\");\n\n      // Update the token\n      fireEvent.change(tokenInput, { target: { value: \"Bearer new_token\" } });\n\n      // Rerender with updated token\n      rerender(\n        <TooltipProvider>\n          <Sidebar\n            {...defaultProps}\n            customHeaders={[\n              {\n                name: \"Authorization\",\n                value: \"Bearer new_token\",\n                enabled: true,\n              },\n            ]}\n            transportType=\"sse\"\n          />\n        </TooltipProvider>,\n      );\n\n      // Token input should still exist after update\n      expect(screen.getByTestId(\"header-value-input-0\")).toBeInTheDocument();\n    });\n\n    it(\"should maintain token visibility state after update\", () => {\n      const { rerender } = renderSidebar({\n        customHeaders: [\n          {\n            name: \"Authorization\",\n            value: \"Bearer existing_token\",\n            enabled: true,\n          },\n        ],\n        transportType: \"sse\", // Set transport type to SSE\n      });\n\n      openAuthSection();\n\n      // Token input should be a password field\n      const tokenInput = screen.getByTestId(\"header-value-input-0\");\n      expect(tokenInput).toHaveProperty(\"type\", \"password\");\n\n      // Update the token\n      fireEvent.change(tokenInput, { target: { value: \"Bearer new_token\" } });\n\n      // Rerender with updated token\n      rerender(\n        <TooltipProvider>\n          <Sidebar\n            {...defaultProps}\n            customHeaders={[\n              {\n                name: \"Authorization\",\n                value: \"Bearer new_token\",\n                enabled: true,\n              },\n            ]}\n            transportType=\"sse\"\n          />\n        </TooltipProvider>,\n      );\n\n      // Token input should still exist after update\n      expect(screen.getByTestId(\"header-value-input-0\")).toBeInTheDocument();\n    });\n\n    it(\"should maintain header name when toggling auth section\", () => {\n      renderSidebar({\n        customHeaders: [\n          {\n            name: \"X-API-Key\",\n            value: \"api-key-123\",\n            enabled: true,\n          },\n        ],\n        transportType: \"sse\",\n      });\n\n      // Open auth section\n      openAuthSection();\n\n      // Verify header name is displayed\n      const headerInput = screen.getByTestId(\"header-name-input-0\");\n      expect(headerInput).toHaveValue(\"X-API-Key\");\n\n      // Close auth section\n      const authButton = screen.getByTestId(\"auth-button\");\n      fireEvent.click(authButton);\n\n      // Reopen auth section\n      fireEvent.click(authButton);\n\n      // Verify header name is still preserved\n      expect(screen.getByTestId(\"header-name-input-0\")).toHaveValue(\n        \"X-API-Key\",\n      );\n    });\n\n    it(\"should display placeholder for header name when no headers exist\", () => {\n      renderSidebar({\n        customHeaders: [],\n        transportType: \"sse\",\n      });\n\n      openAuthSection();\n\n      // Verify that the \"No custom headers configured\" message is shown\n      expect(\n        screen.getByText(\"No custom headers configured\"),\n      ).toBeInTheDocument();\n      expect(\n        screen.getByText('Click \"Add\" to get started'),\n      ).toBeInTheDocument();\n\n      // Verify the Add button is present\n      const addButton = screen.getByTestId(\"add-header-button\");\n      expect(addButton).toBeInTheDocument();\n    });\n\n    it(\"should allow editing existing headers\", () => {\n      const setCustomHeaders = jest.fn();\n      renderSidebar({\n        customHeaders: [\n          {\n            name: \"Authorization\",\n            value: \"Bearer token123\",\n            enabled: true,\n          },\n        ],\n        setCustomHeaders,\n        transportType: \"sse\",\n      });\n\n      openAuthSection();\n\n      // Verify header inputs are rendered\n      const headerNameInput = screen.getByTestId(\"header-name-input-0\");\n      const headerValueInput = screen.getByTestId(\"header-value-input-0\");\n\n      expect(headerNameInput).toHaveValue(\"Authorization\");\n      expect(headerValueInput).toHaveValue(\"Bearer token123\");\n      expect(headerNameInput).toHaveAttribute(\"placeholder\", \"Header Name\");\n      expect(headerValueInput).toHaveAttribute(\"placeholder\", \"Header Value\");\n\n      // Update header name\n      fireEvent.change(headerNameInput, { target: { value: \"X-API-Key\" } });\n      expect(setCustomHeaders).toHaveBeenCalledWith([\n        {\n          name: \"X-API-Key\",\n          value: \"Bearer token123\",\n          enabled: true,\n        },\n      ]);\n    });\n  });\n\n  describe(\"Configuration\", () => {\n    const openConfigSection = () => {\n      const button = screen.getByTestId(\"config-button\");\n      fireEvent.click(button);\n    };\n\n    it(\"should update MCP server request timeout\", () => {\n      const setConfig = jest.fn();\n      renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });\n\n      openConfigSection();\n\n      const timeoutInput = screen.getByTestId(\n        \"MCP_SERVER_REQUEST_TIMEOUT-input\",\n      );\n      fireEvent.change(timeoutInput, { target: { value: \"5000\" } });\n\n      expect(setConfig).toHaveBeenCalledWith(\n        expect.objectContaining({\n          MCP_SERVER_REQUEST_TIMEOUT: {\n            label: \"Request Timeout\",\n            description:\n              \"Client-side timeout (ms) - Inspector will cancel requests after this time\",\n            value: 5000,\n            is_session_item: false,\n          },\n        }),\n      );\n    });\n\n    it(\"should update MCP server proxy address\", () => {\n      const setConfig = jest.fn();\n      renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });\n\n      openConfigSection();\n\n      const proxyAddressInput = screen.getByTestId(\n        \"MCP_PROXY_FULL_ADDRESS-input\",\n      );\n      fireEvent.change(proxyAddressInput, {\n        target: { value: \"http://localhost:8080\" },\n      });\n\n      expect(setConfig).toHaveBeenCalledWith(\n        expect.objectContaining({\n          MCP_PROXY_FULL_ADDRESS: {\n            label: \"Inspector Proxy Address\",\n            description:\n              \"Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577\",\n            value: \"http://localhost:8080\",\n            is_session_item: false,\n          },\n        }),\n      );\n    });\n\n    it(\"should update max total timeout\", () => {\n      const setConfig = jest.fn();\n      renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });\n\n      openConfigSection();\n\n      const maxTotalTimeoutInput = screen.getByTestId(\n        \"MCP_REQUEST_MAX_TOTAL_TIMEOUT-input\",\n      );\n      fireEvent.change(maxTotalTimeoutInput, {\n        target: { value: \"10000\" },\n      });\n\n      expect(setConfig).toHaveBeenCalledWith(\n        expect.objectContaining({\n          MCP_REQUEST_MAX_TOTAL_TIMEOUT: {\n            label: \"Maximum Total Timeout\",\n            description:\n              \"Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)\",\n            value: 10000,\n            is_session_item: false,\n          },\n        }),\n      );\n    });\n\n    it(\"should handle invalid timeout values entered by user\", () => {\n      const setConfig = jest.fn();\n      renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });\n\n      openConfigSection();\n\n      const timeoutInput = screen.getByTestId(\n        \"MCP_SERVER_REQUEST_TIMEOUT-input\",\n      );\n      fireEvent.change(timeoutInput, { target: { value: \"abc1\" } });\n\n      expect(setConfig).toHaveBeenCalledWith(\n        expect.objectContaining({\n          MCP_SERVER_REQUEST_TIMEOUT: {\n            label: \"Request Timeout\",\n            description:\n              \"Client-side timeout (ms) - Inspector will cancel requests after this time\",\n            value: 0,\n            is_session_item: false,\n          },\n        }),\n      );\n    });\n\n    it(\"should maintain configuration state after multiple updates\", () => {\n      const setConfig = jest.fn();\n      const { rerender } = renderSidebar({\n        config: DEFAULT_INSPECTOR_CONFIG,\n        setConfig,\n      });\n\n      openConfigSection();\n      // First update\n      const timeoutInput = screen.getByTestId(\n        \"MCP_SERVER_REQUEST_TIMEOUT-input\",\n      );\n      fireEvent.change(timeoutInput, { target: { value: \"5000\" } });\n\n      // Get the updated config from the first setConfig call\n      const updatedConfig = setConfig.mock.calls[0][0] as InspectorConfig;\n\n      // Rerender with the updated config\n      rerender(\n        <TooltipProvider>\n          <Sidebar\n            {...defaultProps}\n            config={updatedConfig}\n            setConfig={setConfig}\n          />\n        </TooltipProvider>,\n      );\n\n      // Second update\n      const updatedTimeoutInput = screen.getByTestId(\n        \"MCP_SERVER_REQUEST_TIMEOUT-input\",\n      );\n      fireEvent.change(updatedTimeoutInput, { target: { value: \"3000\" } });\n\n      // Verify the final state matches what we expect\n      expect(setConfig).toHaveBeenLastCalledWith(\n        expect.objectContaining({\n          MCP_SERVER_REQUEST_TIMEOUT: {\n            label: \"Request Timeout\",\n            description:\n              \"Client-side timeout (ms) - Inspector will cancel requests after this time\",\n            value: 3000,\n            is_session_item: false,\n          },\n        }),\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/components/__tests__/ToolsTab.test.tsx",
    "content": "import { render, screen, fireEvent, act } from \"@testing-library/react\";\nimport \"@testing-library/jest-dom\";\nimport { describe, it, jest, beforeEach } from \"@jest/globals\";\nimport ToolsTab, { ExtendedTool } from \"../ToolsTab\";\nimport { Tool } from \"@modelcontextprotocol/sdk/types.js\";\nimport { Tabs } from \"../ui/tabs\";\nimport { cacheToolOutputSchemas } from \"../../utils/schemaUtils\";\nimport { within } from \"@testing-library/react\";\nimport {\n  META_NAME_RULES_MESSAGE,\n  META_PREFIX_RULES_MESSAGE,\n  RESERVED_NAMESPACE_MESSAGE,\n} from \"../../utils/metaUtils\";\n\ndescribe(\"ToolsTab\", () => {\n  beforeEach(() => {\n    // Clear the output schema cache before each test\n    cacheToolOutputSchemas([]);\n  });\n\n  const mockTools: Tool[] = [\n    {\n      name: \"tool1\",\n      description: \"First tool\",\n      inputSchema: {\n        type: \"object\" as const,\n        properties: {\n          num: { type: \"number\" as const },\n        },\n      },\n    },\n    {\n      name: \"tool3\",\n      description: \"Integer tool\",\n      inputSchema: {\n        type: \"object\" as const,\n        properties: {\n          count: { type: \"integer\" as const },\n        },\n      },\n    },\n    {\n      name: \"tool2\",\n      description: \"Second tool\",\n      inputSchema: {\n        type: \"object\" as const,\n        properties: {\n          num: { type: \"number\" as const },\n        },\n      },\n    },\n    {\n      name: \"tool4\",\n      description: \"Tool with nullable field\",\n      inputSchema: {\n        type: \"object\" as const,\n        properties: {\n          num: { type: [\"number\", \"null\"] as const },\n        },\n      },\n    },\n  ];\n\n  const defaultProps = {\n    tools: mockTools,\n    listTools: jest.fn(),\n    clearTools: jest.fn(),\n    callTool: jest.fn(async () => {}),\n    selectedTool: null,\n    setSelectedTool: jest.fn(),\n    toolResult: null,\n    nextCursor: \"\",\n    error: null,\n    resourceContent: {},\n    onReadResource: jest.fn(),\n    serverSupportsTaskRequests: true,\n  };\n\n  const renderToolsTab = (props = {}) => {\n    return render(\n      <Tabs defaultValue=\"tools\">\n        <ToolsTab {...defaultProps} {...props} />\n      </Tabs>,\n    );\n  };\n\n  it(\"should reset input values when switching tools\", async () => {\n    const { rerender } = renderToolsTab({\n      selectedTool: mockTools[0],\n    });\n\n    // Enter a value in the first tool's input\n    const input = screen.getByRole(\"spinbutton\") as HTMLInputElement;\n    await act(async () => {\n      fireEvent.change(input, { target: { value: \"42\" } });\n    });\n    expect(input.value).toBe(\"42\");\n\n    // Switch to second tool\n    rerender(\n      <Tabs defaultValue=\"tools\">\n        <ToolsTab {...defaultProps} selectedTool={mockTools[2]} />\n      </Tabs>,\n    );\n\n    // Verify input is reset\n    const newInput = screen.getByRole(\"spinbutton\") as HTMLInputElement;\n    expect(newInput.value).toBe(\"\");\n  });\n\n  it(\"should show/hide/disable run-as-task checkbox based on taskSupport\", async () => {\n    const forbiddenTool: ExtendedTool = {\n      ...mockTools[0],\n      name: \"forbiddenTool\",\n      execution: { taskSupport: \"forbidden\" },\n    };\n    const requiredTool: ExtendedTool = {\n      ...mockTools[0],\n      name: \"requiredTool\",\n      execution: { taskSupport: \"required\" },\n    };\n    const optionalTool: ExtendedTool = {\n      ...mockTools[0],\n      name: \"optionalTool\",\n      execution: { taskSupport: \"optional\" },\n    };\n\n    const { rerender } = renderToolsTab({\n      selectedTool: forbiddenTool,\n    });\n\n    expect(screen.queryByLabelText(/run as task/i)).not.toBeInTheDocument();\n\n    rerender(\n      <Tabs defaultValue=\"tools\">\n        <ToolsTab {...defaultProps} selectedTool={optionalTool} />\n      </Tabs>,\n    );\n    const optionalCheckbox = screen.getByLabelText(\n      /run as task/i,\n    ) as HTMLInputElement;\n    expect(optionalCheckbox).toBeInTheDocument();\n    expect(optionalCheckbox.getAttribute(\"aria-checked\")).toBe(\"false\");\n    expect(optionalCheckbox).not.toBeDisabled();\n\n    rerender(\n      <Tabs defaultValue=\"tools\">\n        <ToolsTab {...defaultProps} selectedTool={requiredTool} />\n      </Tabs>,\n    );\n    const requiredCheckbox = screen.getByLabelText(\n      /run as task/i,\n    ) as HTMLInputElement;\n    expect(requiredCheckbox).toBeInTheDocument();\n    expect(requiredCheckbox.getAttribute(\"aria-checked\")).toBe(\"true\");\n    expect(requiredCheckbox).toBeDisabled();\n  });\n\n  it(\"should hide run-as-task checkbox when serverSupportsTaskRequests is false even for required/optional tools\", async () => {\n    const requiredTool: ExtendedTool = {\n      ...mockTools[0],\n      name: \"requiredTool\",\n      execution: { taskSupport: \"required\" },\n    };\n    const optionalTool: ExtendedTool = {\n      ...mockTools[0],\n      name: \"optionalTool\",\n      execution: { taskSupport: \"optional\" },\n    };\n\n    const { rerender } = renderToolsTab({\n      selectedTool: requiredTool,\n      serverSupportsTaskRequests: false,\n    });\n\n    expect(screen.queryByLabelText(/run as task/i)).not.toBeInTheDocument();\n\n    rerender(\n      <Tabs defaultValue=\"tools\">\n        <ToolsTab\n          {...defaultProps}\n          selectedTool={optionalTool}\n          serverSupportsTaskRequests={false}\n        />\n      </Tabs>,\n    );\n\n    expect(screen.queryByLabelText(/run as task/i)).not.toBeInTheDocument();\n  });\n\n  it(\"should handle integer type inputs\", async () => {\n    renderToolsTab({\n      selectedTool: mockTools[1], // Use the tool with integer type\n    });\n\n    const input = screen.getByRole(\"spinbutton\", {\n      name: /count/i,\n    }) as HTMLInputElement;\n    expect(input).toHaveProperty(\"type\", \"number\");\n    fireEvent.change(input, { target: { value: \"42\" } });\n    expect(input.value).toBe(\"42\");\n\n    const submitButton = screen.getByRole(\"button\", { name: /run tool/i });\n    await act(async () => {\n      fireEvent.click(submitButton);\n    });\n\n    expect(defaultProps.callTool).toHaveBeenCalledWith(\n      mockTools[1].name,\n      {\n        count: 42,\n      },\n      undefined,\n      false,\n    );\n  });\n\n  it(\"should allow typing negative numbers\", async () => {\n    renderToolsTab({\n      selectedTool: mockTools[0],\n    });\n\n    const input = screen.getByRole(\"spinbutton\") as HTMLInputElement;\n\n    // Complete the negative number\n    fireEvent.change(input, { target: { value: \"-42\" } });\n    expect(input.value).toBe(\"-42\");\n\n    const submitButton = screen.getByRole(\"button\", { name: /run tool/i });\n    await act(async () => {\n      fireEvent.click(submitButton);\n    });\n\n    expect(defaultProps.callTool).toHaveBeenCalledWith(\n      mockTools[0].name,\n      {\n        num: -42,\n      },\n      undefined,\n      false,\n    );\n  });\n\n  it(\"should allow specifying null value\", async () => {\n    const mockCallTool = jest.fn();\n    const toolWithNullableField = mockTools[3];\n\n    renderToolsTab({\n      tools: [toolWithNullableField],\n      selectedTool: toolWithNullableField,\n      callTool: mockCallTool,\n    });\n\n    const nullToggleButton = screen.getByRole(\"checkbox\", { name: /null/i });\n    expect(nullToggleButton).toBeInTheDocument();\n\n    await act(async () => {\n      fireEvent.click(nullToggleButton);\n    });\n\n    expect(screen.getByRole(\"toolinputwrapper\").classList).toContain(\n      \"pointer-events-none\",\n    );\n\n    const runButton = screen.getByRole(\"button\", { name: /run tool/i });\n    await act(async () => {\n      fireEvent.click(runButton);\n    });\n\n    // Tool should have been called with null value\n    expect(mockCallTool).toHaveBeenCalledWith(\n      toolWithNullableField.name,\n      {\n        num: null,\n      },\n      undefined,\n      false,\n    );\n  });\n\n  it(\"should support tri-state nullable boolean (null -> false -> true -> null)\", async () => {\n    const mockCallTool = jest.fn();\n    const toolWithNullableBoolean: Tool = {\n      name: \"testTool\",\n      description: \"Tool with nullable boolean\",\n      inputSchema: {\n        type: \"object\" as const,\n        properties: {\n          optionalBoolean: {\n            type: [\"boolean\", \"null\"] as const,\n            default: null,\n          },\n        },\n      },\n    };\n\n    renderToolsTab({\n      tools: [toolWithNullableBoolean],\n      selectedTool: toolWithNullableBoolean,\n      callTool: mockCallTool,\n    });\n\n    const nullCheckbox = screen.getByRole(\"checkbox\", { name: /null/i });\n    const runButton = screen.getByRole(\"button\", { name: /run tool/i });\n\n    // State 1: Initial state should be null (input disabled)\n    const wrapper = screen.getByRole(\"toolinputwrapper\");\n    expect(wrapper.classList).toContain(\"pointer-events-none\");\n    expect(wrapper.classList).toContain(\"opacity-50\");\n\n    // Verify tool is called with null initially\n    await act(async () => {\n      fireEvent.click(runButton);\n    });\n    expect(mockCallTool).toHaveBeenCalledWith(\n      toolWithNullableBoolean.name,\n      {\n        optionalBoolean: null,\n      },\n      undefined,\n      false,\n    );\n\n    // State 2: Uncheck null checkbox -> should set value to false and enable input\n    await act(async () => {\n      fireEvent.click(nullCheckbox);\n    });\n    expect(wrapper.classList).not.toContain(\"pointer-events-none\");\n\n    // Clear previous calls to make assertions clearer\n    mockCallTool.mockClear();\n\n    // Verify tool can be called with false\n    await act(async () => {\n      fireEvent.click(runButton);\n    });\n    expect(mockCallTool).toHaveBeenLastCalledWith(\n      toolWithNullableBoolean.name,\n      {\n        optionalBoolean: false,\n      },\n      undefined,\n      false,\n    );\n\n    // State 3: Check boolean checkbox -> should set value to true\n    // Find the boolean checkbox within the input wrapper (to avoid ID conflict with null checkbox)\n    const booleanCheckbox = within(wrapper).getByRole(\"checkbox\");\n\n    mockCallTool.mockClear();\n\n    await act(async () => {\n      fireEvent.click(booleanCheckbox);\n    });\n\n    // Verify tool can be called with true\n    await act(async () => {\n      fireEvent.click(runButton);\n    });\n    expect(mockCallTool).toHaveBeenLastCalledWith(\n      toolWithNullableBoolean.name,\n      {\n        optionalBoolean: true,\n      },\n      undefined,\n      false,\n    );\n\n    // State 4: Check null checkbox again -> should set value back to null and disable input\n    await act(async () => {\n      fireEvent.click(nullCheckbox);\n    });\n    expect(wrapper.classList).toContain(\"pointer-events-none\");\n\n    // Verify tool can be called with null again\n    await act(async () => {\n      fireEvent.click(runButton);\n    });\n    expect(mockCallTool).toHaveBeenLastCalledWith(\n      toolWithNullableBoolean.name,\n      {\n        optionalBoolean: null,\n      },\n      undefined,\n      false,\n    );\n  });\n\n  it(\"should disable button and change text while tool is running\", async () => {\n    // Create a promise that we can resolve later\n    let resolvePromise: ((value: unknown) => void) | undefined;\n    const mockPromise = new Promise((resolve) => {\n      resolvePromise = resolve;\n    });\n\n    // Mock callTool to return our promise\n    const mockCallTool = jest.fn().mockReturnValue(mockPromise);\n\n    renderToolsTab({\n      selectedTool: mockTools[0],\n      callTool: mockCallTool,\n    });\n\n    const submitButton = screen.getByRole(\"button\", { name: /run tool/i });\n    expect(submitButton.getAttribute(\"disabled\")).toBeNull();\n\n    // Click the button and verify immediate state changes\n    await act(async () => {\n      fireEvent.click(submitButton);\n    });\n\n    // Verify button is disabled and text changed\n    expect(submitButton.getAttribute(\"disabled\")).not.toBeNull();\n    expect(submitButton.textContent).toBe(\"Running...\");\n\n    // Resolve the promise to simulate tool completion\n    await act(async () => {\n      if (resolvePromise) {\n        await resolvePromise({});\n      }\n    });\n\n    expect(submitButton.getAttribute(\"disabled\")).toBeNull();\n  });\n\n  describe(\"Output Schema Display\", () => {\n    const toolWithOutputSchema: Tool = {\n      name: \"weatherTool\",\n      description: \"Get weather\",\n      inputSchema: {\n        type: \"object\" as const,\n        properties: {\n          city: { type: \"string\" as const },\n        },\n      },\n      outputSchema: {\n        type: \"object\" as const,\n        properties: {\n          temperature: { type: \"number\" as const },\n          humidity: { type: \"number\" as const },\n        },\n        required: [\"temperature\", \"humidity\"],\n      },\n    };\n\n    it(\"should display output schema when tool has one\", () => {\n      renderToolsTab({\n        tools: [toolWithOutputSchema],\n        selectedTool: toolWithOutputSchema,\n      });\n\n      expect(screen.getByText(\"Output Schema:\")).toBeInTheDocument();\n      // Check for expand/collapse button\n      expect(\n        screen.getByRole(\"button\", { name: /expand/i }),\n      ).toBeInTheDocument();\n    });\n\n    it(\"should not display output schema section when tool doesn't have one\", () => {\n      renderToolsTab({\n        selectedTool: mockTools[0], // Tool without outputSchema\n      });\n\n      expect(screen.queryByText(\"Output Schema:\")).not.toBeInTheDocument();\n    });\n\n    it(\"should toggle output schema expansion\", () => {\n      renderToolsTab({\n        tools: [toolWithOutputSchema],\n        selectedTool: toolWithOutputSchema,\n      });\n\n      const toggleButton = screen.getByRole(\"button\", { name: /expand/i });\n\n      // Click to expand\n      fireEvent.click(toggleButton);\n      expect(\n        screen.getByRole(\"button\", { name: /collapse/i }),\n      ).toBeInTheDocument();\n\n      // Click to collapse\n      fireEvent.click(toggleButton);\n      expect(\n        screen.getByRole(\"button\", { name: /expand/i }),\n      ).toBeInTheDocument();\n    });\n  });\n\n  describe(\"Structured Output Results\", () => {\n    const toolWithOutputSchema: Tool = {\n      name: \"weatherTool\",\n      description: \"Get weather\",\n      inputSchema: {\n        type: \"object\" as const,\n        properties: {},\n      },\n      outputSchema: {\n        type: \"object\" as const,\n        properties: {\n          temperature: { type: \"number\" as const },\n        },\n        required: [\"temperature\"],\n      },\n    };\n\n    beforeEach(() => {\n      // Cache the tool's output schema before each test\n      cacheToolOutputSchemas([toolWithOutputSchema]);\n    });\n\n    it(\"should display structured content when present\", () => {\n      const structuredResult = {\n        content: [],\n        structuredContent: {\n          temperature: 25,\n        },\n      };\n\n      renderToolsTab({\n        tools: [toolWithOutputSchema],\n        selectedTool: toolWithOutputSchema,\n        toolResult: structuredResult,\n      });\n\n      expect(screen.getByText(\"Structured Content:\")).toBeInTheDocument();\n      expect(\n        screen.getByText(/Valid according to output schema/),\n      ).toBeInTheDocument();\n    });\n\n    it(\"should show validation error for invalid structured content\", () => {\n      const invalidResult = {\n        content: [],\n        structuredContent: {\n          temperature: \"25\", // String instead of number\n        },\n      };\n\n      renderToolsTab({\n        tools: [toolWithOutputSchema],\n        selectedTool: toolWithOutputSchema,\n        toolResult: invalidResult,\n      });\n\n      expect(screen.getByText(/Validation Error:/)).toBeInTheDocument();\n    });\n\n    it(\"should show error when tool with output schema doesn't return structured content\", () => {\n      const resultWithoutStructured = {\n        content: [{ type: \"text\", text: \"some result\" }],\n        // No structuredContent\n      };\n\n      renderToolsTab({\n        tools: [toolWithOutputSchema],\n        selectedTool: toolWithOutputSchema,\n        toolResult: resultWithoutStructured,\n      });\n\n      expect(\n        screen.getByText(\n          /Tool has an output schema but did not return structured content/,\n        ),\n      ).toBeInTheDocument();\n    });\n\n    it(\"should show unstructured content title when both structured and unstructured exist\", () => {\n      const resultWithBoth = {\n        content: [{ type: \"text\", text: '{\"temperature\": 25}' }],\n        structuredContent: { temperature: 25 },\n      };\n\n      renderToolsTab({\n        tools: [toolWithOutputSchema],\n        selectedTool: toolWithOutputSchema,\n        toolResult: resultWithBoth,\n      });\n\n      expect(screen.getByText(\"Structured Content:\")).toBeInTheDocument();\n      expect(screen.getByText(\"Unstructured Content:\")).toBeInTheDocument();\n    });\n\n    it(\"should not show unstructured content title when only unstructured exists\", () => {\n      const resultWithUnstructuredOnly = {\n        content: [{ type: \"text\", text: \"some result\" }],\n      };\n\n      renderToolsTab({\n        selectedTool: mockTools[0], // Tool without output schema\n        toolResult: resultWithUnstructuredOnly,\n      });\n\n      expect(\n        screen.queryByText(\"Unstructured Content:\"),\n      ).not.toBeInTheDocument();\n    });\n\n    it(\"should show compatibility check when tool has output schema\", () => {\n      const compatibleResult = {\n        content: [{ type: \"text\", text: '{\"temperature\": 25}' }],\n        structuredContent: { temperature: 25 },\n      };\n\n      renderToolsTab({\n        tools: [toolWithOutputSchema],\n        selectedTool: toolWithOutputSchema,\n        toolResult: compatibleResult,\n      });\n\n      // Should show compatibility result\n      expect(\n        screen.getByText(/structured content matches/i),\n      ).toBeInTheDocument();\n    });\n\n    it(\"should accept multiple content blocks with structured output\", () => {\n      const multipleBlocksResult = {\n        content: [\n          { type: \"text\", text: \"Here is the weather data:\" },\n          { type: \"text\", text: '{\"temperature\": 25}' },\n          { type: \"text\", text: \"Have a nice day!\" },\n        ],\n        structuredContent: { temperature: 25 },\n      };\n\n      renderToolsTab({\n        tools: [toolWithOutputSchema],\n        selectedTool: toolWithOutputSchema,\n        toolResult: multipleBlocksResult,\n      });\n\n      // Should show compatible result with multiple blocks\n      expect(\n        screen.getByText(/structured content matches.*multiple/i),\n      ).toBeInTheDocument();\n    });\n\n    it(\"should accept mixed content types with structured output\", () => {\n      const mixedContentResult = {\n        content: [\n          { type: \"text\", text: \"Weather report:\" },\n          { type: \"text\", text: '{\"temperature\": 25}' },\n          { type: \"image\", data: \"base64data\", mimeType: \"image/png\" },\n        ],\n        structuredContent: { temperature: 25 },\n      };\n\n      renderToolsTab({\n        tools: [toolWithOutputSchema],\n        selectedTool: toolWithOutputSchema,\n        toolResult: mixedContentResult,\n      });\n\n      // Should render without crashing - the validation logic has been updated\n      expect(screen.getAllByText(\"weatherTool\")).toHaveLength(2);\n    });\n\n    it(\"should reject when no text blocks match structured content\", () => {\n      const noMatchResult = {\n        content: [\n          { type: \"text\", text: \"Some text\" },\n          { type: \"text\", text: '{\"humidity\": 60}' }, // Different structure\n        ],\n        structuredContent: { temperature: 25 },\n      };\n\n      renderToolsTab({\n        tools: [toolWithOutputSchema],\n        selectedTool: toolWithOutputSchema,\n        toolResult: noMatchResult,\n      });\n\n      // Should render without crashing - the validation logic has been updated\n      expect(screen.getAllByText(\"weatherTool\")).toHaveLength(2);\n    });\n\n    it(\"should reject when no text blocks are present\", () => {\n      const noTextBlocksResult = {\n        content: [{ type: \"image\", data: \"base64data\", mimeType: \"image/png\" }],\n        structuredContent: { temperature: 25 },\n      };\n\n      renderToolsTab({\n        tools: [toolWithOutputSchema],\n        selectedTool: toolWithOutputSchema,\n        toolResult: noTextBlocksResult,\n      });\n\n      // Should render without crashing - the validation logic has been updated\n      expect(screen.getAllByText(\"weatherTool\")).toHaveLength(2);\n    });\n\n    it(\"should not show compatibility check when tool has no output schema\", () => {\n      const resultWithBoth = {\n        content: [{ type: \"text\", text: '{\"data\": \"value\"}' }],\n        structuredContent: { different: \"data\" },\n      };\n\n      renderToolsTab({\n        selectedTool: mockTools[0], // Tool without output schema\n        toolResult: resultWithBoth,\n      });\n\n      // Should not show any compatibility messages\n      expect(\n        screen.queryByText(\n          /structured content matches|no text blocks|no.*matches/i,\n        ),\n      ).not.toBeInTheDocument();\n    });\n  });\n\n  describe(\"Resource Link Content Type\", () => {\n    it(\"should render resource_link content type and handle expansion\", async () => {\n      const mockOnReadResource = jest.fn();\n      const resourceContent = {\n        \"test://static/resource/1\": JSON.stringify({\n          contents: [\n            {\n              uri: \"test://static/resource/1\",\n              name: \"Resource 1\",\n              mimeType: \"text/plain\",\n              text: \"Resource 1: This is a plaintext resource\",\n            },\n          ],\n        }),\n      };\n\n      const result = {\n        content: [\n          {\n            type: \"resource_link\",\n            uri: \"test://static/resource/1\",\n            name: \"Resource 1\",\n            description: \"Resource 1: plaintext resource\",\n            mimeType: \"text/plain\",\n          },\n          {\n            type: \"resource_link\",\n            uri: \"test://static/resource/2\",\n            name: \"Resource 2\",\n            description: \"Resource 2: binary blob resource\",\n            mimeType: \"application/octet-stream\",\n          },\n          {\n            type: \"resource_link\",\n            uri: \"test://static/resource/3\",\n            name: \"Resource 3\",\n            description: \"Resource 3: plaintext resource\",\n            mimeType: \"text/plain\",\n          },\n        ],\n      };\n\n      renderToolsTab({\n        selectedTool: mockTools[0],\n        toolResult: result,\n        resourceContent,\n        onReadResource: mockOnReadResource,\n      });\n\n      [\"1\", \"2\", \"3\"].forEach((id) => {\n        expect(\n          screen.getByText(`test://static/resource/${id}`),\n        ).toBeInTheDocument();\n        expect(screen.getByText(`Resource ${id}`)).toBeInTheDocument();\n      });\n\n      expect(screen.getAllByText(\"text/plain\")).toHaveLength(2);\n      expect(screen.getByText(\"application/octet-stream\")).toBeInTheDocument();\n\n      const expandButtons = screen.getAllByRole(\"button\", {\n        name: /expand resource/i,\n      });\n      expect(expandButtons).toHaveLength(3);\n      expect(screen.queryByText(\"Resource:\")).not.toBeInTheDocument();\n\n      expandButtons.forEach((button) => {\n        expect(button).toHaveAttribute(\"aria-expanded\", \"false\");\n      });\n\n      const resource1Button = screen.getByRole(\"button\", {\n        name: /expand resource test:\\/\\/static\\/resource\\/1/i,\n      });\n\n      await act(async () => {\n        fireEvent.click(resource1Button);\n      });\n\n      expect(mockOnReadResource).toHaveBeenCalledWith(\n        \"test://static/resource/1\",\n      );\n      expect(screen.getByText(\"Resource:\")).toBeInTheDocument();\n      expect(document.body).toHaveTextContent(\"contents:\");\n      expect(document.body).toHaveTextContent('uri:\"test://static/resource/1\"');\n      expect(resource1Button).toHaveAttribute(\"aria-expanded\", \"true\");\n\n      await act(async () => {\n        fireEvent.click(resource1Button);\n      });\n\n      expect(screen.queryByText(\"Resource:\")).not.toBeInTheDocument();\n      expect(document.body).not.toHaveTextContent(\"contents:\");\n      expect(document.body).not.toHaveTextContent(\n        'uri:\"test://static/resource/1\"',\n      );\n      expect(resource1Button).toHaveAttribute(\"aria-expanded\", \"false\");\n      expect(mockOnReadResource).toHaveBeenCalledTimes(1);\n    });\n  });\n\n  describe(\"Metadata Display\", () => {\n    const toolWithMetadata = {\n      name: \"metaTool\",\n      description: \"Tool with metadata\",\n      inputSchema: {\n        type: \"object\" as const,\n        properties: {\n          foo: { type: \"string\" as const },\n        },\n      },\n      _meta: {\n        author: \"tester\",\n        version: 1,\n      },\n    } as unknown as Tool;\n\n    it(\"should display metadata section when tool has _meta\", () => {\n      renderToolsTab({\n        tools: [toolWithMetadata],\n        selectedTool: toolWithMetadata,\n      });\n\n      expect(screen.getByText(\"Meta:\")).toBeInTheDocument();\n      expect(\n        screen.getByRole(\"button\", { name: /expand/i }),\n      ).toBeInTheDocument();\n    });\n\n    it(\"should toggle metadata schema expansion\", () => {\n      renderToolsTab({\n        tools: [toolWithMetadata],\n        selectedTool: toolWithMetadata,\n      });\n\n      // There might be multiple Expand buttons (Output Schema, Meta). We need the one within Meta section\n      const metaHeading = screen.getByText(\"Meta:\");\n      const metaContainer = metaHeading.closest(\"div\");\n      expect(metaContainer).toBeTruthy();\n      const toggleButton = within(metaContainer as HTMLElement).getByRole(\n        \"button\",\n        { name: /expand/i },\n      );\n\n      // Expand Meta\n      fireEvent.click(toggleButton);\n      expect(\n        within(metaContainer as HTMLElement).getByRole(\"button\", {\n          name: /collapse/i,\n        }),\n      ).toBeInTheDocument();\n\n      // Collapse Meta\n      fireEvent.click(toggleButton);\n      expect(\n        within(metaContainer as HTMLElement).getByRole(\"button\", {\n          name: /expand/i,\n        }),\n      ).toBeInTheDocument();\n    });\n  });\n\n  describe(\"Metadata submission\", () => {\n    it(\"should send metadata values when provided\", async () => {\n      const callToolMock = jest.fn(async () => {});\n\n      renderToolsTab({ selectedTool: mockTools[0], callTool: callToolMock });\n\n      // Add a metadata key/value pair\n      const addPairButton = screen.getByRole(\"button\", { name: /add pair/i });\n      await act(async () => {\n        fireEvent.click(addPairButton);\n      });\n\n      // Fill key and value\n      const keyInputs = screen.getAllByLabelText(/key/i);\n      const valueInputs = screen.getAllByLabelText(/value/i);\n      expect(keyInputs.length).toBeGreaterThan(0);\n      expect(valueInputs.length).toBeGreaterThan(0);\n\n      await act(async () => {\n        fireEvent.change(keyInputs[0], { target: { value: \"requestId\" } });\n        fireEvent.change(valueInputs[0], { target: { value: \"abc123\" } });\n      });\n\n      // Run tool\n      const runButton = screen.getByRole(\"button\", { name: /run tool/i });\n      await act(async () => {\n        fireEvent.click(runButton);\n      });\n\n      expect(callToolMock).toHaveBeenCalledTimes(1);\n      expect(callToolMock).toHaveBeenLastCalledWith(\n        mockTools[0].name,\n        expect.any(Object),\n        { requestId: \"abc123\" },\n        false,\n      );\n    });\n  });\n\n  describe(\"Reserved metadata keys\", () => {\n    test.each`\n      description                             | value                             | message\n      ${\"reserved metadata prefix\"}           | ${\"modelcontextprotocol.io/flip\"} | ${RESERVED_NAMESPACE_MESSAGE}\n      ${\"reserved root without slash\"}        | ${\"modelcontextprotocol.io\"}      | ${RESERVED_NAMESPACE_MESSAGE}\n      ${\"nested modelcontextprotocol domain\"} | ${\"api.modelcontextprotocol.org\"} | ${RESERVED_NAMESPACE_MESSAGE}\n      ${\"nested mcp domain\"}                  | ${\"tools.mcp.com/resource\"}       | ${RESERVED_NAMESPACE_MESSAGE}\n      ${\"invalid name segment\"}               | ${\"custom/bad-\"}                  | ${META_NAME_RULES_MESSAGE}\n      ${\"invalid prefix label\"}               | ${\"1invalid-prefix/value\"}        | ${META_PREFIX_RULES_MESSAGE}\n    `(\n      \"should block execution when $description is provided\",\n      async ({ value, message }) => {\n        renderToolsTab({ selectedTool: mockTools[0] });\n\n        const addPairButton = screen.getByRole(\"button\", { name: /add pair/i });\n        await act(async () => {\n          fireEvent.click(addPairButton);\n        });\n\n        const keyInput = screen.getByPlaceholderText(\"e.g. requestId\");\n        await act(async () => {\n          fireEvent.change(keyInput, { target: { value } });\n        });\n\n        const runButton = screen.getByRole(\"button\", { name: /run tool/i });\n        expect(runButton).toBeDisabled();\n        expect(screen.getByText(message)).toBeInTheDocument();\n      },\n    );\n  });\n\n  describe(\"ToolResults Metadata\", () => {\n    it(\"should display metadata information when present in toolResult\", () => {\n      const resultWithMetadata = {\n        content: [],\n        _meta: { info: \"details\", version: 2 },\n      };\n\n      renderToolsTab({\n        selectedTool: mockTools[0],\n        toolResult: resultWithMetadata,\n      });\n\n      // Only ToolResults metadata should be present since selectedTool has no _meta\n      expect(screen.getAllByText(\"Meta:\")).toHaveLength(1);\n      expect(screen.getByText(/info/i)).toBeInTheDocument();\n      expect(screen.getByText(/version/i)).toBeInTheDocument();\n    });\n  });\n\n  describe(\"Enum Parameters\", () => {\n    const toolWithEnumParam: Tool = {\n      name: \"enumTool\",\n      description: \"Tool with enum parameter\",\n      inputSchema: {\n        type: \"object\" as const,\n        properties: {\n          format: {\n            type: \"string\" as const,\n            enum: [\"json\", \"xml\", \"csv\", \"yaml\"],\n            description: \"Output format\",\n          },\n        },\n      },\n    };\n\n    beforeEach(() => {\n      // Mock scrollIntoView for Radix UI Select\n      Element.prototype.scrollIntoView = jest.fn();\n    });\n\n    it(\"should render enum parameter as dropdown\", () => {\n      renderToolsTab({\n        tools: [toolWithEnumParam],\n        selectedTool: toolWithEnumParam,\n      });\n\n      // Should render a select button instead of textarea\n      const selectTrigger = screen.getByRole(\"combobox\", { name: /format/i });\n      expect(selectTrigger).toBeInTheDocument();\n    });\n\n    it(\"should render non-enum string parameter as textarea\", () => {\n      const toolWithStringParam: Tool = {\n        name: \"stringTool\",\n        description: \"Tool with regular string parameter\",\n        inputSchema: {\n          type: \"object\" as const,\n          properties: {\n            text: {\n              type: \"string\" as const,\n              description: \"Some text input\",\n            },\n          },\n        },\n      };\n\n      renderToolsTab({\n        tools: [toolWithStringParam],\n        selectedTool: toolWithStringParam,\n      });\n\n      // Should render textarea, not select\n      expect(screen.queryByRole(\"combobox\")).not.toBeInTheDocument();\n      expect(screen.getByRole(\"textbox\")).toBeInTheDocument();\n    });\n  });\n\n  describe(\"JSON Validation Integration\", () => {\n    const toolWithJsonParams: Tool = {\n      name: \"jsonTool\",\n      description: \"Tool with JSON parameters\",\n      inputSchema: {\n        type: \"object\" as const,\n        required: [\"config\", \"data\"], // Make them required so they render as form fields\n        properties: {\n          config: {\n            type: \"object\" as const,\n            // No properties defined - this will force JSON mode\n          },\n          data: {\n            type: \"array\" as const,\n            // No items defined - this will force JSON mode\n          },\n        },\n      },\n    };\n\n    it(\"should prevent tool execution when JSON validation fails\", async () => {\n      const mockCallTool = jest.fn();\n      renderToolsTab({\n        tools: [toolWithJsonParams],\n        selectedTool: toolWithJsonParams,\n        callTool: mockCallTool,\n      });\n\n      // Find JSON editor textareas (there should be at least 1 for JSON parameters)\n      const textareas = screen.getAllByRole(\"textbox\");\n      expect(textareas.length).toBeGreaterThanOrEqual(1);\n\n      // Enter invalid JSON in the first textarea\n      const configTextarea = textareas[0];\n      fireEvent.change(configTextarea, {\n        target: { value: '{ \"invalid\": json }' },\n      });\n\n      // Try to run the tool\n      const runButton = screen.getByRole(\"button\", { name: /run tool/i });\n      await act(async () => {\n        fireEvent.click(runButton);\n      });\n\n      // Tool should not have been called due to validation failure\n      expect(mockCallTool).not.toHaveBeenCalled();\n    });\n\n    it(\"should allow tool execution when JSON validation passes\", async () => {\n      const mockCallTool = jest.fn();\n      renderToolsTab({\n        tools: [toolWithJsonParams],\n        selectedTool: toolWithJsonParams,\n        callTool: mockCallTool,\n      });\n\n      // Find JSON editor textareas (should have one for each required field: config and data)\n      const textareas = screen.getAllByRole(\"textbox\");\n      expect(textareas.length).toBe(2);\n\n      // Enter valid JSON in each textarea\n      fireEvent.change(textareas[0], {\n        target: { value: '{ \"setting\": \"value\" }' },\n      });\n      fireEvent.change(textareas[1], {\n        target: { value: '[\"item1\", \"item2\"]' },\n      });\n\n      // Wait for debounced updates\n      await act(async () => {\n        await new Promise((resolve) => setTimeout(resolve, 350));\n      });\n\n      // Try to run the tool\n      const runButton = screen.getByRole(\"button\", { name: /run tool/i });\n      await act(async () => {\n        fireEvent.click(runButton);\n      });\n\n      // Tool should have been called successfully\n      expect(mockCallTool).toHaveBeenCalled();\n    });\n\n    it(\"should handle mixed valid and invalid JSON parameters\", async () => {\n      const mockCallTool = jest.fn();\n      renderToolsTab({\n        tools: [toolWithJsonParams],\n        selectedTool: toolWithJsonParams,\n        callTool: mockCallTool,\n      });\n\n      const textareas = screen.getAllByRole(\"textbox\");\n\n      // Enter invalid JSON that contains both valid and invalid parts\n      fireEvent.change(textareas[0], {\n        target: {\n          value:\n            '{ \"config\": { \"setting\": \"value\" }, \"data\": [\"unclosed array\" }',\n        },\n      });\n\n      // Try to run the tool\n      const runButton = screen.getByRole(\"button\", { name: /run tool/i });\n      await act(async () => {\n        fireEvent.click(runButton);\n      });\n\n      // Tool should not have been called due to validation failure\n      expect(mockCallTool).not.toHaveBeenCalled();\n    });\n\n    it(\"should work with tools that have no JSON parameters\", async () => {\n      const mockCallTool = jest.fn();\n      const simpleToolWithStringParam: Tool = {\n        name: \"simpleTool\",\n        description: \"Tool with simple parameters\",\n        inputSchema: {\n          type: \"object\" as const,\n          properties: {\n            message: { type: \"string\" as const },\n            count: { type: \"number\" as const },\n          },\n        },\n      };\n\n      renderToolsTab({\n        tools: [simpleToolWithStringParam],\n        selectedTool: simpleToolWithStringParam,\n        callTool: mockCallTool,\n      });\n\n      // Fill in the simple parameters\n      const messageInput = screen.getByRole(\"textbox\");\n      const countInput = screen.getByRole(\"spinbutton\");\n\n      fireEvent.change(messageInput, { target: { value: \"test message\" } });\n      fireEvent.change(countInput, { target: { value: \"5\" } });\n\n      // Run the tool\n      const runButton = screen.getByRole(\"button\", { name: /run tool/i });\n      await act(async () => {\n        fireEvent.click(runButton);\n      });\n\n      // Tool should have been called successfully (no JSON validation needed)\n      expect(mockCallTool).toHaveBeenCalledWith(\n        simpleToolWithStringParam.name,\n        {\n          message: \"test message\",\n          count: 5,\n        },\n        undefined,\n        false,\n      );\n    });\n\n    it(\"should handle empty JSON parameters correctly\", async () => {\n      const mockCallTool = jest.fn();\n      renderToolsTab({\n        tools: [toolWithJsonParams],\n        selectedTool: toolWithJsonParams,\n        callTool: mockCallTool,\n      });\n\n      const textareas = screen.getAllByRole(\"textbox\");\n      expect(textareas.length).toBe(2);\n\n      // Clear both textareas (empty JSON should be valid)\n      fireEvent.change(textareas[0], { target: { value: \"{}\" } });\n      fireEvent.change(textareas[1], { target: { value: \"[]\" } });\n\n      // Wait for debounced updates\n      await act(async () => {\n        await new Promise((resolve) => setTimeout(resolve, 350));\n      });\n\n      // Try to run the tool\n      const runButton = screen.getByRole(\"button\", { name: /run tool/i });\n      await act(async () => {\n        fireEvent.click(runButton);\n      });\n\n      // Tool should have been called (empty JSON is considered valid)\n      expect(mockCallTool).toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/components/__tests__/samplingRequest.test.tsx",
    "content": "import { render, screen, fireEvent } from \"@testing-library/react\";\nimport SamplingRequest from \"../SamplingRequest\";\nimport { PendingRequest } from \"../SamplingTab\";\n\nconst mockRequest: PendingRequest = {\n  id: 1,\n  request: {\n    method: \"sampling/createMessage\",\n    params: {\n      messages: [\n        {\n          role: \"user\",\n          content: {\n            type: \"text\",\n            text: \"What files are in the current directory?\",\n          },\n        },\n      ],\n      systemPrompt: \"You are a helpful file system assistant.\",\n      includeContext: \"thisServer\",\n      maxTokens: 100,\n    },\n  },\n};\n\ndescribe(\"Form to handle sampling response\", () => {\n  const mockOnApprove = jest.fn();\n  const mockOnReject = jest.fn();\n\n  afterEach(() => {\n    jest.clearAllMocks();\n  });\n\n  it(\"should call onApprove with correct text content when Approve button is clicked\", () => {\n    render(\n      <SamplingRequest\n        request={mockRequest}\n        onApprove={mockOnApprove}\n        onReject={mockOnReject}\n      />,\n    );\n\n    // Click the Approve button\n    fireEvent.click(screen.getByRole(\"button\", { name: /approve/i }));\n\n    // Assert that onApprove is called with the correct arguments\n    expect(mockOnApprove).toHaveBeenCalledWith(mockRequest.id, {\n      model: \"stub-model\",\n      stopReason: \"endTurn\",\n      role: \"assistant\",\n      content: {\n        type: \"text\",\n        text: \"\",\n      },\n    });\n  });\n\n  it(\"should call onReject with correct request id when Reject button is clicked\", () => {\n    render(\n      <SamplingRequest\n        request={mockRequest}\n        onApprove={mockOnApprove}\n        onReject={mockOnReject}\n      />,\n    );\n\n    // Click the Approve button\n    fireEvent.click(screen.getByRole(\"button\", { name: /Reject/i }));\n\n    // Assert that onApprove is called with the correct arguments\n    expect(mockOnReject).toHaveBeenCalledWith(mockRequest.id);\n  });\n});\n"
  },
  {
    "path": "client/src/components/__tests__/samplingTab.test.tsx",
    "content": "import { render, screen } from \"@testing-library/react\";\nimport { Tabs } from \"@/components/ui/tabs\";\nimport SamplingTab, { PendingRequest } from \"../SamplingTab\";\n\ndescribe(\"Sampling tab\", () => {\n  const mockOnApprove = jest.fn();\n  const mockOnReject = jest.fn();\n\n  const renderSamplingTab = (pendingRequests: PendingRequest[]) =>\n    render(\n      <Tabs defaultValue=\"sampling\">\n        <SamplingTab\n          pendingRequests={pendingRequests}\n          onApprove={mockOnApprove}\n          onReject={mockOnReject}\n        />\n      </Tabs>,\n    );\n\n  it(\"should render 'No pending requests' when there are no pending requests\", () => {\n    renderSamplingTab([]);\n    expect(\n      screen.getByText(\n        \"When the server requests LLM sampling, requests will appear here for approval.\",\n      ),\n    ).toBeTruthy();\n    expect(screen.findByText(\"No pending requests\")).toBeTruthy();\n  });\n\n  it(\"should render the correct number of requests\", () => {\n    renderSamplingTab(\n      Array.from({ length: 5 }, (_, i) => ({\n        id: i,\n        request: {\n          method: \"sampling/createMessage\",\n          params: {\n            messages: [\n              {\n                role: \"user\",\n                content: {\n                  type: \"text\",\n                  text: \"What files are in the current directory?\",\n                },\n              },\n            ],\n            systemPrompt: \"You are a helpful file system assistant.\",\n            includeContext: \"thisServer\",\n            maxTokens: 100,\n          },\n        },\n      })),\n    );\n    expect(screen.getAllByTestId(\"sampling-request\").length).toBe(5);\n  });\n});\n"
  },
  {
    "path": "client/src/components/ui/alert.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst alertVariants = cva(\n  \"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-background text-foreground\",\n        destructive:\n          \"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nconst Alert = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>\n>(({ className, variant, ...props }, ref) => (\n  <div\n    ref={ref}\n    role=\"alert\"\n    className={cn(alertVariants({ variant }), className)}\n    {...props}\n  />\n));\nAlert.displayName = \"Alert\";\n\nconst AlertTitle = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n  <h5\n    ref={ref}\n    className={cn(\"mb-1 font-medium leading-none tracking-tight\", className)}\n    {...props}\n  />\n));\nAlertTitle.displayName = \"AlertTitle\";\n\nconst AlertDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"text-sm [&_p]:leading-relaxed\", className)}\n    {...props}\n  />\n));\nAlertDescription.displayName = \"AlertDescription\";\n\nexport { Alert, AlertTitle, AlertDescription };\n"
  },
  {
    "path": "client/src/components/ui/button.tsx",
    "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"bg-primary text-primary-foreground shadow hover:bg-primary/90 dark:bg-gray-800 dark:text-white dark:hover:bg-gray-700\",\n        destructive:\n          \"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90\",\n        outline:\n          \"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground\",\n        secondary:\n          \"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2\",\n        sm: \"h-8 rounded-md px-3 text-xs\",\n        lg: \"h-10 rounded-md px-8\",\n        icon: \"h-9 w-9\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nexport interface ButtonProps\n  extends\n    React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\";\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nButton.displayName = \"Button\";\n\nexport { Button };\n"
  },
  {
    "path": "client/src/components/ui/checkbox.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\";\nimport { Check } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Checkbox = React.forwardRef<\n  React.ElementRef<typeof CheckboxPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <CheckboxPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground\",\n      className,\n    )}\n    {...props}\n  >\n    <CheckboxPrimitive.Indicator\n      className={cn(\"flex items-center justify-center text-current\")}\n    >\n      <Check className=\"h-4 w-4\" />\n    </CheckboxPrimitive.Indicator>\n  </CheckboxPrimitive.Root>\n));\nCheckbox.displayName = CheckboxPrimitive.Root.displayName;\n\nexport { Checkbox };\n"
  },
  {
    "path": "client/src/components/ui/combobox.tsx",
    "content": "import React from \"react\";\nimport { Check, ChevronsUpDown } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n} from \"@/components/ui/command\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\n\ninterface ComboboxProps {\n  value: string;\n  onChange: (value: string) => void;\n  onInputChange: (value: string) => void;\n  onFocus?: () => void;\n  options: string[];\n  placeholder?: string;\n  emptyMessage?: string;\n  id?: string;\n}\n\nexport function Combobox({\n  value,\n  onChange,\n  onInputChange,\n  onFocus,\n  options = [],\n  placeholder = \"Select...\",\n  emptyMessage = \"No results found.\",\n  id,\n}: ComboboxProps) {\n  const [open, setOpen] = React.useState(false);\n\n  const handleOpenChange = React.useCallback(\n    (newOpen: boolean) => {\n      setOpen(newOpen);\n      if (newOpen && onFocus) {\n        onFocus();\n      }\n    },\n    [onFocus],\n  );\n\n  const handleSelect = React.useCallback(\n    (option: string) => {\n      onChange(option);\n      setOpen(false);\n    },\n    [onChange],\n  );\n\n  const handleInputChange = React.useCallback(\n    (value: string) => {\n      onInputChange(value);\n    },\n    [onInputChange],\n  );\n\n  return (\n    <Popover open={open} onOpenChange={handleOpenChange}>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"outline\"\n          role=\"combobox\"\n          aria-expanded={open}\n          aria-controls={id}\n          className=\"w-full justify-between\"\n        >\n          {value || placeholder}\n          <ChevronsUpDown className=\"ml-2 h-4 w-4 shrink-0 opacity-50\" />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-full p-0\" align=\"start\">\n        <Command shouldFilter={false} id={id}>\n          <CommandInput\n            placeholder={placeholder}\n            value={value}\n            onValueChange={handleInputChange}\n          />\n          <CommandEmpty>{emptyMessage}</CommandEmpty>\n          <CommandGroup>\n            {options.map((option) => (\n              <CommandItem\n                key={option}\n                value={option}\n                onSelect={() => handleSelect(option)}\n              >\n                <Check\n                  className={cn(\n                    \"mr-2 h-4 w-4\",\n                    value === option ? \"opacity-100\" : \"opacity-0\",\n                  )}\n                />\n                {option}\n              </CommandItem>\n            ))}\n          </CommandGroup>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "client/src/components/ui/command.tsx",
    "content": "import * as React from \"react\";\nimport { type DialogProps } from \"@radix-ui/react-dialog\";\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { cn } from \"@/lib/utils\";\nimport { Dialog, DialogContent } from \"@/components/ui/dialog\";\nimport { MagnifyingGlassIcon } from \"@radix-ui/react-icons\";\n\nconst Command = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive\n    ref={ref}\n    className={cn(\n      \"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground\",\n      className,\n    )}\n    {...props}\n  />\n));\nCommand.displayName = CommandPrimitive.displayName;\n\nconst CommandDialog = ({ children, ...props }: DialogProps) => {\n  return (\n    <Dialog {...props}>\n      <DialogContent className=\"overflow-hidden p-0\">\n        <Command className=\"[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n          {children}\n        </Command>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst CommandInput = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Input>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>\n>(({ className, ...props }, ref) => (\n  <div className=\"flex items-center border-b px-3\" cmdk-input-wrapper=\"\">\n    <MagnifyingGlassIcon className=\"mr-2 h-4 w-4 shrink-0 opacity-50\" />\n    <CommandPrimitive.Input\n      ref={ref}\n      className={cn(\n        \"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    />\n  </div>\n));\n\nCommandInput.displayName = CommandPrimitive.Input.displayName;\n\nconst CommandList = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.List\n    ref={ref}\n    className={cn(\"max-h-[300px] overflow-y-auto overflow-x-hidden\", className)}\n    {...props}\n  />\n));\n\nCommandList.displayName = CommandPrimitive.List.displayName;\n\nconst CommandEmpty = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Empty>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>\n>((props, ref) => (\n  <CommandPrimitive.Empty\n    ref={ref}\n    className=\"py-6 text-center text-sm\"\n    {...props}\n  />\n));\n\nCommandEmpty.displayName = CommandPrimitive.Empty.displayName;\n\nconst CommandGroup = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Group>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Group\n    ref={ref}\n    className={cn(\n      \"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground\",\n      className,\n    )}\n    {...props}\n  />\n));\n\nCommandGroup.displayName = CommandPrimitive.Group.displayName;\n\nconst CommandSeparator = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 h-px bg-border\", className)}\n    {...props}\n  />\n));\nCommandSeparator.displayName = CommandPrimitive.Separator.displayName;\n\nconst CommandItem = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n      className,\n    )}\n    {...props}\n  />\n));\n\nCommandItem.displayName = CommandPrimitive.Item.displayName;\n\nconst CommandShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\n        \"ml-auto text-xs tracking-widest text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n};\nCommandShortcut.displayName = \"CommandShortcut\";\n\nexport {\n  Command,\n  CommandDialog,\n  CommandInput,\n  CommandList,\n  CommandEmpty,\n  CommandGroup,\n  CommandItem,\n  CommandShortcut,\n  CommandSeparator,\n};\n"
  },
  {
    "path": "client/src/components/ui/dialog.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { cn } from \"@/lib/utils\";\nimport { Cross2Icon } from \"@radix-ui/react-icons\";\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className,\n    )}\n    {...props}\n  />\n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <DialogPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground\">\n        <Cross2Icon className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n));\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-1.5 text-center sm:text-left\",\n      className,\n    )}\n    {...props}\n  />\n);\nDialogHeader.displayName = \"DialogHeader\";\n\nconst DialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n      className,\n    )}\n    {...props}\n  />\n);\nDialogFooter.displayName = \"DialogFooter\";\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\n      \"text-lg font-semibold leading-none tracking-tight\",\n      className,\n    )}\n    {...props}\n  />\n));\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogTrigger,\n  DialogClose,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n};\n"
  },
  {
    "path": "client/src/components/ui/input.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type InputProps = React.InputHTMLAttributes<HTMLInputElement>;\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ className, type, ...props }, ref) => {\n    return (\n      <input\n        type={type}\n        className={cn(\n          \"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50\",\n          className,\n        )}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nInput.displayName = \"Input\";\n\nexport { Input };\n"
  },
  {
    "path": "client/src/components/ui/label.tsx",
    "content": "import * as React from \"react\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst labelVariants = cva(\n  \"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\",\n);\n\nconst Label = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &\n    VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n  <LabelPrimitive.Root\n    ref={ref}\n    className={cn(labelVariants(), className)}\n    {...props}\n  />\n));\nLabel.displayName = LabelPrimitive.Root.displayName;\n\nexport { Label };\n"
  },
  {
    "path": "client/src/components/ui/popover.tsx",
    "content": "import * as React from \"react\";\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Popover = PopoverPrimitive.Root;\n\nconst PopoverTrigger = PopoverPrimitive.Trigger;\n\nconst PopoverAnchor = PopoverPrimitive.Anchor;\n\nconst PopoverContent = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = 4, ...props }, ref) => (\n  <PopoverPrimitive.Portal>\n    <PopoverPrimitive.Content\n      ref={ref}\n      align={align}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className,\n      )}\n      {...props}\n    />\n  </PopoverPrimitive.Portal>\n));\nPopoverContent.displayName = PopoverPrimitive.Content.displayName;\n\nexport { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };\n"
  },
  {
    "path": "client/src/components/ui/select.tsx",
    "content": "import * as React from \"react\";\nimport {\n  CaretSortIcon,\n  CheckIcon,\n  ChevronDownIcon,\n  ChevronUpIcon,\n} from \"@radix-ui/react-icons\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Select = SelectPrimitive.Root;\n\nconst SelectGroup = SelectPrimitive.Group;\n\nconst SelectValue = SelectPrimitive.Value;\n\nconst SelectTrigger = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 hover:border-[#646cff] hover:border-1\",\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <SelectPrimitive.Icon asChild>\n      <CaretSortIcon className=\"h-4 w-4 opacity-50\" />\n    </SelectPrimitive.Icon>\n  </SelectPrimitive.Trigger>\n));\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName;\n\nconst SelectScrollUpButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollUpButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className,\n    )}\n    {...props}\n  >\n    <ChevronUpIcon />\n  </SelectPrimitive.ScrollUpButton>\n));\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;\n\nconst SelectScrollDownButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollDownButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className,\n    )}\n    {...props}\n  >\n    <ChevronDownIcon />\n  </SelectPrimitive.ScrollDownButton>\n));\nSelectScrollDownButton.displayName =\n  SelectPrimitive.ScrollDownButton.displayName;\n\nconst SelectContent = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n  <SelectPrimitive.Portal>\n    <SelectPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        position === \"popper\" &&\n          \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n        className,\n      )}\n      position={position}\n      {...props}\n    >\n      <SelectScrollUpButton />\n      <SelectPrimitive.Viewport\n        className={cn(\n          \"p-1\",\n          position === \"popper\" &&\n            \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\",\n        )}\n      >\n        {children}\n      </SelectPrimitive.Viewport>\n      <SelectScrollDownButton />\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n));\nSelectContent.displayName = SelectPrimitive.Content.displayName;\n\nconst SelectLabel = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label\n    ref={ref}\n    className={cn(\"px-2 py-1.5 text-sm font-semibold\", className)}\n    {...props}\n  />\n));\nSelectLabel.displayName = SelectPrimitive.Label.displayName;\n\nconst SelectItem = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute right-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <SelectPrimitive.ItemIndicator>\n        <CheckIcon className=\"h-4 w-4\" />\n      </SelectPrimitive.ItemIndicator>\n    </span>\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n  </SelectPrimitive.Item>\n));\nSelectItem.displayName = SelectPrimitive.Item.displayName;\n\nconst SelectSeparator = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n));\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName;\n\nexport {\n  Select,\n  SelectGroup,\n  SelectValue,\n  SelectTrigger,\n  SelectContent,\n  SelectLabel,\n  SelectItem,\n  SelectSeparator,\n  SelectScrollUpButton,\n  SelectScrollDownButton,\n};\n"
  },
  {
    "path": "client/src/components/ui/switch.tsx",
    "content": "import * as React from \"react\";\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Switch = React.forwardRef<\n  React.ElementRef<typeof SwitchPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>\n>(({ className, ...props }, ref) => (\n  <SwitchPrimitives.Root\n    className={cn(\n      \"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input\",\n      className,\n    )}\n    {...props}\n    ref={ref}\n  >\n    <SwitchPrimitives.Thumb\n      className={cn(\n        \"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0\",\n      )}\n    />\n  </SwitchPrimitives.Root>\n));\nSwitch.displayName = SwitchPrimitives.Root.displayName;\n\nexport { Switch };\n"
  },
  {
    "path": "client/src/components/ui/tabs.tsx",
    "content": "import * as React from \"react\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Tabs = TabsPrimitive.Root;\n\nconst TabsList = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.List\n    ref={ref}\n    className={cn(\n      \"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground\",\n      className,\n    )}\n    {...props}\n  />\n));\nTabsList.displayName = TabsPrimitive.List.displayName;\n\nconst TabsTrigger = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-muted data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow\",\n      className,\n    )}\n    {...props}\n  />\n));\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName;\n\nconst TabsContent = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Content\n    ref={ref}\n    className={cn(\n      \"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n      className,\n    )}\n    {...props}\n  />\n));\nTabsContent.displayName = TabsPrimitive.Content.displayName;\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent };\n"
  },
  {
    "path": "client/src/components/ui/textarea.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n  ({ className, ...props }, ref) => {\n    return (\n      <textarea\n        className={cn(\n          \"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50\",\n          className,\n        )}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nTextarea.displayName = \"Textarea\";\n\nexport { Textarea };\n"
  },
  {
    "path": "client/src/components/ui/toast.tsx",
    "content": "import * as React from \"react\";\nimport * as ToastPrimitives from \"@radix-ui/react-toast\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/utils\";\nimport { Cross2Icon } from \"@radix-ui/react-icons\";\n\nconst ToastProvider = ToastPrimitives.Provider;\n\nconst ToastViewport = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Viewport>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Viewport\n    ref={ref}\n    className={cn(\n      \"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]\",\n      className,\n    )}\n    {...props}\n  />\n));\nToastViewport.displayName = ToastPrimitives.Viewport.displayName;\n\nconst toastVariants = cva(\n  \"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full\",\n  {\n    variants: {\n      variant: {\n        default: \"border bg-background text-foreground\",\n        destructive:\n          \"destructive group border-destructive bg-destructive text-destructive-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nconst Toast = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &\n    VariantProps<typeof toastVariants>\n>(({ className, variant, ...props }, ref) => {\n  return (\n    <ToastPrimitives.Root\n      ref={ref}\n      className={cn(toastVariants({ variant }), className)}\n      {...props}\n    />\n  );\n});\nToast.displayName = ToastPrimitives.Root.displayName;\n\nconst ToastAction = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Action>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Action\n    ref={ref}\n    className={cn(\n      \"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive\",\n      className,\n    )}\n    {...props}\n  />\n));\nToastAction.displayName = ToastPrimitives.Action.displayName;\n\nconst ToastClose = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Close>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Close\n    ref={ref}\n    className={cn(\n      \"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600\",\n      className,\n    )}\n    toast-close=\"\"\n    {...props}\n  >\n    <Cross2Icon className=\"h-4 w-4\" />\n  </ToastPrimitives.Close>\n));\nToastClose.displayName = ToastPrimitives.Close.displayName;\n\nconst ToastTitle = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Title>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Title\n    ref={ref}\n    className={cn(\"text-sm font-semibold [&+div]:text-xs\", className)}\n    {...props}\n  />\n));\nToastTitle.displayName = ToastPrimitives.Title.displayName;\n\nconst ToastDescription = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Description>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Description\n    ref={ref}\n    className={cn(\"text-sm opacity-90\", className)}\n    {...props}\n  />\n));\nToastDescription.displayName = ToastPrimitives.Description.displayName;\n\ntype ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;\n\ntype ToastActionElement = React.ReactElement<typeof ToastAction>;\n\nexport {\n  type ToastProps,\n  type ToastActionElement,\n  ToastProvider,\n  ToastViewport,\n  Toast,\n  ToastTitle,\n  ToastDescription,\n  ToastClose,\n  ToastAction,\n};\n"
  },
  {
    "path": "client/src/components/ui/toaster.tsx",
    "content": "import { useToast } from \"@/lib/hooks/useToast\";\nimport {\n  Toast,\n  ToastClose,\n  ToastDescription,\n  ToastProvider,\n  ToastTitle,\n  ToastViewport,\n} from \"@/components/ui/toast\";\n\nexport function Toaster() {\n  const { toasts } = useToast();\n\n  return (\n    <ToastProvider>\n      {toasts.map(function ({ id, title, description, action, ...props }) {\n        return (\n          <Toast key={id} {...props}>\n            <div className=\"grid gap-1\">\n              {title && <ToastTitle>{title}</ToastTitle>}\n              {description && (\n                <ToastDescription>{description}</ToastDescription>\n              )}\n            </div>\n            {action}\n            <ToastClose />\n          </Toast>\n        );\n      })}\n      <ToastViewport />\n    </ToastProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/components/ui/tooltip.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst TooltipProvider = TooltipPrimitive.Provider;\n\nconst Tooltip = TooltipPrimitive.Root;\n\nconst TooltipTrigger = TooltipPrimitive.Trigger;\n\nconst TooltipContent = React.forwardRef<\n  React.ElementRef<typeof TooltipPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <TooltipPrimitive.Content\n    ref={ref}\n    sideOffset={sideOffset}\n    className={cn(\n      \"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]\",\n      className,\n    )}\n    {...props}\n  />\n));\nTooltipContent.displayName = TooltipPrimitive.Content.displayName;\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };\n"
  },
  {
    "path": "client/src/index.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n:root {\n  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;\n  line-height: 1.5;\n  font-weight: 400;\n\n  color-scheme: light dark;\n  color: rgba(255, 255, 255, 0.87);\n  background-color: #242424;\n\n  font-synthesis: none;\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\na {\n  font-weight: 500;\n  color: #646cff;\n  text-decoration: inherit;\n}\na:hover {\n  color: #535bf2;\n}\n\nbody {\n  margin: 0;\n  place-items: center;\n  min-width: 320px;\n  min-height: 100vh;\n}\n\nh1 {\n  font-size: 3.2em;\n  line-height: 1.1;\n}\n\n@media (prefers-color-scheme: light) {\n  :root {\n    color: #213547;\n    background-color: #ffffff;\n  }\n  a:hover {\n    color: #747bff;\n  }\n}\n\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --foreground: 222.2 84% 4.9%;\n    --card: 0 0% 100%;\n    --card-foreground: 222.2 84% 4.9%;\n    --popover: 0 0% 100%;\n    --popover-foreground: 222.2 84% 4.9%;\n    --primary: 222.2 47.4% 11.2%;\n    --primary-foreground: 210 40% 98%;\n    --secondary: 210 40% 96.1%;\n    --secondary-foreground: 222.2 47.4% 11.2%;\n    --muted: 210 40% 96.1%;\n    --muted-foreground: 215.4 16.3% 46.9%;\n    --accent: 210 40% 96.1%;\n    --accent-foreground: 222.2 47.4% 11.2%;\n    --destructive: 0 84.2% 60.2%;\n    --destructive-foreground: 210 40% 98%;\n    --border: 214.3 31.8% 91.4%;\n    --input: 214.3 31.8% 91.4%;\n    --ring: 222.2 84% 4.9%;\n    --chart-1: 12 76% 61%;\n    --chart-2: 173 58% 39%;\n    --chart-3: 197 37% 24%;\n    --chart-4: 43 74% 66%;\n    --chart-5: 27 87% 67%;\n    --radius: 0.5rem;\n  }\n  .dark {\n    --background: 222.2 84% 4.9%;\n    --foreground: 210 40% 98%;\n    --card: 222.2 84% 4.9%;\n    --card-foreground: 210 40% 98%;\n    --popover: 222.2 84% 4.9%;\n    --popover-foreground: 210 40% 98%;\n    --primary: 210 40% 98%;\n    --primary-foreground: 222.2 47.4% 11.2%;\n    --secondary: 217.2 32.6% 17.5%;\n    --secondary-foreground: 210 40% 98%;\n    --muted: 217.2 32.6% 17.5%;\n    --muted-foreground: 215 20.2% 65.1%;\n    --accent: 217.2 32.6% 17.5%;\n    --accent-foreground: 210 40% 98%;\n    --destructive: 0 62.8% 30.6%;\n    --destructive-foreground: 210 40% 98%;\n    --border: 217.2 24% 24%;\n    --input: 217.2 24% 24%;\n    --ring: 212.7 26.8% 83.9%;\n    --chart-1: 220 70% 50%;\n    --chart-2: 160 60% 45%;\n    --chart-3: 30 80% 55%;\n    --chart-4: 280 65% 60%;\n    --chart-5: 340 75% 55%;\n  }\n}\n\n@layer base {\n  * {\n    @apply border-border;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n"
  },
  {
    "path": "client/src/lib/__tests__/auth.test.ts",
    "content": "import { discoverScopes } from \"../auth\";\nimport { discoverAuthorizationServerMetadata } from \"@modelcontextprotocol/sdk/client/auth.js\";\n\njest.mock(\"@modelcontextprotocol/sdk/client/auth.js\", () => ({\n  discoverAuthorizationServerMetadata: jest.fn(),\n}));\n\nconst mockDiscoverAuth =\n  discoverAuthorizationServerMetadata as jest.MockedFunction<\n    typeof discoverAuthorizationServerMetadata\n  >;\n\nconst baseMetadata = {\n  issuer: \"https://test.com\",\n  authorization_endpoint: \"https://test.com/authorize\",\n  token_endpoint: \"https://test.com/token\",\n  response_types_supported: [\"code\"],\n  grant_types_supported: [\"authorization_code\"],\n  scopes_supported: [\"read\", \"write\"],\n};\n\ndescribe(\"discoverScopes\", () => {\n  beforeEach(() => {\n    jest.clearAllMocks();\n  });\n\n  const testCases = [\n    {\n      name: \"returns joined scopes from OAuth metadata\",\n      mockResolves: baseMetadata,\n      serverUrl: \"https://example.com\",\n      expected: \"read write\",\n      expectedCallUrl: \"https://example.com/\",\n    },\n    {\n      name: \"prefers resource metadata over OAuth metadata\",\n      mockResolves: baseMetadata,\n      serverUrl: \"https://example.com\",\n      resourceMetadata: {\n        resource: \"https://example.com\",\n        scopes_supported: [\"admin\", \"full\"],\n      },\n      expected: \"admin full\",\n    },\n    {\n      name: \"falls back to OAuth when resource has empty scopes\",\n      mockResolves: baseMetadata,\n      serverUrl: \"https://example.com\",\n      resourceMetadata: {\n        resource: \"https://example.com\",\n        scopes_supported: [],\n      },\n      expected: \"read write\",\n    },\n    {\n      name: \"normalizes URL with port and path\",\n      mockResolves: baseMetadata,\n      serverUrl: \"https://example.com:8080/some/path\",\n      expected: \"read write\",\n      expectedCallUrl: \"https://example.com:8080/\",\n    },\n    {\n      name: \"normalizes URL with trailing slash\",\n      mockResolves: baseMetadata,\n      serverUrl: \"https://example.com/\",\n      expected: \"read write\",\n      expectedCallUrl: \"https://example.com/\",\n    },\n    {\n      name: \"handles single scope\",\n      mockResolves: { ...baseMetadata, scopes_supported: [\"admin\"] },\n      serverUrl: \"https://example.com\",\n      expected: \"admin\",\n    },\n    {\n      name: \"prefers resource metadata even with fewer scopes\",\n      mockResolves: {\n        ...baseMetadata,\n        scopes_supported: [\"read\", \"write\", \"admin\", \"full\"],\n      },\n      serverUrl: \"https://example.com\",\n      resourceMetadata: {\n        resource: \"https://example.com\",\n        scopes_supported: [\"read\"],\n      },\n      expected: \"read\",\n    },\n  ];\n\n  const undefinedCases = [\n    {\n      name: \"returns undefined when OAuth discovery fails\",\n      mockRejects: new Error(\"Discovery failed\"),\n      serverUrl: \"https://example.com\",\n    },\n    {\n      name: \"returns undefined when OAuth has no scopes\",\n      mockResolves: { ...baseMetadata, scopes_supported: [] },\n      serverUrl: \"https://example.com\",\n    },\n    {\n      name: \"returns undefined when scopes_supported missing\",\n      mockResolves: (() => {\n        // eslint-disable-next-line @typescript-eslint/no-unused-vars\n        const { scopes_supported, ...rest } = baseMetadata;\n        return rest;\n      })(),\n      serverUrl: \"https://example.com\",\n    },\n    {\n      name: \"returns undefined with resource metadata but OAuth fails\",\n      mockRejects: new Error(\"No OAuth metadata\"),\n      serverUrl: \"https://example.com\",\n      resourceMetadata: {\n        resource: \"https://example.com\",\n        scopes_supported: [\"read\", \"write\"],\n      },\n    },\n  ];\n\n  test.each(testCases)(\n    \"$name\",\n    async ({\n      mockResolves,\n      serverUrl,\n      resourceMetadata,\n      expected,\n      expectedCallUrl,\n    }) => {\n      mockDiscoverAuth.mockResolvedValue(mockResolves);\n\n      const result = await discoverScopes(serverUrl, resourceMetadata);\n\n      expect(result).toBe(expected);\n      if (expectedCallUrl) {\n        expect(mockDiscoverAuth).toHaveBeenCalledWith(new URL(expectedCallUrl));\n      }\n    },\n  );\n\n  test.each(undefinedCases)(\n    \"$name\",\n    async ({ mockResolves, mockRejects, serverUrl, resourceMetadata }) => {\n      if (mockRejects) {\n        mockDiscoverAuth.mockRejectedValue(mockRejects);\n      } else {\n        mockDiscoverAuth.mockResolvedValue(mockResolves);\n      }\n\n      const result = await discoverScopes(serverUrl, resourceMetadata);\n\n      expect(result).toBeUndefined();\n    },\n  );\n});\n"
  },
  {
    "path": "client/src/lib/auth-types.ts",
    "content": "import {\n  OAuthMetadata,\n  OAuthClientInformationFull,\n  OAuthClientInformation,\n  OAuthTokens,\n  OAuthProtectedResourceMetadata,\n} from \"@modelcontextprotocol/sdk/shared/auth.js\";\n\n// OAuth flow steps\nexport type OAuthStep =\n  | \"metadata_discovery\"\n  | \"client_registration\"\n  | \"authorization_redirect\"\n  | \"authorization_code\"\n  | \"token_request\"\n  | \"complete\";\n\n// Message types for inline feedback\nexport type MessageType = \"success\" | \"error\" | \"info\";\n\nexport interface StatusMessage {\n  type: MessageType;\n  message: string;\n}\n\n// Single state interface for OAuth state\nexport interface AuthDebuggerState {\n  isInitiatingAuth: boolean;\n  oauthTokens: OAuthTokens | null;\n  oauthStep: OAuthStep;\n  resourceMetadata: OAuthProtectedResourceMetadata | null;\n  resourceMetadataError: Error | null;\n  resource: URL | null;\n  authServerUrl: URL | null;\n  oauthMetadata: OAuthMetadata | null;\n  oauthClientInfo: OAuthClientInformationFull | OAuthClientInformation | null;\n  authorizationUrl: URL | null;\n  authorizationCode: string;\n  latestError: Error | null;\n  statusMessage: StatusMessage | null;\n  validationError: string | null;\n}\n\nexport const EMPTY_DEBUGGER_STATE: AuthDebuggerState = {\n  isInitiatingAuth: false,\n  oauthTokens: null,\n  oauthStep: \"metadata_discovery\",\n  oauthMetadata: null,\n  resourceMetadata: null,\n  resourceMetadataError: null,\n  resource: null,\n  authServerUrl: null,\n  oauthClientInfo: null,\n  authorizationUrl: null,\n  authorizationCode: \"\",\n  latestError: null,\n  statusMessage: null,\n  validationError: null,\n};\n"
  },
  {
    "path": "client/src/lib/auth.ts",
    "content": "import { OAuthClientProvider } from \"@modelcontextprotocol/sdk/client/auth.js\";\nimport {\n  OAuthClientInformationSchema,\n  OAuthClientInformation,\n  OAuthTokens,\n  OAuthTokensSchema,\n  OAuthClientMetadata,\n  OAuthMetadata,\n  OAuthProtectedResourceMetadata,\n} from \"@modelcontextprotocol/sdk/shared/auth.js\";\nimport { discoverAuthorizationServerMetadata } from \"@modelcontextprotocol/sdk/client/auth.js\";\nimport { SESSION_KEYS, getServerSpecificKey } from \"./constants\";\nimport { generateOAuthState } from \"@/utils/oauthUtils\";\nimport { validateRedirectUrl } from \"@/utils/urlValidation\";\n\n/**\n * Discovers OAuth scopes from server metadata, with preference for resource metadata scopes\n * @param serverUrl - The MCP server URL\n * @param resourceMetadata - Optional resource metadata containing preferred scopes\n * @returns Promise resolving to space-separated scope string or undefined\n */\nexport const discoverScopes = async (\n  serverUrl: string,\n  resourceMetadata?: OAuthProtectedResourceMetadata,\n): Promise<string | undefined> => {\n  try {\n    const metadata = await discoverAuthorizationServerMetadata(\n      new URL(\"/\", serverUrl),\n    );\n\n    // Prefer resource metadata scopes, but fall back to OAuth metadata if empty\n    const resourceScopes = resourceMetadata?.scopes_supported;\n    const oauthScopes = metadata?.scopes_supported;\n\n    const scopesSupported =\n      resourceScopes && resourceScopes.length > 0\n        ? resourceScopes\n        : oauthScopes;\n\n    return scopesSupported && scopesSupported.length > 0\n      ? scopesSupported.join(\" \")\n      : undefined;\n  } catch (error) {\n    console.debug(\"OAuth scope discovery failed:\", error);\n    return undefined;\n  }\n};\n\nexport const getClientInformationFromSessionStorage = async ({\n  serverUrl,\n  isPreregistered,\n}: {\n  serverUrl: string;\n  isPreregistered?: boolean;\n}) => {\n  const key = getServerSpecificKey(\n    isPreregistered\n      ? SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION\n      : SESSION_KEYS.CLIENT_INFORMATION,\n    serverUrl,\n  );\n\n  const value = sessionStorage.getItem(key);\n  if (!value) {\n    return undefined;\n  }\n\n  return await OAuthClientInformationSchema.parseAsync(JSON.parse(value));\n};\n\nexport const saveClientInformationToSessionStorage = ({\n  serverUrl,\n  clientInformation,\n  isPreregistered,\n}: {\n  serverUrl: string;\n  clientInformation: OAuthClientInformation;\n  isPreregistered?: boolean;\n}) => {\n  const key = getServerSpecificKey(\n    isPreregistered\n      ? SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION\n      : SESSION_KEYS.CLIENT_INFORMATION,\n    serverUrl,\n  );\n  sessionStorage.setItem(key, JSON.stringify(clientInformation));\n};\n\nexport const clearClientInformationFromSessionStorage = ({\n  serverUrl,\n  isPreregistered,\n}: {\n  serverUrl: string;\n  isPreregistered?: boolean;\n}) => {\n  const key = getServerSpecificKey(\n    isPreregistered\n      ? SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION\n      : SESSION_KEYS.CLIENT_INFORMATION,\n    serverUrl,\n  );\n  sessionStorage.removeItem(key);\n};\n\nexport const getScopeFromSessionStorage = (\n  serverUrl: string,\n): string | undefined => {\n  const key = getServerSpecificKey(SESSION_KEYS.SCOPE, serverUrl);\n  const value = sessionStorage.getItem(key);\n  return value || undefined;\n};\n\nexport const saveScopeToSessionStorage = (\n  serverUrl: string,\n  scope: string | undefined,\n) => {\n  const key = getServerSpecificKey(SESSION_KEYS.SCOPE, serverUrl);\n  if (scope) {\n    sessionStorage.setItem(key, scope);\n  } else {\n    sessionStorage.removeItem(key);\n  }\n};\n\nexport const clearScopeFromSessionStorage = (serverUrl: string) => {\n  const key = getServerSpecificKey(SESSION_KEYS.SCOPE, serverUrl);\n  sessionStorage.removeItem(key);\n};\n\nexport class InspectorOAuthClientProvider implements OAuthClientProvider {\n  constructor(protected serverUrl: string) {\n    // Save the server URL to session storage\n    sessionStorage.setItem(SESSION_KEYS.SERVER_URL, serverUrl);\n  }\n\n  get scope(): string | undefined {\n    return getScopeFromSessionStorage(this.serverUrl);\n  }\n\n  get redirectUrl() {\n    return window.location.origin + \"/oauth/callback\";\n  }\n\n  get debugRedirectUrl() {\n    return window.location.origin + \"/oauth/callback/debug\";\n  }\n\n  get redirect_uris() {\n    // Normally register both redirect URIs to support both normal and debug flows\n    // In debug subclass, redirectUrl may be the same as debugRedirectUrl, so remove duplicates\n    // See: https://github.com/modelcontextprotocol/inspector/issues/825\n    return [...new Set([this.redirectUrl, this.debugRedirectUrl])];\n  }\n\n  get clientMetadata(): OAuthClientMetadata {\n    const metadata: OAuthClientMetadata = {\n      redirect_uris: this.redirect_uris,\n      token_endpoint_auth_method: \"none\",\n      grant_types: [\"authorization_code\", \"refresh_token\"],\n      response_types: [\"code\"],\n      client_name: \"MCP Inspector\",\n      client_uri: \"https://github.com/modelcontextprotocol/inspector\",\n    };\n\n    // Only include scope if it's defined and non-empty\n    // Per OAuth spec, omit the scope field entirely if no scopes are requested\n    if (this.scope) {\n      metadata.scope = this.scope;\n    }\n\n    return metadata;\n  }\n\n  state(): string | Promise<string> {\n    return generateOAuthState();\n  }\n\n  async clientInformation() {\n    // Try to get the preregistered client information from session storage first\n    const preregisteredClientInformation =\n      await getClientInformationFromSessionStorage({\n        serverUrl: this.serverUrl,\n        isPreregistered: true,\n      });\n\n    // If no preregistered client information is found, get the dynamically registered client information\n    return (\n      preregisteredClientInformation ??\n      (await getClientInformationFromSessionStorage({\n        serverUrl: this.serverUrl,\n        isPreregistered: false,\n      }))\n    );\n  }\n\n  saveClientInformation(clientInformation: OAuthClientInformation) {\n    // Save the dynamically registered client information to session storage\n    saveClientInformationToSessionStorage({\n      serverUrl: this.serverUrl,\n      clientInformation,\n      isPreregistered: false,\n    });\n  }\n\n  async tokens() {\n    const key = getServerSpecificKey(SESSION_KEYS.TOKENS, this.serverUrl);\n    const tokens = sessionStorage.getItem(key);\n    if (!tokens) {\n      return undefined;\n    }\n\n    return await OAuthTokensSchema.parseAsync(JSON.parse(tokens));\n  }\n\n  saveTokens(tokens: OAuthTokens) {\n    const key = getServerSpecificKey(SESSION_KEYS.TOKENS, this.serverUrl);\n    sessionStorage.setItem(key, JSON.stringify(tokens));\n  }\n\n  redirectToAuthorization(authorizationUrl: URL) {\n    // Validate the URL using the shared utility\n    validateRedirectUrl(authorizationUrl.href);\n    window.location.href = authorizationUrl.href;\n  }\n\n  saveCodeVerifier(codeVerifier: string) {\n    const key = getServerSpecificKey(\n      SESSION_KEYS.CODE_VERIFIER,\n      this.serverUrl,\n    );\n    sessionStorage.setItem(key, codeVerifier);\n  }\n\n  codeVerifier() {\n    const key = getServerSpecificKey(\n      SESSION_KEYS.CODE_VERIFIER,\n      this.serverUrl,\n    );\n    const verifier = sessionStorage.getItem(key);\n    if (!verifier) {\n      throw new Error(\"No code verifier saved for session\");\n    }\n\n    return verifier;\n  }\n\n  clear() {\n    clearClientInformationFromSessionStorage({\n      serverUrl: this.serverUrl,\n      isPreregistered: false,\n    });\n    sessionStorage.removeItem(\n      getServerSpecificKey(SESSION_KEYS.TOKENS, this.serverUrl),\n    );\n    sessionStorage.removeItem(\n      getServerSpecificKey(SESSION_KEYS.CODE_VERIFIER, this.serverUrl),\n    );\n  }\n}\n\n// Overrides redirect URL to use the debug endpoint and allows saving server OAuth metadata to\n// display in debug UI.\nexport class DebugInspectorOAuthClientProvider extends InspectorOAuthClientProvider {\n  get redirectUrl(): string {\n    // We can use the debug redirect URL here because it was already registered\n    // in the parent class's clientMetadata along with the normal redirect URL\n    return this.debugRedirectUrl;\n  }\n\n  saveServerMetadata(metadata: OAuthMetadata) {\n    const key = getServerSpecificKey(\n      SESSION_KEYS.SERVER_METADATA,\n      this.serverUrl,\n    );\n    sessionStorage.setItem(key, JSON.stringify(metadata));\n  }\n\n  getServerMetadata(): OAuthMetadata | null {\n    const key = getServerSpecificKey(\n      SESSION_KEYS.SERVER_METADATA,\n      this.serverUrl,\n    );\n    const metadata = sessionStorage.getItem(key);\n    if (!metadata) {\n      return null;\n    }\n    return JSON.parse(metadata);\n  }\n\n  clear() {\n    super.clear();\n    sessionStorage.removeItem(\n      getServerSpecificKey(SESSION_KEYS.SERVER_METADATA, this.serverUrl),\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/lib/configurationTypes.ts",
    "content": "export type ConfigItem = {\n  label: string;\n  description: string;\n  value: string | number | boolean;\n  is_session_item: boolean;\n};\n\n/**\n * Configuration interface for the MCP Inspector, including settings for the MCP Client,\n * Proxy Server, and Inspector UI/UX.\n *\n * Note: Configuration related to which MCP Server to use or any other MCP Server\n * specific settings are outside the scope of this interface as of now.\n */\nexport type InspectorConfig = {\n  /**\n   * Client-side timeout in milliseconds. The Inspector will cancel the request if no response\n   * is received within this time. Note: This is independent of any server-side timeouts.\n   */\n  MCP_SERVER_REQUEST_TIMEOUT: ConfigItem;\n\n  /**\n   * Whether to reset the timeout on progress notifications. Useful for long-running operations that send periodic progress updates.\n   * Refer: https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/utilities/progress/#progress-flow\n   */\n  MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS: ConfigItem;\n\n  /**\n   * Maximum total time in milliseconds to wait for a response from the MCP server before timing out. Used in conjunction with MCP_SERVER_REQUEST_TIMEOUT_RESET_ON_PROGRESS.\n   * Refer: https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/utilities/progress/#progress-flow\n   */\n  MCP_REQUEST_MAX_TOTAL_TIMEOUT: ConfigItem;\n\n  /**\n   * The full address of the MCP Proxy Server, in case it is running on a non-default address. Example: http://10.1.1.22:5577\n   */\n  MCP_PROXY_FULL_ADDRESS: ConfigItem;\n\n  /**\n   * Session token for authenticating with the MCP Proxy Server. This token is displayed in the proxy server console on startup.\n   */\n  MCP_PROXY_AUTH_TOKEN: ConfigItem;\n\n  /**\n   * Default Time-to-Live (TTL) in milliseconds for newly created tasks.\n   */\n  MCP_TASK_TTL: ConfigItem;\n};\n"
  },
  {
    "path": "client/src/lib/constants.ts",
    "content": "import { InspectorConfig } from \"./configurationTypes\";\nimport packageJson from \"../../package.json\";\n\n// Client identity for MCP connections\nexport const CLIENT_IDENTITY = (() => {\n  const [, name = packageJson.name] = packageJson.name.split(\"/\");\n  const version = packageJson.version;\n  return { name, version };\n})();\n\n// OAuth-related session storage keys\nexport const SESSION_KEYS = {\n  CODE_VERIFIER: \"mcp_code_verifier\",\n  SERVER_URL: \"mcp_server_url\",\n  TOKENS: \"mcp_tokens\",\n  CLIENT_INFORMATION: \"mcp_client_information\",\n  PREREGISTERED_CLIENT_INFORMATION: \"mcp_preregistered_client_information\",\n  SERVER_METADATA: \"mcp_server_metadata\",\n  AUTH_DEBUGGER_STATE: \"mcp_auth_debugger_state\",\n  SCOPE: \"mcp_scope\",\n} as const;\n\n// Generate server-specific session storage keys\nexport const getServerSpecificKey = (\n  baseKey: string,\n  serverUrl?: string,\n): string => {\n  if (!serverUrl) return baseKey;\n  return `[${serverUrl}] ${baseKey}`;\n};\n\nexport type ConnectionStatus =\n  | \"disconnected\"\n  | \"connected\"\n  | \"error\"\n  | \"error-connecting-to-proxy\";\n\nexport const DEFAULT_MCP_PROXY_LISTEN_PORT = \"6277\";\n\n/**\n * Default configuration for the MCP Inspector, Currently persisted in local_storage in the Browser.\n * Future plans: Provide json config file + Browser local_storage to override default values\n **/\nexport const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = {\n  MCP_SERVER_REQUEST_TIMEOUT: {\n    label: \"Request Timeout\",\n    description:\n      \"Client-side timeout (ms) - Inspector will cancel requests after this time\",\n    value: 300000, // 5 minutes - increased to support elicitation and other long-running tools\n    is_session_item: false,\n  },\n  MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS: {\n    label: \"Reset Timeout on Progress\",\n    description: \"Reset timeout on progress notifications\",\n    value: true,\n    is_session_item: false,\n  },\n  MCP_REQUEST_MAX_TOTAL_TIMEOUT: {\n    label: \"Maximum Total Timeout\",\n    description:\n      \"Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)\",\n    value: 60000,\n    is_session_item: false,\n  },\n  MCP_PROXY_FULL_ADDRESS: {\n    label: \"Inspector Proxy Address\",\n    description:\n      \"Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577\",\n    value: \"\",\n    is_session_item: false,\n  },\n  MCP_PROXY_AUTH_TOKEN: {\n    label: \"Proxy Session Token\",\n    description:\n      \"Session token for authenticating with the MCP Proxy Server (displayed in proxy console on startup)\",\n    value: \"\",\n    is_session_item: true,\n  },\n  MCP_TASK_TTL: {\n    label: \"Task TTL\",\n    description:\n      \"Default Time-to-Live (TTL) in milliseconds for newly created tasks\",\n    value: 60000,\n    is_session_item: false,\n  },\n} as const;\n"
  },
  {
    "path": "client/src/lib/hooks/__tests__/useConnection.test.tsx",
    "content": "import { renderHook, act } from \"@testing-library/react\";\nimport { useConnection } from \"../useConnection\";\nimport { z } from \"zod/v3\";\nimport {\n  ClientRequest,\n  CreateTaskResultSchema,\n  JSONRPCMessage,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport type {\n  AnySchema,\n  SchemaOutput,\n} from \"@modelcontextprotocol/sdk/server/zod-compat.js\";\nimport { DEFAULT_INSPECTOR_CONFIG, CLIENT_IDENTITY } from \"../../constants\";\nimport {\n  SSEClientTransportOptions,\n  SseError,\n} from \"@modelcontextprotocol/sdk/client/sse.js\";\nimport {\n  ElicitResult,\n  ElicitRequest,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { auth } from \"@modelcontextprotocol/sdk/client/auth.js\";\nimport { discoverScopes } from \"../../auth\";\nimport { CustomHeaders } from \"../../types/customHeaders\";\n\n// Mock fetch\nglobal.fetch = jest.fn().mockResolvedValue({\n  json: () => Promise.resolve({ status: \"ok\" }),\n  headers: {\n    get: jest.fn().mockReturnValue(null),\n  },\n});\n\n// Mock the SDK dependencies\nconst mockRequest = jest.fn().mockResolvedValue({ test: \"response\" });\nconst mockClient = {\n  request: mockRequest,\n  notification: jest.fn(),\n  connect: jest.fn().mockResolvedValue(undefined),\n  close: jest.fn(),\n  getServerCapabilities: jest.fn(),\n  getServerVersion: jest.fn(),\n  getInstructions: jest.fn(),\n  setNotificationHandler: jest.fn(),\n  setRequestHandler: jest.fn(),\n};\n\n// Mock transport instances\nconst mockSSETransport: {\n  start: jest.Mock;\n  url: URL | undefined;\n  options: SSEClientTransportOptions | undefined;\n  onmessage?: (message: JSONRPCMessage) => void;\n} = {\n  start: jest.fn(),\n  url: undefined,\n  options: undefined,\n  onmessage: undefined,\n};\n\nconst mockStreamableHTTPTransport: {\n  start: jest.Mock;\n  url: URL | undefined;\n  options: SSEClientTransportOptions | undefined;\n} = {\n  start: jest.fn(),\n  url: undefined,\n  options: undefined,\n};\n\njest.mock(\"@modelcontextprotocol/sdk/client/index.js\", () => ({\n  Client: jest.fn().mockImplementation(() => mockClient),\n}));\n\njest.mock(\"@modelcontextprotocol/sdk/client/sse.js\", () => {\n  // Minimal mock class that supports instanceof checks\n  class SseError extends Error {\n    code: number;\n    event: ErrorEvent;\n    constructor(code: number, message: string, event: ErrorEvent) {\n      super(message);\n      this.code = code;\n      this.event = event;\n    }\n  }\n\n  return {\n    SSEClientTransport: jest.fn((url, options) => {\n      mockSSETransport.url = url;\n      mockSSETransport.options = options;\n      return mockSSETransport;\n    }),\n    SseError,\n  };\n});\n\njest.mock(\"@modelcontextprotocol/sdk/client/streamableHttp.js\", () => ({\n  StreamableHTTPClientTransport: jest.fn((url, options) => {\n    mockStreamableHTTPTransport.url = url;\n    mockStreamableHTTPTransport.options = options;\n    return mockStreamableHTTPTransport;\n  }),\n}));\n\njest.mock(\"@modelcontextprotocol/sdk/client/auth.js\", () => ({\n  auth: jest.fn().mockResolvedValue(\"AUTHORIZED\"),\n}));\n\n// Mock the toast hook\nconst mockToast = jest.fn();\njest.mock(\"@/lib/hooks/useToast\", () => ({\n  useToast: () => ({\n    toast: mockToast,\n  }),\n}));\n\n// Mock the auth provider\njest.mock(\"../../auth\", () => ({\n  InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({\n    tokens: jest.fn().mockResolvedValue({ access_token: \"mock-token\" }),\n    redirectUrl: \"http://localhost:3000/oauth/callback\",\n  })),\n  clearClientInformationFromSessionStorage: jest.fn(),\n  saveClientInformationToSessionStorage: jest.fn(),\n  saveScopeToSessionStorage: jest.fn(),\n  clearScopeFromSessionStorage: jest.fn(),\n  discoverScopes: jest.fn(),\n}));\n\nconst mockAuth = auth as jest.MockedFunction<typeof auth>;\nconst mockDiscoverScopes = discoverScopes as jest.MockedFunction<\n  typeof discoverScopes\n>;\n\ndescribe(\"useConnection\", () => {\n  const defaultProps: Parameters<typeof useConnection>[0] = {\n    transportType: \"sse\" as const,\n    command: \"\",\n    args: \"\",\n    sseUrl: \"http://localhost:8080\",\n    env: {},\n    config: DEFAULT_INSPECTOR_CONFIG,\n  };\n\n  describe(\"Request Configuration\", () => {\n    beforeEach(() => {\n      jest.clearAllMocks();\n    });\n\n    test(\"uses the default config values in makeRequest\", async () => {\n      const { result } = renderHook(() => useConnection(defaultProps));\n\n      // Connect the client\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      // Wait for state update\n      await act(async () => {\n        await new Promise((resolve) => setTimeout(resolve, 0));\n      });\n\n      const mockRequest: ClientRequest = {\n        method: \"ping\",\n        params: {},\n      };\n\n      const mockSchema = z.object({\n        test: z.string(),\n      });\n\n      const mockSchemaAny: AnySchema = mockSchema as unknown as AnySchema;\n\n      await act(async () => {\n        await result.current.makeRequest(mockRequest, mockSchemaAny);\n      });\n\n      expect(mockClient.request).toHaveBeenCalledWith(\n        mockRequest,\n        mockSchema,\n        expect.objectContaining({\n          timeout: DEFAULT_INSPECTOR_CONFIG.MCP_SERVER_REQUEST_TIMEOUT.value,\n          maxTotalTimeout:\n            DEFAULT_INSPECTOR_CONFIG.MCP_REQUEST_MAX_TOTAL_TIMEOUT.value,\n          resetTimeoutOnProgress:\n            DEFAULT_INSPECTOR_CONFIG.MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS\n              .value,\n        }),\n      );\n    });\n\n    test(\"overrides the default config values when passed in options in makeRequest\", async () => {\n      const { result } = renderHook(() => useConnection(defaultProps));\n\n      // Connect the client\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      // Wait for state update\n      await act(async () => {\n        await new Promise((resolve) => setTimeout(resolve, 0));\n      });\n\n      const mockRequest: ClientRequest = {\n        method: \"ping\",\n        params: {},\n      };\n\n      const mockSchema = z.object({\n        test: z.string(),\n      });\n\n      const mockSchemaAny: AnySchema = mockSchema as unknown as AnySchema;\n\n      await act(async () => {\n        await result.current.makeRequest(mockRequest, mockSchemaAny, {\n          timeout: 1000,\n          maxTotalTimeout: 2000,\n          resetTimeoutOnProgress: false,\n        });\n      });\n\n      expect(mockClient.request).toHaveBeenCalledWith(\n        mockRequest,\n        mockSchema,\n        expect.objectContaining({\n          timeout: 1000,\n          maxTotalTimeout: 2000,\n          resetTimeoutOnProgress: false,\n        }),\n      );\n    });\n  });\n\n  describe(\"Receiver-side Tasks (task-augmented incoming requests)\", () => {\n    beforeEach(() => {\n      jest.clearAllMocks();\n    });\n\n    test(\"declares tasks.requests.sampling.createMessage when onPendingRequest is provided\", async () => {\n      const Client = jest.requireMock(\n        \"@modelcontextprotocol/sdk/client/index.js\",\n      ).Client;\n\n      const propsWithPending = {\n        ...defaultProps,\n        onPendingRequest: jest.fn(),\n      };\n\n      const { result } = renderHook(() => useConnection(propsWithPending));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      expect(Client).toHaveBeenCalledWith(\n        expect.any(Object),\n        expect.objectContaining({\n          capabilities: expect.objectContaining({\n            tasks: expect.objectContaining({\n              requests: expect.objectContaining({\n                sampling: expect.objectContaining({\n                  createMessage: {},\n                }),\n              }),\n            }),\n          }),\n        }),\n      );\n    });\n\n    test(\"task-augmented sampling/createMessage returns { task } and tasks/result blocks until resolved\", async () => {\n      let pendingResolve: ((value: unknown) => void) | undefined;\n      let pendingReject: ((reason?: unknown) => void) | undefined;\n\n      const mockOnPendingRequest = jest.fn((_request, resolve, reject) => {\n        pendingResolve = resolve;\n        pendingReject = reject;\n      });\n\n      const propsWithPending = {\n        ...defaultProps,\n        onPendingRequest: mockOnPendingRequest,\n      };\n\n      const { result } = renderHook(() => useConnection(propsWithPending));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      const samplingRequest = {\n        method: \"sampling/createMessage\",\n        params: {\n          task: { ttl: 0 },\n          messages: [\n            {\n              role: \"user\",\n              content: { type: \"text\", text: \"hello\" },\n            },\n          ],\n          maxTokens: 1,\n        },\n      };\n\n      // Locate the sampling/createMessage handler\n      const samplingHandlerCall = mockClient.setRequestHandler.mock.calls.find(\n        (call) => {\n          try {\n            const schema = call[0];\n            const parseResult =\n              schema.safeParse && schema.safeParse(samplingRequest);\n            return parseResult?.success;\n          } catch {\n            return false;\n          }\n        },\n      );\n\n      expect(samplingHandlerCall).toBeDefined();\n      const [, samplingHandler] = samplingHandlerCall;\n\n      // Invoke handler; should return a CreateTaskResult immediately\n      let createTaskResult: SchemaOutput<typeof CreateTaskResultSchema>;\n      await act(async () => {\n        createTaskResult = await samplingHandler(samplingRequest);\n      });\n\n      expect(createTaskResult).toHaveProperty(\"task\");\n      expect(createTaskResult.task).toEqual(\n        expect.objectContaining({\n          taskId: expect.any(String),\n          status: \"input_required\",\n          ttl: 0,\n          createdAt: expect.any(String),\n          lastUpdatedAt: expect.any(String),\n        }),\n      );\n\n      expect(mockOnPendingRequest).toHaveBeenCalledTimes(1);\n      expect(pendingResolve).toBeDefined();\n      expect(pendingReject).toBeDefined();\n\n      const taskId = createTaskResult.task.taskId as string;\n\n      // Locate tasks/get and tasks/result handlers\n      const taskGetRequest = { method: \"tasks/get\", params: { taskId } };\n      const taskResultRequest = { method: \"tasks/result\", params: { taskId } };\n\n      const taskGetHandlerCall = mockClient.setRequestHandler.mock.calls.find(\n        (call) => {\n          try {\n            const schema = call[0];\n            const parseResult =\n              schema.safeParse && schema.safeParse(taskGetRequest);\n            return parseResult?.success;\n          } catch {\n            return false;\n          }\n        },\n      );\n      const taskResultHandlerCall =\n        mockClient.setRequestHandler.mock.calls.find((call) => {\n          try {\n            const schema = call[0];\n            const parseResult =\n              schema.safeParse && schema.safeParse(taskResultRequest);\n            return parseResult?.success;\n          } catch {\n            return false;\n          }\n        });\n\n      expect(taskGetHandlerCall).toBeDefined();\n      expect(taskResultHandlerCall).toBeDefined();\n\n      const [, taskGetHandler] = taskGetHandlerCall;\n      const [, taskResultHandler] = taskResultHandlerCall;\n\n      // Verify tasks/get sees the in-progress task\n      const getBefore = await taskGetHandler(taskGetRequest);\n      expect(getBefore.status).toBe(\"input_required\");\n\n      // tasks/result should block until user flow resolves\n      const payloadPromise = taskResultHandler(taskResultRequest);\n      const race = await Promise.race([\n        payloadPromise.then(() => \"resolved\"),\n        new Promise((r) => setTimeout(() => r(\"timeout\"), 10)),\n      ]);\n      expect(race).toBe(\"timeout\");\n\n      const mockPayload = {\n        model: \"test-model\",\n        role: \"assistant\",\n        content: { type: \"text\", text: \"ok\" },\n      };\n\n      await act(async () => {\n        pendingResolve!(mockPayload);\n        // Let the background updater run\n        await new Promise((r) => setTimeout(r, 0));\n      });\n\n      await expect(payloadPromise).resolves.toEqual(mockPayload);\n\n      const getAfter = await taskGetHandler(taskGetRequest);\n      expect(getAfter.status).toBe(\"completed\");\n    });\n\n    test(\"task-augmented elicitation/create returns { task } immediately\", async () => {\n      const mockOnElicitationRequest = jest.fn();\n      const propsWithElicitation = {\n        ...defaultProps,\n        onElicitationRequest: mockOnElicitationRequest,\n      };\n\n      const { result } = renderHook(() => useConnection(propsWithElicitation));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      const elicitationRequest = {\n        method: \"elicitation/create\",\n        params: {\n          task: { ttl: 0 },\n          message: \"Please provide your name\",\n          requestedSchema: {\n            type: \"object\",\n            properties: {\n              name: { type: \"string\" },\n            },\n            required: [\"name\"],\n          },\n        },\n      };\n\n      const elicitRequestHandlerCall =\n        mockClient.setRequestHandler.mock.calls.find((call) => {\n          try {\n            const schema = call[0];\n            const parseResult =\n              schema.safeParse && schema.safeParse(elicitationRequest);\n            return parseResult?.success;\n          } catch {\n            return false;\n          }\n        });\n\n      expect(elicitRequestHandlerCall).toBeDefined();\n      const [, handler] = elicitRequestHandlerCall;\n\n      mockOnElicitationRequest.mockImplementation((_request, resolve) => {\n        resolve({ action: \"accept\", content: { name: \"test\" } });\n      });\n\n      const resultValue = await handler(elicitationRequest);\n\n      expect(resultValue).toHaveProperty(\"task\");\n      expect(resultValue.task).toEqual(\n        expect.objectContaining({\n          taskId: expect.any(String),\n          status: \"input_required\",\n          ttl: 0,\n        }),\n      );\n    });\n  });\n\n  test(\"throws error when mcpClient is not connected\", async () => {\n    const { result } = renderHook(() => {\n      const { makeRequest } = useConnection(defaultProps) as unknown as {\n        makeRequest: (\n          request: ClientRequest,\n          schema: AnySchema,\n        ) => Promise<unknown>;\n      };\n      return { makeRequest };\n    });\n\n    const mockRequest: ClientRequest = {\n      method: \"ping\",\n      params: {},\n    };\n\n    const mockSchema = z.object({\n      test: z.string(),\n    });\n\n    const mockSchemaAny: AnySchema = mockSchema as unknown as AnySchema;\n\n    await expect(\n      result.current.makeRequest(mockRequest, mockSchemaAny),\n    ).rejects.toThrow(\"MCP client not connected\");\n  });\n\n  describe(\"Elicitation Support\", () => {\n    beforeEach(() => {\n      jest.clearAllMocks();\n    });\n\n    test(\"declares elicitation capability during client initialization\", async () => {\n      const Client = jest.requireMock(\n        \"@modelcontextprotocol/sdk/client/index.js\",\n      ).Client;\n\n      const { result } = renderHook(() => useConnection(defaultProps));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      expect(Client).toHaveBeenCalledWith(\n        expect.objectContaining({\n          name: CLIENT_IDENTITY.name,\n          version: CLIENT_IDENTITY.version,\n        }),\n        expect.objectContaining({\n          capabilities: expect.objectContaining({\n            elicitation: {},\n          }),\n        }),\n      );\n    });\n\n    test(\"sets up elicitation request handler when onElicitationRequest is provided\", async () => {\n      const mockOnElicitationRequest = jest.fn();\n      const propsWithElicitation = {\n        ...defaultProps,\n        onElicitationRequest: mockOnElicitationRequest,\n      };\n\n      const { result } = renderHook(() => useConnection(propsWithElicitation));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      const elicitRequestHandlerCall =\n        mockClient.setRequestHandler.mock.calls.find((call) => {\n          try {\n            const schema = call[0];\n            const testRequest = {\n              method: \"elicitation/create\",\n              params: {\n                message: \"test message\",\n                requestedSchema: {\n                  type: \"object\",\n                  properties: {\n                    name: { type: \"string\" },\n                  },\n                },\n              },\n            };\n            const parseResult =\n              schema.safeParse && schema.safeParse(testRequest);\n            return parseResult?.success;\n          } catch {\n            return false;\n          }\n        });\n\n      expect(elicitRequestHandlerCall).toBeDefined();\n      expect(mockClient.setRequestHandler).toHaveBeenCalledWith(\n        expect.any(Object),\n        expect.any(Function),\n      );\n    });\n\n    test(\"does not set up elicitation request handler when onElicitationRequest is not provided\", async () => {\n      const { result } = renderHook(() => useConnection(defaultProps));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      const elicitRequestHandlerCall =\n        mockClient.setRequestHandler.mock.calls.find((call) => {\n          try {\n            const schema = call[0];\n            const testRequest = {\n              method: \"elicitation/create\",\n              params: {\n                message: \"test message\",\n                requestedSchema: {\n                  type: \"object\",\n                  properties: {\n                    name: { type: \"string\" },\n                  },\n                },\n              },\n            };\n            const parseResult =\n              schema.safeParse && schema.safeParse(testRequest);\n            return parseResult?.success;\n          } catch {\n            return false;\n          }\n        });\n\n      expect(elicitRequestHandlerCall).toBeUndefined();\n    });\n\n    test(\"elicitation request handler calls onElicitationRequest callback\", async () => {\n      const mockOnElicitationRequest = jest.fn();\n      const propsWithElicitation = {\n        ...defaultProps,\n        onElicitationRequest: mockOnElicitationRequest,\n      };\n\n      const { result } = renderHook(() => useConnection(propsWithElicitation));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      const elicitRequestHandlerCall =\n        mockClient.setRequestHandler.mock.calls.find((call) => {\n          try {\n            const schema = call[0];\n            const testRequest = {\n              method: \"elicitation/create\",\n              params: {\n                message: \"test message\",\n                requestedSchema: {\n                  type: \"object\",\n                  properties: {\n                    name: { type: \"string\" },\n                  },\n                },\n              },\n            };\n            const parseResult =\n              schema.safeParse && schema.safeParse(testRequest);\n            return parseResult?.success;\n          } catch {\n            return false;\n          }\n        });\n\n      expect(elicitRequestHandlerCall).toBeDefined();\n      const [, handler] = elicitRequestHandlerCall;\n\n      const mockElicitationRequest: ElicitRequest = {\n        method: \"elicitation/create\",\n        params: {\n          message: \"Please provide your name\",\n          requestedSchema: {\n            type: \"object\",\n            properties: {\n              name: { type: \"string\" },\n            },\n            required: [\"name\"],\n          },\n        },\n      };\n\n      mockOnElicitationRequest.mockImplementation((_request, resolve) => {\n        resolve({ action: \"accept\", content: { name: \"test\" } });\n      });\n\n      await act(async () => {\n        await handler(mockElicitationRequest);\n      });\n\n      expect(mockOnElicitationRequest).toHaveBeenCalledWith(\n        mockElicitationRequest,\n        expect.any(Function),\n      );\n    });\n\n    test(\"elicitation request handler returns a promise that resolves with the callback result\", async () => {\n      const mockOnElicitationRequest = jest.fn();\n      const propsWithElicitation = {\n        ...defaultProps,\n        onElicitationRequest: mockOnElicitationRequest,\n      };\n\n      const { result } = renderHook(() => useConnection(propsWithElicitation));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      const elicitRequestHandlerCall =\n        mockClient.setRequestHandler.mock.calls.find((call) => {\n          try {\n            const schema = call[0];\n            const testRequest = {\n              method: \"elicitation/create\",\n              params: {\n                message: \"test message\",\n                requestedSchema: {\n                  type: \"object\",\n                  properties: {\n                    name: { type: \"string\" },\n                  },\n                },\n              },\n            };\n            const parseResult =\n              schema.safeParse && schema.safeParse(testRequest);\n            return parseResult?.success;\n          } catch {\n            return false;\n          }\n        });\n\n      const [, handler] = elicitRequestHandlerCall;\n\n      const mockElicitationRequest: ElicitRequest = {\n        method: \"elicitation/create\",\n        params: {\n          message: \"Please provide your name\",\n          requestedSchema: {\n            type: \"object\",\n            properties: {\n              name: { type: \"string\" },\n            },\n            required: [\"name\"],\n          },\n        },\n      };\n\n      const mockResponse: ElicitResult = {\n        action: \"accept\",\n        content: { name: \"John Doe\" },\n      };\n\n      mockOnElicitationRequest.mockImplementation((_request, resolve) => {\n        resolve(mockResponse);\n      });\n\n      let handlerResult;\n      await act(async () => {\n        handlerResult = await handler(mockElicitationRequest);\n      });\n\n      expect(handlerResult).toEqual(mockResponse);\n    });\n  });\n\n  describe(\"Ref Resolution\", () => {\n    beforeEach(() => {\n      jest.clearAllMocks();\n    });\n\n    test(\"resolves $ref references in requestedSchema properties before validation\", async () => {\n      const mockProtocolOnMessage = jest.fn();\n\n      mockSSETransport.onmessage = mockProtocolOnMessage;\n\n      const { result } = renderHook(() => useConnection(defaultProps));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      const mockRequestWithRef: JSONRPCMessage = {\n        jsonrpc: \"2.0\",\n        id: 1,\n        method: \"elicitation/create\",\n        params: {\n          message: \"Please provide your information\",\n          requestedSchema: {\n            type: \"object\",\n            properties: {\n              source: {\n                type: \"string\",\n                minLength: 1,\n                title: \"A Connectable Node\",\n              },\n              target: {\n                $ref: \"#/properties/source\",\n              },\n            },\n          },\n        },\n      };\n\n      await act(async () => {\n        mockSSETransport.onmessage!(mockRequestWithRef);\n      });\n\n      expect(mockProtocolOnMessage).toHaveBeenCalledTimes(1);\n\n      const message = mockProtocolOnMessage.mock.calls[0][0];\n      expect(message.params.requestedSchema.properties.target).toEqual({\n        type: \"string\",\n        minLength: 1,\n        title: \"A Connectable Node\",\n      });\n    });\n\n    test(\"resolves $ref references to $defs in requestedSchema\", async () => {\n      const mockProtocolOnMessage = jest.fn();\n\n      mockSSETransport.onmessage = mockProtocolOnMessage;\n\n      const { result } = renderHook(() => useConnection(defaultProps));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      const mockRequestWithDefs: JSONRPCMessage = {\n        jsonrpc: \"2.0\",\n        id: 1,\n        method: \"elicitation/create\",\n        params: {\n          message: \"Please provide your information\",\n          requestedSchema: {\n            type: \"object\",\n            properties: {\n              user: {\n                $ref: \"#/$defs/UserInput\",\n              },\n            },\n            $defs: {\n              UserInput: {\n                type: \"object\",\n                properties: {\n                  name: {\n                    type: \"string\",\n                    title: \"Name\",\n                  },\n                  age: {\n                    type: \"integer\",\n                    title: \"Age\",\n                    minimum: 0,\n                  },\n                },\n                required: [\"name\"],\n              },\n            },\n          },\n        },\n      };\n\n      await act(async () => {\n        mockSSETransport.onmessage!(mockRequestWithDefs);\n      });\n\n      expect(mockProtocolOnMessage).toHaveBeenCalledTimes(1);\n\n      const message = mockProtocolOnMessage.mock.calls[0][0];\n      // The $ref should be resolved to the actual UserInput definition\n      expect(message.params.requestedSchema.properties.user).toEqual({\n        type: \"object\",\n        properties: {\n          name: {\n            type: \"string\",\n            title: \"Name\",\n          },\n          age: {\n            type: \"integer\",\n            title: \"Age\",\n            minimum: 0,\n          },\n        },\n        required: [\"name\"],\n      });\n    });\n  });\n\n  describe(\"URL Port Handling\", () => {\n    const SSEClientTransport = jest.requireMock(\n      \"@modelcontextprotocol/sdk/client/sse.js\",\n    ).SSEClientTransport;\n    const StreamableHTTPClientTransport = jest.requireMock(\n      \"@modelcontextprotocol/sdk/client/streamableHttp.js\",\n    ).StreamableHTTPClientTransport;\n\n    beforeEach(() => {\n      jest.clearAllMocks();\n    });\n\n    test(\"preserves HTTPS port number when connecting\", async () => {\n      const props = {\n        ...defaultProps,\n        sseUrl: \"https://example.com:8443/api\",\n        transportType: \"sse\" as const,\n      };\n\n      const { result } = renderHook(() => useConnection(props));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      const call = SSEClientTransport.mock.calls[0][0];\n      expect(call.toString()).toContain(\n        \"url=https%3A%2F%2Fexample.com%3A8443%2Fapi\",\n      );\n    });\n\n    test(\"preserves HTTP port number when connecting\", async () => {\n      const props = {\n        ...defaultProps,\n        sseUrl: \"http://localhost:3000/api\",\n        transportType: \"sse\" as const,\n      };\n\n      const { result } = renderHook(() => useConnection(props));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      const call = SSEClientTransport.mock.calls[0][0];\n      expect(call.toString()).toContain(\n        \"url=http%3A%2F%2Flocalhost%3A3000%2Fapi\",\n      );\n    });\n\n    test(\"uses default port for HTTPS when not specified\", async () => {\n      const props = {\n        ...defaultProps,\n        sseUrl: \"https://example.com/api\",\n        transportType: \"sse\" as const,\n      };\n\n      const { result } = renderHook(() => useConnection(props));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      const call = SSEClientTransport.mock.calls[0][0];\n      expect(call.toString()).toContain(\"url=https%3A%2F%2Fexample.com%2Fapi\");\n      expect(call.toString()).not.toContain(\"%3A443\");\n    });\n\n    test(\"preserves port number in streamable-http transport\", async () => {\n      const props = {\n        ...defaultProps,\n        sseUrl: \"https://example.com:8443/api\",\n        transportType: \"streamable-http\" as const,\n      };\n\n      const { result } = renderHook(() => useConnection(props));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      const call = StreamableHTTPClientTransport.mock.calls[0][0];\n      expect(call.toString()).toContain(\n        \"url=https%3A%2F%2Fexample.com%3A8443%2Fapi\",\n      );\n    });\n  });\n\n  describe(\"Proxy Authentication Headers\", () => {\n    beforeEach(() => {\n      jest.clearAllMocks();\n      // Reset the mock transport objects\n      mockSSETransport.url = undefined;\n      mockSSETransport.options = undefined;\n      mockStreamableHTTPTransport.url = undefined;\n      mockStreamableHTTPTransport.options = undefined;\n    });\n\n    test(\"sends X-MCP-Proxy-Auth header when proxy auth token is configured for proxy connectionType\", async () => {\n      const propsWithProxyAuth = {\n        ...defaultProps,\n        connectionType: \"proxy\" as const,\n        config: {\n          ...DEFAULT_INSPECTOR_CONFIG,\n          MCP_PROXY_AUTH_TOKEN: {\n            ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,\n            value: \"test-proxy-token\",\n          },\n        },\n      };\n\n      const { result } = renderHook(() => useConnection(propsWithProxyAuth));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      // Check that the transport was created with the correct headers\n      expect(mockSSETransport.options).toBeDefined();\n      expect(mockSSETransport.options?.requestInit).toBeDefined();\n\n      expect(mockSSETransport.options?.requestInit?.headers).toHaveProperty(\n        \"X-MCP-Proxy-Auth\",\n        \"Bearer test-proxy-token\",\n      );\n      expect(mockSSETransport?.options?.eventSourceInit?.fetch).toBeDefined();\n\n      // Verify the fetch function includes the proxy auth header\n      const mockFetch = mockSSETransport.options?.eventSourceInit?.fetch;\n      const testUrl = \"http://test.com\";\n      await mockFetch?.(testUrl, {\n        headers: {\n          Accept: \"text/event-stream\",\n        },\n        cache: \"no-store\",\n        mode: \"cors\",\n        signal: new AbortController().signal,\n        redirect: \"follow\",\n        credentials: \"include\",\n      });\n\n      expect(global.fetch).toHaveBeenCalledTimes(2);\n      expect(\n        (global.fetch as jest.Mock).mock.calls[0][1].headers,\n      ).toHaveProperty(\"X-MCP-Proxy-Auth\", \"Bearer test-proxy-token\");\n      expect((global.fetch as jest.Mock).mock.calls[1][0]).toBe(testUrl);\n      expect(\n        (global.fetch as jest.Mock).mock.calls[1][1].headers,\n      ).toHaveProperty(\"X-MCP-Proxy-Auth\", \"Bearer test-proxy-token\");\n    });\n\n    test(\"does NOT send X-MCP-Proxy-Auth header when proxy auth token is configured for direct connectionType\", async () => {\n      const propsWithProxyAuth = {\n        ...defaultProps,\n        connectionType: \"direct\" as const,\n        config: {\n          ...DEFAULT_INSPECTOR_CONFIG,\n          MCP_PROXY_AUTH_TOKEN: {\n            ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,\n            value: \"test-proxy-token\",\n          },\n        },\n      };\n\n      const { result } = renderHook(() => useConnection(propsWithProxyAuth));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      // Check that the transport was created with the correct headers\n      expect(mockSSETransport.options).toBeDefined();\n      expect(mockSSETransport.options?.requestInit).toBeDefined();\n\n      // Verify that X-MCP-Proxy-Auth header is NOT present for direct connections\n      expect(mockSSETransport.options?.requestInit?.headers).not.toHaveProperty(\n        \"X-MCP-Proxy-Auth\",\n      );\n      expect(mockSSETransport?.options?.fetch).toBeDefined();\n\n      // Verify the fetch function does NOT include the proxy auth header\n      const mockFetch = mockSSETransport.options?.fetch;\n      const testUrl = \"http://test.com\";\n      await mockFetch?.(testUrl, {\n        headers: {\n          Accept: \"text/event-stream\",\n        },\n        cache: \"no-store\",\n        mode: \"cors\",\n        signal: new AbortController().signal,\n        redirect: \"follow\",\n        credentials: \"include\",\n      });\n\n      expect(global.fetch).toHaveBeenCalledTimes(1);\n      expect((global.fetch as jest.Mock).mock.calls[0][0]).toBe(testUrl);\n      expect(\n        (global.fetch as jest.Mock).mock.calls[0][1].headers,\n      ).not.toHaveProperty(\"X-MCP-Proxy-Auth\");\n    });\n\n    test(\"does NOT send Authorization header for proxy auth\", async () => {\n      const propsWithProxyAuth = {\n        ...defaultProps,\n        config: {\n          ...DEFAULT_INSPECTOR_CONFIG,\n          proxyAuthToken: \"test-proxy-token\",\n        },\n      };\n\n      const { result } = renderHook(() => useConnection(propsWithProxyAuth));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      // Check that Authorization header is NOT used for proxy auth\n      expect(mockSSETransport.options?.requestInit?.headers).not.toHaveProperty(\n        \"Authorization\",\n        \"Bearer test-proxy-token\",\n      );\n    });\n\n    test(\"preserves server Authorization header when proxy auth is configured\", async () => {\n      const customHeaders: CustomHeaders = [\n        {\n          name: \"Authorization\",\n          value: \"Bearer server-auth-token\",\n          enabled: true,\n        },\n      ];\n\n      const propsWithBothAuth = {\n        ...defaultProps,\n        customHeaders,\n        config: {\n          ...DEFAULT_INSPECTOR_CONFIG,\n          MCP_PROXY_AUTH_TOKEN: {\n            ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,\n            value: \"test-proxy-token\",\n          },\n        },\n      };\n\n      const { result } = renderHook(() => useConnection(propsWithBothAuth));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      // Check that both headers are present and distinct\n      const headers = mockSSETransport.options?.requestInit?.headers;\n      expect(headers).toHaveProperty(\n        \"Authorization\",\n        \"Bearer server-auth-token\",\n      );\n      expect(headers).toHaveProperty(\n        \"X-MCP-Proxy-Auth\",\n        \"Bearer test-proxy-token\",\n      );\n    });\n\n    test(\"sends X-MCP-Proxy-Auth in health check requests\", async () => {\n      const fetchMock = global.fetch as jest.Mock;\n      fetchMock.mockClear();\n\n      const propsWithProxyAuth = {\n        ...defaultProps,\n        config: {\n          ...DEFAULT_INSPECTOR_CONFIG,\n          MCP_PROXY_AUTH_TOKEN: {\n            ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,\n            value: \"test-proxy-token\",\n          },\n        },\n      };\n\n      const { result } = renderHook(() => useConnection(propsWithProxyAuth));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      // Find the health check call\n      const healthCheckCall = fetchMock.mock.calls.find(\n        (call) => call[0].pathname === \"/health\",\n      );\n\n      expect(healthCheckCall).toBeDefined();\n      expect(healthCheckCall[1].headers).toHaveProperty(\n        \"X-MCP-Proxy-Auth\",\n        \"Bearer test-proxy-token\",\n      );\n    });\n\n    test(\"works correctly with streamable-http transport\", async () => {\n      const propsWithStreamableHttp = {\n        ...defaultProps,\n        transportType: \"streamable-http\" as const,\n        config: {\n          ...DEFAULT_INSPECTOR_CONFIG,\n          MCP_PROXY_AUTH_TOKEN: {\n            ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,\n            value: \"test-proxy-token\",\n          },\n        },\n      };\n\n      const { result } = renderHook(() =>\n        useConnection(propsWithStreamableHttp),\n      );\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      // Check that the streamable HTTP transport was created with the correct headers\n      expect(mockStreamableHTTPTransport.options).toBeDefined();\n      expect(\n        mockStreamableHTTPTransport.options?.requestInit?.headers,\n      ).toHaveProperty(\"X-MCP-Proxy-Auth\", \"Bearer test-proxy-token\");\n    });\n  });\n\n  describe(\"Custom Headers\", () => {\n    beforeEach(() => {\n      jest.clearAllMocks();\n      // Reset the mock transport objects\n      mockSSETransport.url = undefined;\n      mockSSETransport.options = undefined;\n      mockStreamableHTTPTransport.url = undefined;\n      mockStreamableHTTPTransport.options = undefined;\n    });\n\n    test(\"sends multiple custom headers correctly\", async () => {\n      const customHeaders: CustomHeaders = [\n        { name: \"Authorization\", value: \"Bearer token123\", enabled: true },\n        { name: \"X-Tenant-ID\", value: \"acme-inc\", enabled: true },\n        { name: \"X-Environment\", value: \"staging\", enabled: true },\n      ];\n\n      const propsWithCustomHeaders = {\n        ...defaultProps,\n        customHeaders,\n      };\n\n      const { result } = renderHook(() =>\n        useConnection(propsWithCustomHeaders),\n      );\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      // Check that the transport was created with the correct headers\n      expect(mockSSETransport.options).toBeDefined();\n      expect(mockSSETransport.options?.requestInit?.headers).toBeDefined();\n\n      const headers = mockSSETransport.options?.requestInit?.headers;\n      expect(headers).toHaveProperty(\"Authorization\", \"Bearer token123\");\n      expect(headers).toHaveProperty(\"X-Tenant-ID\", \"acme-inc\");\n      expect(headers).toHaveProperty(\"X-Environment\", \"staging\");\n      expect(headers).toHaveProperty(\n        \"x-custom-auth-headers\",\n        JSON.stringify([\"X-Tenant-ID\", \"X-Environment\"]),\n      );\n    });\n\n    test(\"ignores disabled custom headers\", async () => {\n      const customHeaders: CustomHeaders = [\n        { name: \"Authorization\", value: \"Bearer token123\", enabled: true },\n        { name: \"X-Disabled\", value: \"should-not-appear\", enabled: false },\n        { name: \"X-Enabled\", value: \"should-appear\", enabled: true },\n      ];\n\n      const propsWithCustomHeaders = {\n        ...defaultProps,\n        customHeaders,\n      };\n\n      const { result } = renderHook(() =>\n        useConnection(propsWithCustomHeaders),\n      );\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      const headers = mockSSETransport.options?.requestInit?.headers;\n      expect(headers).toHaveProperty(\"Authorization\", \"Bearer token123\");\n      expect(headers).toHaveProperty(\"X-Enabled\", \"should-appear\");\n      expect(headers).not.toHaveProperty(\"X-Disabled\");\n    });\n\n    test(\"handles migrated legacy auth via custom headers\", async () => {\n      // Simulate what App.tsx would do - migrate legacy auth to custom headers\n      const customHeaders: CustomHeaders = [\n        { name: \"X-Custom-Auth\", value: \"legacy-token\", enabled: true },\n      ];\n\n      const propsWithMigratedAuth = {\n        ...defaultProps,\n        customHeaders,\n      };\n\n      const { result } = renderHook(() => useConnection(propsWithMigratedAuth));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      const headers = mockSSETransport.options?.requestInit?.headers;\n      expect(headers).toHaveProperty(\"X-Custom-Auth\", \"legacy-token\");\n      expect(headers).toHaveProperty(\n        \"x-custom-auth-headers\",\n        JSON.stringify([\"X-Custom-Auth\"]),\n      );\n    });\n\n    test(\"uses OAuth token when no custom headers or legacy auth provided\", async () => {\n      const propsWithoutAuth = {\n        ...defaultProps,\n      };\n\n      const { result } = renderHook(() => useConnection(propsWithoutAuth));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      const headers = mockSSETransport.options?.requestInit?.headers;\n      expect(headers).toHaveProperty(\"Authorization\", \"Bearer mock-token\");\n    });\n\n    test(\"warns of enabled empty Bearer token\", async () => {\n      // This test prevents regression of the bug where default \"Bearer \" header\n      // prevented OAuth token injection, causing infinite auth loops\n      const customHeaders: CustomHeaders = [\n        {\n          name: \"Authorization\",\n          value: \"Bearer \", // Empty Bearer token placeholder\n          enabled: true, // enabled\n        },\n      ];\n\n      const propsWithEmptyBearer = {\n        ...defaultProps,\n        customHeaders,\n      };\n\n      const { result } = renderHook(() => useConnection(propsWithEmptyBearer));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      const headers = mockSSETransport.options?.requestInit?.headers;\n\n      expect(headers).toHaveProperty(\"Authorization\", \"Bearer\");\n      // Should not have the x-custom-auth-headers since Authorization is standard\n      expect(headers).not.toHaveProperty(\"x-custom-auth-headers\");\n\n      // Should show toast notification for empty Authorization header\n      expect(mockToast).toHaveBeenCalledWith({\n        title: \"Invalid Authorization Header\",\n        description: expect.any(String),\n        variant: \"destructive\",\n      });\n    });\n\n    test(\"prioritizes custom headers over legacy auth\", async () => {\n      const customHeaders: CustomHeaders = [\n        { name: \"Authorization\", value: \"Bearer custom-token\", enabled: true },\n      ];\n\n      const propsWithBothAuth = {\n        ...defaultProps,\n        customHeaders,\n        bearerToken: \"legacy-token\",\n        headerName: \"Authorization\",\n      };\n\n      const { result } = renderHook(() => useConnection(propsWithBothAuth));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      const headers = mockSSETransport.options?.requestInit?.headers;\n      expect(headers).toHaveProperty(\"Authorization\", \"Bearer custom-token\");\n    });\n  });\n\n  describe(\"Connection URL Verification\", () => {\n    beforeEach(() => {\n      jest.clearAllMocks();\n      // Reset the mock transport objects\n      mockSSETransport.url = undefined;\n      mockSSETransport.options = undefined;\n      mockStreamableHTTPTransport.url = undefined;\n      mockStreamableHTTPTransport.options = undefined;\n    });\n\n    test(\"uses server URL directly when connectionType is 'direct'\", async () => {\n      const directProps = {\n        ...defaultProps,\n        connectionType: \"direct\" as const,\n      };\n\n      const { result } = renderHook(() => useConnection(directProps));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      // Verify the transport was created with the direct server URL\n      expect(mockSSETransport.url).toBeDefined();\n      expect(mockSSETransport.url?.toString()).toBe(\"http://localhost:8080/\");\n    });\n\n    test(\"uses proxy server URL when connectionType is 'proxy'\", async () => {\n      const proxyProps = {\n        ...defaultProps,\n        connectionType: \"proxy\" as const,\n      };\n\n      const { result } = renderHook(() => useConnection(proxyProps));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      // Verify the transport was created with a proxy server URL\n      expect(mockSSETransport.url).toBeDefined();\n      expect(mockSSETransport.url?.pathname).toBe(\"/sse\");\n      expect(mockSSETransport.url?.searchParams.get(\"url\")).toBe(\n        \"http://localhost:8080\",\n      );\n      expect(mockSSETransport.url?.searchParams.get(\"transportType\")).toBe(\n        \"sse\",\n      );\n    });\n  });\n\n  describe(\"OAuth Error Handling with Scope Discovery\", () => {\n    beforeEach(() => {\n      jest.clearAllMocks();\n      mockAuth.mockResolvedValue(\"AUTHORIZED\");\n      mockDiscoverScopes.mockResolvedValue(undefined);\n    });\n\n    const setup401Error = () => {\n      const mockErrorEvent = new ErrorEvent(\"error\", {\n        message: \"Mock error event\",\n      });\n      mockClient.connect.mockRejectedValueOnce(\n        new SseError(401, \"Unauthorized\", mockErrorEvent),\n      );\n    };\n\n    const attemptConnection = async (props = defaultProps) => {\n      const { result } = renderHook(() => useConnection(props));\n      await act(async () => {\n        try {\n          await result.current.connect();\n        } catch {\n          // Expected error from auth handling\n        }\n      });\n    };\n\n    const testCases = [\n      [\n        \"discovers and includes scopes in auth call\",\n        {\n          discoveredScope: \"read write admin\",\n          oauthScope: undefined,\n          expectScopeCall: true,\n          expectedAuthScope: \"read write admin\",\n          authResult: \"AUTHORIZED\",\n        },\n      ],\n      [\n        \"handles scope discovery failure gracefully\",\n        {\n          discoveredScope: undefined,\n          oauthScope: undefined,\n          expectScopeCall: true,\n          expectedAuthScope: undefined,\n          authResult: \"AUTHORIZED\",\n        },\n      ],\n      [\n        \"uses manual oauthScope override instead of discovered scopes\",\n        {\n          discoveredScope: \"discovered:scope\",\n          oauthScope: \"manual:scope\",\n          expectScopeCall: false,\n          expectedAuthScope: \"manual:scope\",\n          authResult: \"AUTHORIZED\",\n        },\n      ],\n      [\n        \"triggers scope discovery when oauthScope is whitespace\",\n        {\n          discoveredScope: \"discovered:scope\",\n          oauthScope: \"   \",\n          expectScopeCall: true,\n          expectedAuthScope: \"discovered:scope\",\n          authResult: \"AUTHORIZED\",\n        },\n      ],\n      [\n        \"handles auth failure after scope discovery\",\n        {\n          discoveredScope: \"read write\",\n          oauthScope: undefined,\n          expectScopeCall: true,\n          expectedAuthScope: \"read write\",\n          authResult: \"UNAUTHORIZED\",\n        },\n      ],\n    ] as const;\n\n    test.each(testCases)(\n      \"should %s\",\n      async (\n        _,\n        {\n          discoveredScope,\n          oauthScope,\n          expectScopeCall,\n          expectedAuthScope,\n          authResult = \"AUTHORIZED\",\n        },\n      ) => {\n        mockDiscoverScopes.mockResolvedValue(discoveredScope);\n        mockAuth.mockResolvedValue(authResult as never);\n        setup401Error();\n\n        const props =\n          oauthScope !== undefined\n            ? { ...defaultProps, oauthScope }\n            : defaultProps;\n        await attemptConnection(props);\n\n        if (expectScopeCall) {\n          expect(mockDiscoverScopes).toHaveBeenCalledWith(\n            defaultProps.sseUrl,\n            undefined,\n          );\n        } else {\n          expect(mockDiscoverScopes).not.toHaveBeenCalled();\n        }\n\n        expect(mockAuth).toHaveBeenCalledWith(expect.any(Object), {\n          serverUrl: defaultProps.sseUrl,\n          scope: expectedAuthScope,\n        });\n      },\n    );\n\n    it(\"should handle slow scope discovery gracefully\", async () => {\n      mockDiscoverScopes.mockImplementation(\n        () =>\n          new Promise((resolve) => setTimeout(() => resolve(undefined), 100)),\n      );\n\n      setup401Error();\n      await attemptConnection();\n\n      expect(mockDiscoverScopes).toHaveBeenCalledWith(\n        defaultProps.sseUrl,\n        undefined,\n      );\n      expect(mockAuth).toHaveBeenCalledWith(expect.any(Object), {\n        serverUrl: defaultProps.sseUrl,\n        scope: undefined,\n      });\n    });\n  });\n\n  describe(\"MCP_PROXY_FULL_ADDRESS Configuration\", () => {\n    beforeEach(() => {\n      jest.clearAllMocks();\n      // Reset the mock transport objects\n      mockSSETransport.url = undefined;\n      mockSSETransport.options = undefined;\n      mockStreamableHTTPTransport.url = undefined;\n      mockStreamableHTTPTransport.options = undefined;\n    });\n\n    test(\"sends proxyFullAddress query parameter for stdio transport when configured\", async () => {\n      const propsWithProxyFullAddress = {\n        ...defaultProps,\n        transportType: \"stdio\" as const,\n        command: \"test-command\",\n        args: \"test-args\",\n        env: {},\n        config: {\n          ...DEFAULT_INSPECTOR_CONFIG,\n          MCP_PROXY_FULL_ADDRESS: {\n            ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_FULL_ADDRESS,\n            value: \"https://example.com/inspector/mcp_proxy\",\n          },\n        },\n      };\n\n      const { result } = renderHook(() =>\n        useConnection(propsWithProxyFullAddress),\n      );\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      // Check that the URL contains the proxyFullAddress parameter\n      expect(mockSSETransport.url?.searchParams.get(\"proxyFullAddress\")).toBe(\n        \"https://example.com/inspector/mcp_proxy\",\n      );\n    });\n\n    test(\"sends proxyFullAddress query parameter for sse transport when configured\", async () => {\n      const propsWithProxyFullAddress = {\n        ...defaultProps,\n        transportType: \"sse\" as const,\n        sseUrl: \"http://localhost:8080\",\n        config: {\n          ...DEFAULT_INSPECTOR_CONFIG,\n          MCP_PROXY_FULL_ADDRESS: {\n            ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_FULL_ADDRESS,\n            value: \"https://example.com/inspector/mcp_proxy\",\n          },\n        },\n      };\n\n      const { result } = renderHook(() =>\n        useConnection(propsWithProxyFullAddress),\n      );\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      // Check that the URL contains the proxyFullAddress parameter\n      expect(mockSSETransport.url?.searchParams.get(\"proxyFullAddress\")).toBe(\n        \"https://example.com/inspector/mcp_proxy\",\n      );\n    });\n\n    test(\"does not send proxyFullAddress parameter when MCP_PROXY_FULL_ADDRESS is empty\", async () => {\n      const propsWithEmptyProxy = {\n        ...defaultProps,\n        transportType: \"stdio\" as const,\n        command: \"test-command\",\n        args: \"test-args\",\n        env: {},\n        config: {\n          ...DEFAULT_INSPECTOR_CONFIG,\n          MCP_PROXY_FULL_ADDRESS: {\n            ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_FULL_ADDRESS,\n            value: \"\",\n          },\n        },\n      };\n\n      const { result } = renderHook(() => useConnection(propsWithEmptyProxy));\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      // Check that the URL does not contain the proxyFullAddress parameter\n      expect(\n        mockSSETransport.url?.searchParams.get(\"proxyFullAddress\"),\n      ).toBeNull();\n    });\n\n    test(\"does not send proxyFullAddress parameter for streamable-http transport\", async () => {\n      const propsWithStreamableHttp = {\n        ...defaultProps,\n        transportType: \"streamable-http\" as const,\n        sseUrl: \"http://localhost:8080\",\n        config: {\n          ...DEFAULT_INSPECTOR_CONFIG,\n          MCP_PROXY_FULL_ADDRESS: {\n            ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_FULL_ADDRESS,\n            value: \"https://example.com/inspector/mcp_proxy\",\n          },\n        },\n      };\n\n      const { result } = renderHook(() =>\n        useConnection(propsWithStreamableHttp),\n      );\n\n      await act(async () => {\n        await result.current.connect();\n      });\n\n      // Check that streamable-http transport doesn't get proxyFullAddress parameter\n      expect(\n        mockStreamableHTTPTransport.url?.searchParams.get(\"proxyFullAddress\"),\n      ).toBeNull();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/lib/hooks/useCompletionState.ts",
    "content": "import { useState, useCallback, useEffect, useRef, useMemo } from \"react\";\nimport {\n  ResourceReference,\n  PromptReference,\n} from \"@modelcontextprotocol/sdk/types.js\";\n\ninterface CompletionState {\n  completions: Record<string, string[]>;\n  loading: Record<string, boolean>;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction debounce<T extends (...args: any[]) => PromiseLike<void>>(\n  func: T,\n  wait: number,\n): (...args: Parameters<T>) => void {\n  let timeout: ReturnType<typeof setTimeout>;\n  return (...args: Parameters<T>) => {\n    clearTimeout(timeout);\n    timeout = setTimeout(() => {\n      void func(...args);\n    }, wait);\n  };\n}\n\nexport function useCompletionState(\n  handleCompletion: (\n    ref: ResourceReference | PromptReference,\n    argName: string,\n    value: string,\n    context?: Record<string, string>,\n    signal?: AbortSignal,\n  ) => Promise<string[]>,\n  completionsSupported: boolean = true,\n  debounceMs: number = 300,\n) {\n  const [state, setState] = useState<CompletionState>({\n    completions: {},\n    loading: {},\n  });\n\n  const abortControllerRef = useRef<AbortController | null>(null);\n\n  const cleanup = useCallback(() => {\n    if (abortControllerRef.current) {\n      abortControllerRef.current.abort();\n      abortControllerRef.current = null;\n    }\n  }, []);\n\n  // Cleanup on unmount\n  useEffect(() => {\n    return cleanup;\n  }, [cleanup]);\n\n  const clearCompletions = useCallback(() => {\n    cleanup();\n    setState({\n      completions: {},\n      loading: {},\n    });\n  }, [cleanup]);\n\n  const requestCompletions = useMemo(() => {\n    return debounce(\n      async (\n        ref: ResourceReference | PromptReference,\n        argName: string,\n        value: string,\n        context?: Record<string, string>,\n      ) => {\n        if (!completionsSupported) {\n          return;\n        }\n\n        cleanup();\n\n        const abortController = new AbortController();\n        abortControllerRef.current = abortController;\n\n        setState((prev) => ({\n          ...prev,\n          loading: { ...prev.loading, [argName]: true },\n        }));\n\n        try {\n          if (context !== undefined) {\n            delete context[argName];\n          }\n\n          const values = await handleCompletion(\n            ref,\n            argName,\n            value,\n            context,\n            abortController.signal,\n          );\n\n          if (!abortController.signal.aborted) {\n            setState((prev) => ({\n              ...prev,\n              completions: { ...prev.completions, [argName]: values },\n              loading: { ...prev.loading, [argName]: false },\n            }));\n          }\n        } catch {\n          console.error(\"completion failed\");\n          if (!abortController.signal.aborted) {\n            setState((prev) => ({\n              ...prev,\n              loading: { ...prev.loading, [argName]: false },\n            }));\n          }\n        } finally {\n          if (abortControllerRef.current === abortController) {\n            abortControllerRef.current = null;\n          }\n        }\n      },\n      debounceMs,\n    );\n  }, [handleCompletion, completionsSupported, cleanup, debounceMs]);\n\n  // Clear completions when support status changes\n  useEffect(() => {\n    if (!completionsSupported) {\n      clearCompletions();\n    }\n  }, [completionsSupported, clearCompletions]);\n\n  return {\n    ...state,\n    clearCompletions,\n    requestCompletions,\n    completionsSupported,\n  };\n}\n"
  },
  {
    "path": "client/src/lib/hooks/useConnection.ts",
    "content": "import { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport {\n  SSEClientTransport,\n  SseError,\n  SSEClientTransportOptions,\n} from \"@modelcontextprotocol/sdk/client/sse.js\";\nimport {\n  StreamableHTTPClientTransport,\n  StreamableHTTPClientTransportOptions,\n  StreamableHTTPError,\n} from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport {\n  ClientNotification,\n  ClientRequest,\n  ClientResult,\n  CreateMessageRequestSchema,\n  ListRootsRequestSchema,\n  ResourceUpdatedNotificationSchema,\n  LoggingMessageNotificationSchema,\n  Request,\n  Result,\n  ServerCapabilities,\n  PromptReference,\n  ResourceReference,\n  McpError,\n  CompleteResultSchema,\n  ErrorCode,\n  CancelledNotificationSchema,\n  ResourceListChangedNotificationSchema,\n  ToolListChangedNotificationSchema,\n  PromptListChangedNotificationSchema,\n  Progress,\n  LoggingLevel,\n  ElicitRequestSchema,\n  Implementation,\n  Task,\n  CreateTaskResultSchema,\n  GetTaskRequestSchema,\n  GetTaskPayloadRequestSchema,\n  ListTasksRequestSchema,\n  CancelTaskRequestSchema,\n  ListTasksResultSchema,\n  CancelTaskResultSchema,\n  TaskStatusNotificationSchema,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport type {\n  AnySchema,\n  SchemaOutput,\n} from \"@modelcontextprotocol/sdk/server/zod-compat.js\";\nimport { RequestOptions } from \"@modelcontextprotocol/sdk/shared/protocol.js\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useToast } from \"@/lib/hooks/useToast\";\nimport { ConnectionStatus, CLIENT_IDENTITY } from \"../constants\";\nimport { Notification } from \"../notificationTypes\";\nimport {\n  auth,\n  discoverOAuthProtectedResourceMetadata,\n} from \"@modelcontextprotocol/sdk/client/auth.js\";\nimport {\n  clearClientInformationFromSessionStorage,\n  InspectorOAuthClientProvider,\n  saveClientInformationToSessionStorage,\n  saveScopeToSessionStorage,\n  clearScopeFromSessionStorage,\n  discoverScopes,\n} from \"../auth\";\nimport {\n  getMCPProxyAddress,\n  getMCPTaskTtl,\n  getMCPServerRequestMaxTotalTimeout,\n  resetRequestTimeoutOnProgress,\n  getMCPProxyAuthToken,\n} from \"@/utils/configUtils\";\nimport { getMCPServerRequestTimeout } from \"@/utils/configUtils\";\nimport { InspectorConfig } from \"../configurationTypes\";\nimport { Transport } from \"@modelcontextprotocol/sdk/shared/transport.js\";\nimport { CustomHeaders } from \"../types/customHeaders\";\nimport { resolveRefsInMessage } from \"@/utils/schemaUtils\";\n\ninterface UseConnectionOptions {\n  transportType: \"stdio\" | \"sse\" | \"streamable-http\";\n  command: string;\n  args: string;\n  sseUrl: string;\n  env: Record<string, string>;\n  // Custom headers support\n  customHeaders?: CustomHeaders;\n  oauthClientId?: string;\n  oauthClientSecret?: string;\n  oauthScope?: string;\n  config: InspectorConfig;\n  connectionType?: \"direct\" | \"proxy\";\n  onNotification?: (notification: Notification) => void;\n  onStdErrNotification?: (notification: Notification) => void;\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  onPendingRequest?: (request: any, resolve: any, reject: any) => void;\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  onElicitationRequest?: (request: any, resolve: any) => void;\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  getRoots?: () => any[];\n  defaultLoggingLevel?: LoggingLevel;\n  serverImplementation?: Implementation;\n  metadata?: Record<string, string>;\n}\n\nexport function useConnection({\n  transportType,\n  command,\n  args,\n  sseUrl,\n  env,\n  customHeaders,\n  oauthClientId,\n  oauthClientSecret,\n  oauthScope,\n  config,\n  connectionType = \"proxy\",\n  onNotification,\n  onPendingRequest,\n  onElicitationRequest,\n  getRoots,\n  defaultLoggingLevel,\n  metadata = {},\n}: UseConnectionOptions) {\n  const [connectionStatus, setConnectionStatus] =\n    useState<ConnectionStatus>(\"disconnected\");\n  const { toast } = useToast();\n  const [serverCapabilities, setServerCapabilities] =\n    useState<ServerCapabilities | null>(null);\n  const [mcpClient, setMcpClient] = useState<Client | null>(null);\n  const [clientTransport, setClientTransport] = useState<Transport | null>(\n    null,\n  );\n  const [requestHistory, setRequestHistory] = useState<\n    { request: string; response?: string }[]\n  >([]);\n  const [completionsSupported, setCompletionsSupported] = useState(false);\n  const [mcpSessionId, setMcpSessionId] = useState<string | null>(null);\n  const [mcpProtocolVersion, setMcpProtocolVersion] = useState<string | null>(\n    null,\n  );\n  const [serverImplementation, setServerImplementation] =\n    useState<Implementation | null>(null);\n\n  type ReceiverTaskRecord = {\n    task: Task;\n    payloadPromise: Promise<ClientResult>;\n    resolvePayload: (payload: ClientResult) => void;\n    rejectPayload: (reason?: unknown) => void;\n    cleanupTimeoutId?: ReturnType<typeof setTimeout>;\n  };\n\n  // Tasks created locally in response to *incoming* task-augmented requests\n  // (e.g. `sampling/createMessage` and `elicitation/create` with `params.task`).\n  const receiverTasksRef = useRef<Map<string, ReceiverTaskRecord>>(new Map());\n\n  useEffect(() => {\n    if (!oauthClientId) {\n      clearClientInformationFromSessionStorage({\n        serverUrl: sseUrl,\n        isPreregistered: true,\n      });\n      return;\n    }\n\n    const clientInformation: { client_id: string; client_secret?: string } = {\n      client_id: oauthClientId,\n    };\n\n    if (oauthClientSecret) {\n      clientInformation.client_secret = oauthClientSecret;\n    }\n\n    saveClientInformationToSessionStorage({\n      serverUrl: sseUrl,\n      clientInformation,\n      isPreregistered: true,\n    });\n  }, [oauthClientId, oauthClientSecret, sseUrl]);\n\n  useEffect(() => {\n    if (!oauthScope) {\n      clearScopeFromSessionStorage(sseUrl);\n      return;\n    }\n\n    saveScopeToSessionStorage(sseUrl, oauthScope);\n  }, [oauthScope, sseUrl]);\n\n  const pushHistory = (request: object, response?: object) => {\n    setRequestHistory((prev) => [\n      ...prev,\n      {\n        request: JSON.stringify(request),\n        response: response !== undefined ? JSON.stringify(response) : undefined,\n      },\n    ]);\n  };\n\n  const makeRequest = async <T extends AnySchema>(\n    request: ClientRequest,\n    schema: T,\n    options?: RequestOptions & { suppressToast?: boolean },\n  ): Promise<SchemaOutput<T>> => {\n    if (!mcpClient) {\n      throw new Error(\"MCP client not connected\");\n    }\n    try {\n      const abortController = new AbortController();\n\n      // Add metadata to the request if available, but skip for tool calls\n      // as they handle metadata merging separately\n      const shouldAddGeneralMetadata =\n        request.method !== \"tools/call\" && Object.keys(metadata).length > 0;\n      const requestWithMetadata = shouldAddGeneralMetadata\n        ? {\n            ...request,\n            params: {\n              ...request.params,\n              _meta: metadata,\n            },\n          }\n        : request;\n\n      // prepare MCP Client request options\n      const mcpRequestOptions: RequestOptions = {\n        signal: options?.signal ?? abortController.signal,\n        resetTimeoutOnProgress:\n          options?.resetTimeoutOnProgress ??\n          resetRequestTimeoutOnProgress(config),\n        timeout: options?.timeout ?? getMCPServerRequestTimeout(config),\n        maxTotalTimeout:\n          options?.maxTotalTimeout ??\n          getMCPServerRequestMaxTotalTimeout(config),\n      };\n\n      // If progress notifications are enabled, add an onprogress hook to the MCP Client request options\n      // This is required by SDK to reset the timeout on progress notifications\n      if (mcpRequestOptions.resetTimeoutOnProgress) {\n        mcpRequestOptions.onprogress = (params: Progress) => {\n          // Add progress notification to `Server Notification` window in the UI\n          if (onNotification) {\n            onNotification({\n              method: \"notifications/progress\",\n              params,\n            });\n          }\n        };\n      }\n\n      let response;\n      try {\n        response = await mcpClient.request(\n          requestWithMetadata,\n          schema,\n          mcpRequestOptions,\n        );\n\n        pushHistory(requestWithMetadata, response);\n      } catch (error) {\n        const errorMessage =\n          error instanceof Error ? error.message : String(error);\n        pushHistory(requestWithMetadata, { error: errorMessage });\n        throw error;\n      }\n\n      return response;\n    } catch (e: unknown) {\n      if (!options?.suppressToast) {\n        const errorString = (e as Error).message ?? String(e);\n        toast({\n          title: \"Error\",\n          description: errorString,\n          variant: \"destructive\",\n        });\n      }\n      throw e;\n    }\n  };\n\n  const handleCompletion = async (\n    ref: ResourceReference | PromptReference,\n    argName: string,\n    value: string,\n    context?: Record<string, string>,\n    signal?: AbortSignal,\n  ): Promise<string[]> => {\n    if (!mcpClient || !completionsSupported) {\n      return [];\n    }\n\n    const request: ClientRequest = {\n      method: \"completion/complete\",\n      params: {\n        argument: {\n          name: argName,\n          value,\n        },\n        ref,\n      },\n    };\n\n    if (context) {\n      request[\"params\"][\"context\"] = {\n        arguments: context,\n      };\n    }\n\n    try {\n      const response = await makeRequest(request, CompleteResultSchema, {\n        signal,\n        suppressToast: true,\n      });\n      return response?.completion.values || [];\n    } catch (e: unknown) {\n      // Disable completions silently if the server doesn't support them.\n      // See https://github.com/modelcontextprotocol/specification/discussions/122\n      if (e instanceof McpError && e.code === ErrorCode.MethodNotFound) {\n        setCompletionsSupported(false);\n        return [];\n      }\n\n      // Unexpected errors - show toast and rethrow\n      toast({\n        title: \"Error\",\n        description: e instanceof Error ? e.message : String(e),\n        variant: \"destructive\",\n      });\n      throw e;\n    }\n  };\n\n  const sendNotification = async (notification: ClientNotification) => {\n    if (!mcpClient) {\n      const error = new Error(\"MCP client not connected\");\n      toast({\n        title: \"Error\",\n        description: error.message,\n        variant: \"destructive\",\n      });\n      throw error;\n    }\n\n    try {\n      await mcpClient.notification(notification);\n      // Log successful notifications\n      pushHistory(notification);\n    } catch (e: unknown) {\n      if (e instanceof McpError) {\n        // Log MCP protocol errors\n        pushHistory(notification, { error: e.message });\n      }\n      toast({\n        title: \"Error\",\n        description: e instanceof Error ? e.message : String(e),\n        variant: \"destructive\",\n      });\n      throw e;\n    }\n  };\n\n  const checkProxyHealth = async () => {\n    try {\n      const proxyHealthUrl = new URL(`${getMCPProxyAddress(config)}/health`);\n      const { token: proxyAuthToken, header: proxyAuthTokenHeader } =\n        getMCPProxyAuthToken(config);\n      const headers: HeadersInit = {};\n      if (proxyAuthToken) {\n        headers[proxyAuthTokenHeader] = `Bearer ${proxyAuthToken}`;\n      }\n      const proxyHealthResponse = await fetch(proxyHealthUrl, { headers });\n      const proxyHealth = await proxyHealthResponse.json();\n      if (proxyHealth?.status !== \"ok\") {\n        throw new Error(\"MCP Proxy Server is not healthy\");\n      }\n    } catch (e) {\n      console.error(\"Couldn't connect to MCP Proxy Server\", e);\n      throw e;\n    }\n  };\n\n  const is401Error = (error: unknown): boolean => {\n    return (\n      (error instanceof SseError && error.code === 401) ||\n      (error instanceof StreamableHTTPError && error.code === 401) ||\n      (error instanceof Error && error.message.includes(\"401\")) ||\n      (error instanceof Error && error.message.includes(\"Unauthorized\")) ||\n      (error instanceof Error &&\n        error.message.includes(\"Missing Authorization header\"))\n    );\n  };\n\n  const isProxyAuthError = (error: unknown): boolean => {\n    return (\n      error instanceof Error &&\n      error.message.includes(\"Authentication required. Use the session token\")\n    );\n  };\n\n  const handleAuthError = async (error: unknown) => {\n    if (is401Error(error)) {\n      let scope = oauthScope?.trim();\n      if (!scope) {\n        // Only discover resource metadata when we need to discover scopes\n        let resourceMetadata;\n        try {\n          resourceMetadata = await discoverOAuthProtectedResourceMetadata(\n            new URL(\"/\", sseUrl),\n          );\n        } catch {\n          // Resource metadata is optional, continue without it\n        }\n        scope = await discoverScopes(sseUrl, resourceMetadata);\n      }\n\n      saveScopeToSessionStorage(sseUrl, scope);\n      const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);\n\n      try {\n        const result = await auth(serverAuthProvider, {\n          serverUrl: sseUrl,\n          scope,\n        });\n        return result === \"AUTHORIZED\";\n      } catch (authError) {\n        // Show user-friendly error message for OAuth failures\n        toast({\n          title: \"OAuth Authentication Failed\",\n          description:\n            authError instanceof Error ? authError.message : String(authError),\n          variant: \"destructive\",\n        });\n        return false;\n      }\n    }\n\n    return false;\n  };\n\n  const captureResponseHeaders = (response: Response): void => {\n    const sessionId = response.headers.get(\"mcp-session-id\");\n    const protocolVersion = response.headers.get(\"mcp-protocol-version\");\n    if (sessionId && sessionId !== mcpSessionId) {\n      setMcpSessionId(sessionId);\n    }\n    if (protocolVersion && protocolVersion !== mcpProtocolVersion) {\n      setMcpProtocolVersion(protocolVersion);\n    }\n  };\n\n  const connect = async (_e?: unknown, retryCount: number = 0) => {\n    const clientCapabilities = {\n      capabilities: {\n        sampling: {},\n        elicitation: {},\n        roots: {\n          listChanged: true,\n        },\n        tasks: {\n          list: {},\n          cancel: {},\n          ...(onPendingRequest || onElicitationRequest\n            ? {\n                requests: {\n                  ...(onPendingRequest\n                    ? { sampling: { createMessage: {} } }\n                    : undefined),\n                  ...(onElicitationRequest\n                    ? { elicitation: { create: {} } }\n                    : undefined),\n                },\n              }\n            : undefined),\n        },\n      },\n    };\n\n    const client = new Client<Request, Notification, Result>(\n      CLIENT_IDENTITY,\n      clientCapabilities,\n    );\n\n    // Only check proxy health for proxy connections\n    if (connectionType === \"proxy\") {\n      try {\n        await checkProxyHealth();\n      } catch {\n        setConnectionStatus(\"error-connecting-to-proxy\");\n        return;\n      }\n    }\n\n    let lastRequest = \"\";\n    try {\n      // Inject auth manually instead of using SSEClientTransport, because we're\n      // proxying through the inspector server first.\n      const headers: HeadersInit = {};\n\n      // Create an auth provider with the current server URL\n      const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);\n\n      // Use custom headers (migration is handled in App.tsx)\n      let finalHeaders: CustomHeaders = customHeaders || [];\n\n      const isEmptyAuthHeader = (header: CustomHeaders[number]) =>\n        header.name.trim().toLowerCase() === \"authorization\" &&\n        header.value.trim().toLowerCase() === \"bearer\";\n\n      // Check for empty Authorization headers and show validation error\n      const hasEmptyAuthHeader = finalHeaders.some(\n        (header) => header.enabled && isEmptyAuthHeader(header),\n      );\n\n      if (hasEmptyAuthHeader) {\n        toast({\n          title: \"Invalid Authorization Header\",\n          description:\n            \"Authorization header is enabled but empty. Please add a token or disable the header.\",\n          variant: \"destructive\",\n        });\n      }\n\n      const needsOAuthToken = !finalHeaders.some(\n        (header) =>\n          header.enabled &&\n          header.name.trim().toLowerCase() === \"authorization\",\n      );\n\n      if (needsOAuthToken) {\n        const oauthToken = (await serverAuthProvider.tokens())?.access_token;\n        if (oauthToken) {\n          // Add the OAuth token\n          finalHeaders = [\n            // Remove any existing Authorization headers with empty tokens\n            ...finalHeaders.filter((header) => !isEmptyAuthHeader(header)),\n            {\n              name: \"Authorization\",\n              value: `Bearer ${oauthToken}`,\n              enabled: true,\n            },\n          ];\n        }\n      }\n\n      // Process all enabled custom headers\n      const customHeaderNames: string[] = [];\n      finalHeaders.forEach((header) => {\n        if (header.enabled && header.name.trim() && header.value.trim()) {\n          const headerName = header.name.trim();\n          const headerValue = header.value.trim();\n\n          headers[headerName] = headerValue;\n\n          // Track custom header names for server processing\n          if (headerName.toLowerCase() !== \"authorization\") {\n            customHeaderNames.push(headerName);\n          }\n        }\n      });\n\n      // Add custom header names as a special request header for server processing\n      if (customHeaderNames.length > 0) {\n        headers[\"x-custom-auth-headers\"] = JSON.stringify(customHeaderNames);\n      }\n\n      // Create appropriate transport\n      let transportOptions:\n        | StreamableHTTPClientTransportOptions\n        | SSEClientTransportOptions;\n\n      let serverUrl: URL;\n\n      // Determine connection URL based on the connection type\n      if (connectionType === \"direct\" && transportType !== \"stdio\") {\n        // Direct connection - use the provided URL directly (not available for STDIO)\n        serverUrl = new URL(sseUrl);\n\n        const requestHeaders = { ...headers };\n        if (mcpSessionId) {\n          requestHeaders[\"mcp-session-id\"] = mcpSessionId;\n        }\n        switch (transportType) {\n          case \"sse\":\n            requestHeaders[\"Accept\"] = \"text/event-stream\";\n            requestHeaders[\"content-type\"] = \"application/json\";\n            transportOptions = {\n              authProvider: serverAuthProvider,\n              fetch: async (\n                url: string | URL | globalThis.Request,\n                init?: RequestInit,\n              ) => {\n                const response = await fetch(url, {\n                  ...init,\n                  headers: requestHeaders,\n                });\n\n                // Capture protocol-related headers from response\n                captureResponseHeaders(response);\n                return response;\n              },\n              requestInit: {\n                headers: requestHeaders,\n              },\n            };\n            break;\n\n          case \"streamable-http\":\n            transportOptions = {\n              authProvider: serverAuthProvider,\n              fetch: async (\n                url: string | URL | globalThis.Request,\n                init?: RequestInit,\n              ) => {\n                requestHeaders[\"Accept\"] =\n                  \"text/event-stream, application/json\";\n                requestHeaders[\"Content-Type\"] = \"application/json\";\n                const response = await fetch(url, {\n                  headers: requestHeaders,\n                  ...init,\n                });\n\n                // Capture protocol-related headers from response\n                captureResponseHeaders(response);\n\n                return response;\n              },\n              requestInit: {\n                headers: requestHeaders,\n              },\n              // TODO these should be configurable...\n              reconnectionOptions: {\n                maxReconnectionDelay: 30000,\n                initialReconnectionDelay: 1000,\n                reconnectionDelayGrowFactor: 1.5,\n                maxRetries: 2,\n              },\n            };\n            break;\n        }\n      } else {\n        // Proxy connection (default behavior)\n        // Add proxy authentication headers for proxy connections only\n        const { token: proxyAuthToken, header: proxyAuthTokenHeader } =\n          getMCPProxyAuthToken(config);\n        const proxyHeaders: HeadersInit = {};\n        if (proxyAuthToken) {\n          proxyHeaders[proxyAuthTokenHeader] = `Bearer ${proxyAuthToken}`;\n        }\n\n        let mcpProxyServerUrl;\n        switch (transportType) {\n          case \"stdio\": {\n            mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/stdio`);\n            mcpProxyServerUrl.searchParams.append(\"command\", command);\n            mcpProxyServerUrl.searchParams.append(\"args\", args);\n            mcpProxyServerUrl.searchParams.append(\"env\", JSON.stringify(env));\n\n            const proxyFullAddress = config.MCP_PROXY_FULL_ADDRESS\n              .value as string;\n            if (proxyFullAddress) {\n              mcpProxyServerUrl.searchParams.append(\n                \"proxyFullAddress\",\n                proxyFullAddress,\n              );\n            }\n            transportOptions = {\n              authProvider: serverAuthProvider,\n              eventSourceInit: {\n                fetch: (\n                  url: string | URL | globalThis.Request,\n                  init?: RequestInit,\n                ) =>\n                  fetch(url, {\n                    ...init,\n                    headers: { ...headers, ...proxyHeaders },\n                  }),\n              },\n              requestInit: {\n                headers: { ...headers, ...proxyHeaders },\n              },\n            };\n            break;\n          }\n\n          case \"sse\": {\n            mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/sse`);\n            mcpProxyServerUrl.searchParams.append(\"url\", sseUrl);\n\n            const proxyFullAddressSSE = config.MCP_PROXY_FULL_ADDRESS\n              .value as string;\n            if (proxyFullAddressSSE) {\n              mcpProxyServerUrl.searchParams.append(\n                \"proxyFullAddress\",\n                proxyFullAddressSSE,\n              );\n            }\n            transportOptions = {\n              authProvider: serverAuthProvider,\n              eventSourceInit: {\n                fetch: (\n                  url: string | URL | globalThis.Request,\n                  init?: RequestInit,\n                ) =>\n                  fetch(url, {\n                    ...init,\n                    headers: { ...headers, ...proxyHeaders },\n                  }),\n              },\n              requestInit: {\n                headers: { ...headers, ...proxyHeaders },\n              },\n            };\n            break;\n          }\n\n          case \"streamable-http\":\n            mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/mcp`);\n            mcpProxyServerUrl.searchParams.append(\"url\", sseUrl);\n            transportOptions = {\n              authProvider: serverAuthProvider,\n              eventSourceInit: {\n                fetch: (\n                  url: string | URL | globalThis.Request,\n                  init?: RequestInit,\n                ) =>\n                  fetch(url, {\n                    ...init,\n                    headers: { ...headers, ...proxyHeaders },\n                  }),\n              },\n              requestInit: {\n                headers: { ...headers, ...proxyHeaders },\n              },\n              // TODO these should be configurable...\n              reconnectionOptions: {\n                maxReconnectionDelay: 30000,\n                initialReconnectionDelay: 1000,\n                reconnectionDelayGrowFactor: 1.5,\n                maxRetries: 2,\n              },\n            };\n            break;\n        }\n        serverUrl = mcpProxyServerUrl as URL;\n        serverUrl.searchParams.append(\"transportType\", transportType);\n      }\n\n      if (onNotification) {\n        [\n          CancelledNotificationSchema,\n          LoggingMessageNotificationSchema,\n          ResourceUpdatedNotificationSchema,\n          ResourceListChangedNotificationSchema,\n          ToolListChangedNotificationSchema,\n          PromptListChangedNotificationSchema,\n          TaskStatusNotificationSchema,\n        ].forEach((notificationSchema) => {\n          client.setNotificationHandler(notificationSchema, onNotification);\n        });\n\n        client.fallbackNotificationHandler = (\n          notification: Notification,\n        ): Promise<void> => {\n          onNotification(notification);\n          return Promise.resolve();\n        };\n      }\n\n      let capabilities;\n      try {\n        const transport =\n          transportType === \"streamable-http\"\n            ? new StreamableHTTPClientTransport(serverUrl, {\n                sessionId: undefined,\n                ...transportOptions,\n              })\n            : new SSEClientTransport(serverUrl, transportOptions);\n\n        await client.connect(transport as Transport);\n\n        const protocolOnMessage = transport.onmessage;\n        if (protocolOnMessage) {\n          transport.onmessage = (message) => {\n            const resolvedMessage = resolveRefsInMessage(message);\n            protocolOnMessage(resolvedMessage);\n          };\n        }\n\n        setClientTransport(transport);\n\n        capabilities = client.getServerCapabilities();\n        const serverInfo = client.getServerVersion();\n        setServerImplementation(serverInfo || null);\n        const initializeRequest = {\n          method: \"initialize\",\n        };\n        pushHistory(initializeRequest, {\n          capabilities,\n          serverInfo: client.getServerVersion(),\n          instructions: client.getInstructions(),\n        });\n      } catch (error) {\n        console.error(\n          connectionType === \"direct\"\n            ? `Failed to connect directly to MCP Server at: ${serverUrl}:`\n            : `Failed to connect to MCP Server via the MCP Inspector Proxy: ${serverUrl}:`,\n          error,\n        );\n\n        // Check if it's a proxy auth error\n        if (isProxyAuthError(error)) {\n          toast({\n            title: \"Proxy Authentication Required\",\n            description:\n              \"Please enter the session token from the proxy server console in the Configuration settings.\",\n            variant: \"destructive\",\n          });\n          setConnectionStatus(\"error\");\n          return;\n        }\n\n        const shouldRetry = await handleAuthError(error);\n        if (shouldRetry) {\n          return connect(undefined, retryCount + 1);\n        }\n        if (is401Error(error)) {\n          // Don't set error state if we're about to redirect for auth\n\n          return;\n        }\n        throw error;\n      }\n      setServerCapabilities(capabilities ?? null);\n      setCompletionsSupported(capabilities?.completions !== undefined);\n\n      const nowIso = () => new Date().toISOString();\n\n      const makeTaskId = () => {\n        // Prefer UUID when available; otherwise fall back to a reasonably unique id.\n        const cryptoAny = globalThis.crypto as unknown as\n          | { randomUUID?: () => string }\n          | undefined;\n        return (\n          cryptoAny?.randomUUID?.() ??\n          `task_${Date.now()}_${Math.random().toString(16).slice(2)}`\n        );\n      };\n\n      const emitTaskStatus = async (task: Task) => {\n        // Best-effort; task status notifications are optional.\n        try {\n          const notification: ClientNotification = {\n            method: \"notifications/tasks/status\",\n            params: task,\n          } as unknown as ClientNotification;\n          await client.notification(notification);\n          pushHistory(notification);\n        } catch (e) {\n          console.warn(\"Failed to send notifications/tasks/status\", e);\n        }\n      };\n\n      const upsertReceiverTask = async (task: Task) => {\n        // Update task record and emit status notification.\n        const record = receiverTasksRef.current.get(task.taskId);\n        if (record) {\n          receiverTasksRef.current.set(task.taskId, { ...record, task });\n        }\n        await emitTaskStatus(task);\n      };\n\n      const createReceiverTask = (opts: {\n        ttl?: number;\n        initialStatus: Task[\"status\"];\n        statusMessage?: string;\n        pollInterval?: number;\n      }): ReceiverTaskRecord => {\n        const taskId = makeTaskId();\n        const createdAt = nowIso();\n        const ttl = opts.ttl ?? getMCPTaskTtl(config);\n\n        let resolvePayload: (payload: ClientResult) => void = () => undefined;\n        let rejectPayload: (reason?: unknown) => void = () => undefined;\n        const payloadPromise = new Promise<ClientResult>((resolve, reject) => {\n          resolvePayload = resolve;\n          rejectPayload = reject;\n        });\n\n        const task: Task = {\n          taskId,\n          status: opts.initialStatus,\n          ttl,\n          createdAt,\n          lastUpdatedAt: createdAt,\n          ...(opts.pollInterval !== undefined\n            ? { pollInterval: opts.pollInterval }\n            : undefined),\n          ...(opts.statusMessage ? { statusMessage: opts.statusMessage } : {}),\n        };\n\n        const record: ReceiverTaskRecord = {\n          task,\n          payloadPromise,\n          resolvePayload,\n          rejectPayload,\n        };\n\n        // Cleanup after TTL (best-effort).\n        if (ttl !== null && ttl > 0) {\n          record.cleanupTimeoutId = setTimeout(() => {\n            receiverTasksRef.current.delete(taskId);\n          }, ttl);\n        }\n\n        receiverTasksRef.current.set(taskId, record);\n        void emitTaskStatus(task);\n        return record;\n      };\n\n      // Server -> client Tasks handlers (receiver side)\n      client.setRequestHandler(ListTasksRequestSchema, async () => {\n        return {\n          tasks: Array.from(receiverTasksRef.current.values()).map(\n            (r) => r.task,\n          ),\n        };\n      });\n\n      client.setRequestHandler(GetTaskRequestSchema, async (request) => {\n        const record = receiverTasksRef.current.get(request.params.taskId);\n        if (!record) {\n          throw new McpError(\n            ErrorCode.InvalidParams,\n            `Unknown taskId: ${request.params.taskId}`,\n          );\n        }\n        return record.task;\n      });\n\n      client.setRequestHandler(GetTaskPayloadRequestSchema, async (request) => {\n        const record = receiverTasksRef.current.get(request.params.taskId);\n        if (!record) {\n          throw new McpError(\n            ErrorCode.InvalidParams,\n            `Unknown taskId: ${request.params.taskId}`,\n          );\n        }\n\n        // Block until the task payload is ready.\n        return await record.payloadPromise;\n      });\n\n      client.setRequestHandler(CancelTaskRequestSchema, async (request) => {\n        const record = receiverTasksRef.current.get(request.params.taskId);\n        if (!record) {\n          throw new McpError(\n            ErrorCode.InvalidParams,\n            `Unknown taskId: ${request.params.taskId}`,\n          );\n        }\n\n        const terminalStatuses: Task[\"status\"][] = [\n          \"completed\",\n          \"failed\",\n          \"cancelled\",\n        ];\n\n        if (!terminalStatuses.includes(record.task.status)) {\n          const updated: Task = {\n            ...record.task,\n            status: \"cancelled\",\n            lastUpdatedAt: nowIso(),\n            statusMessage: \"Cancelled\",\n          };\n          receiverTasksRef.current.set(request.params.taskId, {\n            ...record,\n            task: updated,\n          });\n\n          // Unblock any pending `tasks/result`.\n          record.rejectPayload(\n            new McpError(ErrorCode.InternalError, \"Task was cancelled\"),\n          );\n\n          await emitTaskStatus(updated);\n        }\n\n        return receiverTasksRef.current.get(request.params.taskId)!.task;\n      });\n\n      if (onPendingRequest) {\n        client.setRequestHandler(CreateMessageRequestSchema, (request) => {\n          const taskSpec = (request as { params?: { task?: { ttl?: number } } })\n            .params?.task;\n\n          if (!taskSpec) {\n            return new Promise((resolve, reject) => {\n              onPendingRequest(request, resolve, reject);\n            });\n          }\n\n          // Task-augmented sampling request: return a task immediately and\n          // allow the server to poll via `tasks/get` and `tasks/result`.\n          const record = createReceiverTask({\n            ttl: taskSpec.ttl,\n            initialStatus: \"input_required\",\n            statusMessage: \"Awaiting user input\",\n          });\n\n          // Background runner to complete and resolve this specific task record.\n          void (async () => {\n            try {\n              const payload = await new Promise((resolve, reject) => {\n                onPendingRequest(request, resolve, reject);\n              });\n              record.resolvePayload(payload as ClientResult);\n              const updated: Task = {\n                ...record.task,\n                status: \"completed\",\n                lastUpdatedAt: nowIso(),\n              };\n              receiverTasksRef.current.set(record.task.taskId, {\n                ...record,\n                task: updated,\n              });\n              await upsertReceiverTask(updated);\n            } catch (e) {\n              record.rejectPayload(e);\n              const updated: Task = {\n                ...record.task,\n                status: \"failed\",\n                lastUpdatedAt: nowIso(),\n                statusMessage: e instanceof Error ? e.message : \"Task failed\",\n              };\n              receiverTasksRef.current.set(record.task.taskId, {\n                ...record,\n                task: updated,\n              });\n              await upsertReceiverTask(updated);\n            }\n          })();\n\n          const createTaskResult: SchemaOutput<typeof CreateTaskResultSchema> =\n            {\n              task: record.task,\n            };\n          return createTaskResult;\n        });\n      }\n\n      if (getRoots) {\n        client.setRequestHandler(ListRootsRequestSchema, async () => {\n          return { roots: getRoots() };\n        });\n      }\n\n      if (onElicitationRequest) {\n        client.setRequestHandler(ElicitRequestSchema, (request) => {\n          const taskSpec = (request as { params?: { task?: { ttl?: number } } })\n            .params?.task;\n\n          if (!taskSpec) {\n            return new Promise((resolve) => {\n              onElicitationRequest(request, resolve);\n            });\n          }\n\n          const record = createReceiverTask({\n            ttl: taskSpec.ttl,\n            initialStatus: \"input_required\",\n            statusMessage: \"Awaiting user input\",\n          });\n\n          // Run elicitation flow and resolve the task payload.\n          void (async () => {\n            try {\n              const payload = await new Promise((resolve) => {\n                onElicitationRequest(request, resolve);\n              });\n              record.resolvePayload(payload as ClientResult);\n              const updated: Task = {\n                ...record.task,\n                status: \"completed\",\n                lastUpdatedAt: nowIso(),\n              };\n              receiverTasksRef.current.set(record.task.taskId, {\n                ...record,\n                task: updated,\n              });\n              await upsertReceiverTask(updated);\n            } catch (e) {\n              record.rejectPayload(e);\n              const updated: Task = {\n                ...record.task,\n                status: \"failed\",\n                lastUpdatedAt: nowIso(),\n                statusMessage: e instanceof Error ? e.message : \"Task failed\",\n              };\n              receiverTasksRef.current.set(record.task.taskId, {\n                ...record,\n                task: updated,\n              });\n              await upsertReceiverTask(updated);\n            }\n          })();\n\n          const createTaskResult: SchemaOutput<typeof CreateTaskResultSchema> =\n            {\n              task: record.task,\n            };\n          return createTaskResult;\n        });\n      }\n\n      if (capabilities?.logging && defaultLoggingLevel) {\n        lastRequest = \"logging/setLevel\";\n        await client.setLoggingLevel(defaultLoggingLevel);\n        pushHistory(\n          {\n            method: \"logging/setLevel\",\n            params: {\n              level: defaultLoggingLevel,\n            },\n          },\n          {},\n        );\n        lastRequest = \"\";\n      }\n\n      setMcpClient(client);\n      setConnectionStatus(\"connected\");\n    } catch (e) {\n      if (\n        lastRequest === \"logging/setLevel\" &&\n        e instanceof McpError &&\n        e.code === ErrorCode.MethodNotFound\n      ) {\n        toast({\n          title: \"Error\",\n          description: `Server declares logging capability but doesn't implement method: \"${lastRequest}\"`,\n          variant: \"destructive\",\n        });\n      } else {\n        toast({\n          title: \"Connection error\",\n          description: `Connection failed: \"${e}\"`,\n          variant: \"destructive\",\n        });\n      }\n      console.error(e);\n      setConnectionStatus(\"error\");\n    }\n  };\n\n  const cancelTask = async (taskId: string) => {\n    return makeRequest(\n      {\n        method: \"tasks/cancel\",\n        params: { taskId },\n      },\n      CancelTaskResultSchema,\n    );\n  };\n\n  const listTasks = async (cursor?: string) => {\n    return makeRequest(\n      {\n        method: \"tasks/list\",\n        params: { cursor },\n      },\n      ListTasksResultSchema,\n    );\n  };\n\n  const disconnect = async () => {\n    // Clear any receiver-side tasks + cleanup timers\n    receiverTasksRef.current.forEach((record) => {\n      if (record.cleanupTimeoutId) {\n        clearTimeout(record.cleanupTimeoutId);\n      }\n    });\n    receiverTasksRef.current.clear();\n\n    if (transportType === \"streamable-http\")\n      await (\n        clientTransport as StreamableHTTPClientTransport\n      ).terminateSession();\n    await mcpClient?.close();\n    const authProvider = new InspectorOAuthClientProvider(sseUrl);\n    authProvider.clear();\n    setMcpClient(null);\n    setClientTransport(null);\n    setConnectionStatus(\"disconnected\");\n    setCompletionsSupported(false);\n    setServerCapabilities(null);\n    setMcpSessionId(null);\n    setMcpProtocolVersion(null);\n  };\n\n  const clearRequestHistory = () => {\n    setRequestHistory([]);\n    setServerImplementation(null);\n  };\n\n  return {\n    connectionStatus,\n    serverCapabilities,\n    serverImplementation,\n    mcpClient,\n    requestHistory,\n    clearRequestHistory,\n    makeRequest,\n    cancelTask,\n    listTasks,\n    sendNotification,\n    handleCompletion,\n    completionsSupported,\n    connect,\n    disconnect,\n  };\n}\n"
  },
  {
    "path": "client/src/lib/hooks/useCopy.ts",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\ntype UseCopyProps = {\n  timeout?: number;\n};\n\nfunction useCopy({ timeout = 500 }: UseCopyProps = {}) {\n  const [copied, setCopied] = useState(false);\n\n  useEffect(() => {\n    let timeoutId: NodeJS.Timeout;\n    if (copied) {\n      timeoutId = setTimeout(() => {\n        setCopied(false);\n      }, timeout);\n    }\n    return () => {\n      if (timeoutId) {\n        clearTimeout(timeoutId);\n      }\n    };\n  }, [copied, timeout]);\n\n  return { copied, setCopied };\n}\n\nexport default useCopy;\n"
  },
  {
    "path": "client/src/lib/hooks/useDraggablePane.ts",
    "content": "import { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport function useDraggablePane(initialHeight: number) {\n  const [height, setHeight] = useState(initialHeight);\n  const [isDragging, setIsDragging] = useState(false);\n  const dragStartY = useRef<number>(0);\n  const dragStartHeight = useRef<number>(0);\n\n  const handleDragStart = useCallback(\n    (e: React.MouseEvent) => {\n      setIsDragging(true);\n      dragStartY.current = e.clientY;\n      dragStartHeight.current = height;\n      document.body.style.userSelect = \"none\";\n    },\n    [height],\n  );\n\n  const handleDragMove = useCallback(\n    (e: MouseEvent) => {\n      if (!isDragging) return;\n      const deltaY = dragStartY.current - e.clientY;\n      const newHeight = Math.max(\n        100,\n        Math.min(800, dragStartHeight.current + deltaY),\n      );\n      setHeight(newHeight);\n    },\n    [isDragging],\n  );\n\n  const handleDragEnd = useCallback(() => {\n    setIsDragging(false);\n    document.body.style.userSelect = \"\";\n  }, []);\n\n  useEffect(() => {\n    if (isDragging) {\n      window.addEventListener(\"mousemove\", handleDragMove);\n      window.addEventListener(\"mouseup\", handleDragEnd);\n      return () => {\n        window.removeEventListener(\"mousemove\", handleDragMove);\n        window.removeEventListener(\"mouseup\", handleDragEnd);\n      };\n    }\n  }, [isDragging, handleDragMove, handleDragEnd]);\n\n  return {\n    height,\n    isDragging,\n    handleDragStart,\n  };\n}\n\nexport function useDraggableSidebar(initialWidth: number) {\n  const [width, setWidth] = useState(initialWidth);\n  const [isDragging, setIsDragging] = useState(false);\n  const dragStartX = useRef<number>(0);\n  const dragStartWidth = useRef<number>(0);\n\n  const handleDragStart = useCallback(\n    (e: React.MouseEvent) => {\n      setIsDragging(true);\n      dragStartX.current = e.clientX;\n      dragStartWidth.current = width;\n      document.body.style.userSelect = \"none\";\n    },\n    [width],\n  );\n\n  const handleDragMove = useCallback(\n    (e: MouseEvent) => {\n      if (!isDragging) return;\n      const deltaX = e.clientX - dragStartX.current;\n      const newWidth = Math.max(\n        200,\n        Math.min(600, dragStartWidth.current + deltaX),\n      );\n      setWidth(newWidth);\n    },\n    [isDragging],\n  );\n\n  const handleDragEnd = useCallback(() => {\n    setIsDragging(false);\n    document.body.style.userSelect = \"\";\n  }, []);\n\n  useEffect(() => {\n    if (isDragging) {\n      window.addEventListener(\"mousemove\", handleDragMove);\n      window.addEventListener(\"mouseup\", handleDragEnd);\n      return () => {\n        window.removeEventListener(\"mousemove\", handleDragMove);\n        window.removeEventListener(\"mouseup\", handleDragEnd);\n      };\n    }\n  }, [isDragging, handleDragMove, handleDragEnd]);\n\n  return {\n    width,\n    isDragging,\n    handleDragStart,\n  };\n}\n"
  },
  {
    "path": "client/src/lib/hooks/useTheme.ts",
    "content": "import { useCallback, useEffect, useMemo, useState } from \"react\";\n\ntype Theme = \"light\" | \"dark\" | \"system\";\n\nconst useTheme = (): [Theme, (mode: Theme) => void] => {\n  const [theme, setTheme] = useState<Theme>(() => {\n    const savedTheme = localStorage.getItem(\"theme\") as Theme;\n    return savedTheme || \"system\";\n  });\n\n  useEffect(() => {\n    const darkModeMediaQuery = window.matchMedia(\n      \"(prefers-color-scheme: dark)\",\n    );\n    const handleDarkModeChange = (e: MediaQueryListEvent) => {\n      if (theme === \"system\") {\n        updateDocumentTheme(e.matches ? \"dark\" : \"light\");\n      }\n    };\n\n    const updateDocumentTheme = (newTheme: \"light\" | \"dark\") => {\n      document.documentElement.classList.toggle(\"dark\", newTheme === \"dark\");\n    };\n\n    // Set initial theme based on current mode\n    if (theme === \"system\") {\n      updateDocumentTheme(darkModeMediaQuery.matches ? \"dark\" : \"light\");\n    } else {\n      updateDocumentTheme(theme);\n    }\n\n    darkModeMediaQuery.addEventListener(\"change\", handleDarkModeChange);\n\n    return () => {\n      darkModeMediaQuery.removeEventListener(\"change\", handleDarkModeChange);\n    };\n  }, [theme]);\n\n  const setThemeWithSideEffect = useCallback((newTheme: Theme) => {\n    setTheme(newTheme);\n    localStorage.setItem(\"theme\", newTheme);\n    if (newTheme !== \"system\") {\n      document.documentElement.classList.toggle(\"dark\", newTheme === \"dark\");\n    }\n  }, []);\n  return useMemo(\n    () => [theme, setThemeWithSideEffect],\n    [theme, setThemeWithSideEffect],\n  );\n};\n\nexport default useTheme;\n"
  },
  {
    "path": "client/src/lib/hooks/useToast.ts",
    "content": "\"use client\";\n\n// Inspired by react-hot-toast library\nimport * as React from \"react\";\n\nimport type { ToastActionElement, ToastProps } from \"@/components/ui/toast\";\n\nconst TOAST_LIMIT = 1;\nconst TOAST_REMOVE_DELAY = 1000000;\n\ntype ToasterToast = ToastProps & {\n  id: string;\n  title?: React.ReactNode;\n  description?: React.ReactNode;\n  action?: ToastActionElement;\n};\n\nlet count = 0;\n\nfunction genId() {\n  count = (count + 1) % Number.MAX_SAFE_INTEGER;\n  return count.toString();\n}\n\nconst enum ActionType {\n  ADD_TOAST = \"ADD_TOAST\",\n  UPDATE_TOAST = \"UPDATE_TOAST\",\n  DISMISS_TOAST = \"DISMISS_TOAST\",\n  REMOVE_TOAST = \"REMOVE_TOAST\",\n}\n\ntype Action =\n  | {\n      type: ActionType.ADD_TOAST;\n      toast: ToasterToast;\n    }\n  | {\n      type: ActionType.UPDATE_TOAST;\n      toast: Partial<ToasterToast>;\n    }\n  | {\n      type: ActionType.DISMISS_TOAST;\n      toastId?: ToasterToast[\"id\"];\n    }\n  | {\n      type: ActionType.REMOVE_TOAST;\n      toastId?: ToasterToast[\"id\"];\n    };\n\ninterface State {\n  toasts: ToasterToast[];\n}\n\nconst toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();\n\nconst addToRemoveQueue = (toastId: string) => {\n  if (toastTimeouts.has(toastId)) {\n    return;\n  }\n\n  const timeout = setTimeout(() => {\n    toastTimeouts.delete(toastId);\n    dispatch({\n      type: ActionType.REMOVE_TOAST,\n      toastId: toastId,\n    });\n  }, TOAST_REMOVE_DELAY);\n\n  toastTimeouts.set(toastId, timeout);\n};\n\nexport const reducer = (state: State, action: Action): State => {\n  switch (action.type) {\n    case ActionType.ADD_TOAST:\n      return {\n        ...state,\n        toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),\n      };\n\n    case ActionType.UPDATE_TOAST:\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === action.toast.id ? { ...t, ...action.toast } : t,\n        ),\n      };\n\n    case ActionType.DISMISS_TOAST: {\n      const { toastId } = action;\n\n      // ! Side effects ! - This could be extracted into a dismissToast() action,\n      // but I'll keep it here for simplicity\n      if (toastId) {\n        addToRemoveQueue(toastId);\n      } else {\n        state.toasts.forEach((toast) => {\n          addToRemoveQueue(toast.id);\n        });\n      }\n\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === toastId || toastId === undefined\n            ? {\n                ...t,\n                open: false,\n              }\n            : t,\n        ),\n      };\n    }\n    case ActionType.REMOVE_TOAST:\n      if (action.toastId === undefined) {\n        return {\n          ...state,\n          toasts: [],\n        };\n      }\n      return {\n        ...state,\n        toasts: state.toasts.filter((t) => t.id !== action.toastId),\n      };\n  }\n};\n\nconst listeners: Array<(state: State) => void> = [];\n\nlet memoryState: State = { toasts: [] };\n\nfunction dispatch(action: Action) {\n  memoryState = reducer(memoryState, action);\n  listeners.forEach((listener) => {\n    listener(memoryState);\n  });\n}\n\ntype Toast = Omit<ToasterToast, \"id\">;\n\nfunction toast({ ...props }: Toast) {\n  const id = genId();\n\n  const update = (props: ToasterToast) =>\n    dispatch({\n      type: ActionType.UPDATE_TOAST,\n      toast: { ...props, id },\n    });\n  const dismiss = () =>\n    dispatch({ type: ActionType.DISMISS_TOAST, toastId: id });\n\n  dispatch({\n    type: ActionType.ADD_TOAST,\n    toast: {\n      ...props,\n      id,\n      open: true,\n      onOpenChange: (open) => {\n        if (!open) dismiss();\n      },\n    },\n  });\n\n  return {\n    id: id,\n    dismiss,\n    update,\n  };\n}\n\nfunction useToast() {\n  const [state, setState] = React.useState<State>(memoryState);\n\n  React.useEffect(() => {\n    listeners.push(setState);\n    return () => {\n      const index = listeners.indexOf(setState);\n      if (index > -1) {\n        listeners.splice(index, 1);\n      }\n    };\n  }, [state]);\n\n  return {\n    ...state,\n    toast,\n    dismiss: (toastId?: string) =>\n      dispatch({ type: ActionType.DISMISS_TOAST, toastId }),\n  };\n}\n\nexport { useToast, toast };\n"
  },
  {
    "path": "client/src/lib/notificationTypes.ts",
    "content": "import {\n  NotificationSchema as BaseNotificationSchema,\n  ClientNotificationSchema,\n  ServerNotificationSchema,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport type { SchemaOutput } from \"@modelcontextprotocol/sdk/server/zod-compat.js\";\n\nexport const NotificationSchema = ClientNotificationSchema.or(\n  ServerNotificationSchema,\n).or(BaseNotificationSchema);\n\nexport type Notification = SchemaOutput<typeof NotificationSchema>;\n"
  },
  {
    "path": "client/src/lib/oauth-state-machine.ts",
    "content": "import { OAuthStep, AuthDebuggerState } from \"./auth-types\";\nimport { DebugInspectorOAuthClientProvider, discoverScopes } from \"./auth\";\nimport {\n  discoverAuthorizationServerMetadata,\n  registerClient,\n  startAuthorization,\n  exchangeAuthorization,\n  discoverOAuthProtectedResourceMetadata,\n  selectResourceURL,\n} from \"@modelcontextprotocol/sdk/client/auth.js\";\nimport {\n  OAuthMetadataSchema,\n  OAuthProtectedResourceMetadata,\n} from \"@modelcontextprotocol/sdk/shared/auth.js\";\nimport { generateOAuthState } from \"@/utils/oauthUtils\";\n\nexport interface StateMachineContext {\n  state: AuthDebuggerState;\n  serverUrl: string;\n  provider: DebugInspectorOAuthClientProvider;\n  updateState: (updates: Partial<AuthDebuggerState>) => void;\n}\n\nexport interface StateTransition {\n  canTransition: (context: StateMachineContext) => Promise<boolean>;\n  execute: (context: StateMachineContext) => Promise<void>;\n}\n\n// State machine transitions\nexport const oauthTransitions: Record<OAuthStep, StateTransition> = {\n  metadata_discovery: {\n    canTransition: async () => true,\n    execute: async (context) => {\n      // Default to discovering from the server's URL\n      let authServerUrl = new URL(\"/\", context.serverUrl);\n      let resourceMetadata: OAuthProtectedResourceMetadata | null = null;\n      let resourceMetadataError: Error | null = null;\n      try {\n        resourceMetadata = await discoverOAuthProtectedResourceMetadata(\n          context.serverUrl,\n        );\n        if (resourceMetadata?.authorization_servers?.length) {\n          authServerUrl = new URL(resourceMetadata.authorization_servers[0]);\n        }\n      } catch (e) {\n        if (e instanceof Error) {\n          resourceMetadataError = e;\n        } else {\n          resourceMetadataError = new Error(String(e));\n        }\n      }\n\n      const resource: URL | undefined = await selectResourceURL(\n        context.serverUrl,\n        context.provider,\n        // we default to null, so swap it for undefined if not set\n        resourceMetadata ?? undefined,\n      );\n\n      const metadata = await discoverAuthorizationServerMetadata(authServerUrl);\n      if (!metadata) {\n        throw new Error(\"Failed to discover OAuth metadata\");\n      }\n      const parsedMetadata = await OAuthMetadataSchema.parseAsync(metadata);\n      context.provider.saveServerMetadata(parsedMetadata);\n      context.updateState({\n        resourceMetadata,\n        resource,\n        resourceMetadataError,\n        authServerUrl,\n        oauthMetadata: parsedMetadata,\n        oauthStep: \"client_registration\",\n      });\n    },\n  },\n\n  client_registration: {\n    canTransition: async (context) => !!context.state.oauthMetadata,\n    execute: async (context) => {\n      const metadata = context.state.oauthMetadata!;\n      const clientMetadata = context.provider.clientMetadata;\n\n      // Priority: user-provided scope > discovered scopes\n      if (!context.provider.scope || context.provider.scope.trim() === \"\") {\n        // Prefer scopes from resource metadata if available\n        const scopesSupported =\n          context.state.resourceMetadata?.scopes_supported ||\n          metadata.scopes_supported;\n        // Add all supported scopes to client registration\n        if (scopesSupported) {\n          clientMetadata.scope = scopesSupported.join(\" \");\n        }\n      }\n\n      // Try Static client first, with DCR as fallback\n      let fullInformation = await context.provider.clientInformation();\n      if (!fullInformation) {\n        fullInformation = await registerClient(context.serverUrl, {\n          metadata,\n          clientMetadata,\n        });\n        context.provider.saveClientInformation(fullInformation);\n      }\n\n      context.updateState({\n        oauthClientInfo: fullInformation,\n        oauthStep: \"authorization_redirect\",\n      });\n    },\n  },\n\n  authorization_redirect: {\n    canTransition: async (context) =>\n      !!context.state.oauthMetadata && !!context.state.oauthClientInfo,\n    execute: async (context) => {\n      const metadata = context.state.oauthMetadata!;\n      const clientInformation = context.state.oauthClientInfo!;\n\n      // Priority: user-provided scope > discovered scopes\n      let scope = context.provider.scope;\n      if (!scope || scope.trim() === \"\") {\n        scope = await discoverScopes(\n          context.serverUrl,\n          context.state.resourceMetadata ?? undefined,\n        );\n      }\n\n      const { authorizationUrl, codeVerifier } = await startAuthorization(\n        context.serverUrl,\n        {\n          metadata,\n          clientInformation,\n          redirectUrl: context.provider.redirectUrl,\n          scope,\n          state: generateOAuthState(),\n          resource: context.state.resource ?? undefined,\n        },\n      );\n\n      context.provider.saveCodeVerifier(codeVerifier);\n      context.updateState({\n        authorizationUrl: authorizationUrl,\n        oauthStep: \"authorization_code\",\n      });\n    },\n  },\n\n  authorization_code: {\n    canTransition: async () => true,\n    execute: async (context) => {\n      if (\n        !context.state.authorizationCode ||\n        context.state.authorizationCode.trim() === \"\"\n      ) {\n        context.updateState({\n          validationError: \"You need to provide an authorization code\",\n        });\n        // Don't advance if no code\n        throw new Error(\"Authorization code required\");\n      }\n      context.updateState({\n        validationError: null,\n        oauthStep: \"token_request\",\n      });\n    },\n  },\n\n  token_request: {\n    canTransition: async (context) => {\n      return (\n        !!context.state.authorizationCode &&\n        !!context.provider.getServerMetadata() &&\n        !!(await context.provider.clientInformation())\n      );\n    },\n    execute: async (context) => {\n      const codeVerifier = context.provider.codeVerifier();\n      const metadata = context.provider.getServerMetadata()!;\n      const clientInformation = (await context.provider.clientInformation())!;\n\n      const tokens = await exchangeAuthorization(context.serverUrl, {\n        metadata,\n        clientInformation,\n        authorizationCode: context.state.authorizationCode,\n        codeVerifier,\n        redirectUri: context.provider.redirectUrl,\n        resource: context.state.resource\n          ? context.state.resource instanceof URL\n            ? context.state.resource\n            : new URL(context.state.resource)\n          : undefined,\n      });\n\n      context.provider.saveTokens(tokens);\n      context.updateState({\n        oauthTokens: tokens,\n        oauthStep: \"complete\",\n      });\n    },\n  },\n\n  complete: {\n    canTransition: async () => false,\n    execute: async () => {\n      // No-op for complete state\n    },\n  },\n};\n\nexport class OAuthStateMachine {\n  constructor(\n    private serverUrl: string,\n    private updateState: (updates: Partial<AuthDebuggerState>) => void,\n  ) {}\n\n  async executeStep(state: AuthDebuggerState): Promise<void> {\n    const provider = new DebugInspectorOAuthClientProvider(this.serverUrl);\n    const context: StateMachineContext = {\n      state,\n      serverUrl: this.serverUrl,\n      provider,\n      updateState: this.updateState,\n    };\n\n    const transition = oauthTransitions[state.oauthStep];\n    if (!(await transition.canTransition(context))) {\n      throw new Error(`Cannot transition from ${state.oauthStep}`);\n    }\n\n    await transition.execute(context);\n  }\n}\n"
  },
  {
    "path": "client/src/lib/types/customHeaders.ts",
    "content": "export interface CustomHeader {\n  name: string;\n  value: string;\n  enabled: boolean;\n}\n\nexport type CustomHeaders = CustomHeader[];\n\nexport const createEmptyHeader = (): CustomHeader => ({\n  name: \"\",\n  value: \"\",\n  enabled: true,\n});\n\nexport const createHeaderFromBearerToken = (\n  bearerToken: string,\n  headerName?: string,\n): CustomHeader => ({\n  name: headerName || \"Authorization\",\n  value:\n    headerName?.toLowerCase() === \"authorization\" || !headerName\n      ? `Bearer ${bearerToken}`\n      : bearerToken,\n  enabled: true,\n});\n\nexport const getEnabledHeaders = (headers: CustomHeaders): CustomHeaders => {\n  return headers.filter(\n    (header) => header.enabled && header.name.trim() && header.value.trim(),\n  );\n};\n\nexport const headersToRecord = (\n  headers: CustomHeaders,\n): Record<string, string> => {\n  const enabledHeaders = getEnabledHeaders(headers);\n  const record: Record<string, string> = {};\n\n  enabledHeaders.forEach((header) => {\n    record[header.name.trim()] = header.value.trim();\n  });\n\n  return record;\n};\n\nexport const recordToHeaders = (\n  record: Record<string, string>,\n): CustomHeaders => {\n  return Object.entries(record).map(([name, value]) => ({\n    name,\n    value,\n    enabled: true,\n  }));\n};\n\n// Migration helper for backward compatibility\nexport const migrateFromLegacyAuth = (\n  bearerToken?: string,\n  headerName?: string,\n): CustomHeaders => {\n  return bearerToken\n    ? [createHeaderFromBearerToken(bearerToken, headerName)]\n    : [];\n};\n"
  },
  {
    "path": "client/src/lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "client/src/main.tsx",
    "content": "import { StrictMode } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { Toaster } from \"@/components/ui/toaster.tsx\";\nimport App from \"./App.tsx\";\nimport \"./index.css\";\nimport { TooltipProvider } from \"./components/ui/tooltip.tsx\";\n\ncreateRoot(document.getElementById(\"root\")!).render(\n  <StrictMode>\n    <TooltipProvider>\n      <App />\n    </TooltipProvider>\n    <Toaster />\n  </StrictMode>,\n);\n"
  },
  {
    "path": "client/src/utils/__tests__/configUtils.test.ts",
    "content": "import { getMCPProxyAuthToken } from \"../configUtils\";\nimport { DEFAULT_INSPECTOR_CONFIG } from \"../../lib/constants\";\nimport { InspectorConfig } from \"../../lib/configurationTypes\";\n\ndescribe(\"configUtils\", () => {\n  describe(\"getMCPProxyAuthToken\", () => {\n    test(\"returns token and default header name\", () => {\n      const config: InspectorConfig = {\n        ...DEFAULT_INSPECTOR_CONFIG,\n        MCP_PROXY_AUTH_TOKEN: {\n          ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,\n          value: \"test-token-123\",\n        },\n      };\n\n      const result = getMCPProxyAuthToken(config);\n\n      expect(result).toEqual({\n        token: \"test-token-123\",\n        header: \"X-MCP-Proxy-Auth\",\n      });\n    });\n\n    test(\"returns empty token when not configured\", () => {\n      const config: InspectorConfig = {\n        ...DEFAULT_INSPECTOR_CONFIG,\n        MCP_PROXY_AUTH_TOKEN: {\n          ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,\n          value: \"\",\n        },\n      };\n\n      const result = getMCPProxyAuthToken(config);\n\n      expect(result).toEqual({\n        token: \"\",\n        header: \"X-MCP-Proxy-Auth\",\n      });\n    });\n\n    test(\"always returns X-MCP-Proxy-Auth as header name\", () => {\n      const config: InspectorConfig = {\n        ...DEFAULT_INSPECTOR_CONFIG,\n        MCP_PROXY_AUTH_TOKEN: {\n          ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,\n          value: \"any-token\",\n        },\n      };\n\n      const result = getMCPProxyAuthToken(config);\n\n      expect(result.header).toBe(\"X-MCP-Proxy-Auth\");\n    });\n\n    test(\"handles null/undefined value gracefully\", () => {\n      const config: InspectorConfig = {\n        ...DEFAULT_INSPECTOR_CONFIG,\n        MCP_PROXY_AUTH_TOKEN: {\n          ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,\n          value: null as unknown as string,\n        },\n      };\n\n      const result = getMCPProxyAuthToken(config);\n\n      expect(result).toEqual({\n        token: null,\n        header: \"X-MCP-Proxy-Auth\",\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/utils/__tests__/escapeUnicode.test.ts",
    "content": "import { escapeUnicode } from \"../escapeUnicode\";\n\ndescribe(\"escapeUnicode\", () => {\n  it(\"should escape Unicode characters in a string\", () => {\n    const input = { text: \"你好世界\" };\n    const expected = '{\\n  \"text\": \"\\\\\\\\u4f60\\\\\\\\u597d\\\\\\\\u4e16\\\\\\\\u754c\"\\n}';\n    expect(escapeUnicode(input)).toBe(expected);\n  });\n\n  it(\"should handle empty strings\", () => {\n    const input = { text: \"\" };\n    const expected = '{\\n  \"text\": \"\"\\n}';\n    expect(escapeUnicode(input)).toBe(expected);\n  });\n\n  it(\"should handle null and undefined values\", () => {\n    const input = { text: null, value: undefined };\n    const expected = '{\\n  \"text\": null\\n}';\n    expect(escapeUnicode(input)).toBe(expected);\n  });\n\n  it(\"should handle numbers and booleans\", () => {\n    const input = { number: 123, boolean: true };\n    const expected = '{\\n  \"number\": 123,\\n  \"boolean\": true\\n}';\n    expect(escapeUnicode(input)).toBe(expected);\n  });\n});\n"
  },
  {
    "path": "client/src/utils/__tests__/jsonUtils.test.ts",
    "content": "import {\n  getDataType,\n  tryParseJson,\n  updateValueAtPath,\n  getValueAtPath,\n} from \"../jsonUtils\";\nimport type { JsonValue, JsonSchemaType } from \"../jsonUtils\";\n\ndescribe(\"getDataType\", () => {\n  test(\"should return 'string' for string values\", () => {\n    expect(getDataType(\"hello\")).toBe(\"string\");\n    expect(getDataType(\"\")).toBe(\"string\");\n  });\n\n  test(\"should return 'number' for number values\", () => {\n    expect(getDataType(123)).toBe(\"number\");\n    expect(getDataType(0)).toBe(\"number\");\n    expect(getDataType(-10)).toBe(\"number\");\n    expect(getDataType(1.5)).toBe(\"number\");\n    expect(getDataType(NaN)).toBe(\"number\");\n    expect(getDataType(Infinity)).toBe(\"number\");\n  });\n\n  test(\"should return 'boolean' for boolean values\", () => {\n    expect(getDataType(true)).toBe(\"boolean\");\n    expect(getDataType(false)).toBe(\"boolean\");\n  });\n\n  test(\"should return 'undefined' for undefined value\", () => {\n    expect(getDataType(undefined)).toBe(\"undefined\");\n  });\n\n  test(\"should return 'object' for object values\", () => {\n    expect(getDataType({})).toBe(\"object\");\n    expect(getDataType({ key: \"value\" })).toBe(\"object\");\n  });\n\n  test(\"should return 'array' for array values\", () => {\n    expect(getDataType([])).toBe(\"array\");\n    expect(getDataType([1, 2, 3])).toBe(\"array\");\n    expect(getDataType([\"a\", \"b\", \"c\"])).toBe(\"array\");\n    expect(getDataType([{}, { nested: true }])).toBe(\"array\");\n  });\n\n  test(\"should return 'null' for null value\", () => {\n    expect(getDataType(null)).toBe(\"null\");\n  });\n});\n\ndescribe(\"tryParseJson\", () => {\n  test(\"should correctly parse valid JSON object\", () => {\n    const jsonString = '{\"name\":\"test\",\"value\":123}';\n    const result = tryParseJson(jsonString);\n\n    expect(result.success).toBe(true);\n    expect(result.data).toEqual({ name: \"test\", value: 123 });\n  });\n\n  test(\"should correctly parse valid JSON array\", () => {\n    const jsonString = '[1,2,3,\"test\"]';\n    const result = tryParseJson(jsonString);\n\n    expect(result.success).toBe(true);\n    expect(result.data).toEqual([1, 2, 3, \"test\"]);\n  });\n\n  test(\"should correctly parse JSON with whitespace\", () => {\n    const jsonString = '  {  \"name\"  :  \"test\"  }  ';\n    const result = tryParseJson(jsonString);\n\n    expect(result.success).toBe(true);\n    expect(result.data).toEqual({ name: \"test\" });\n  });\n\n  test(\"should correctly parse nested JSON structures\", () => {\n    const jsonString =\n      '{\"user\":{\"name\":\"test\",\"details\":{\"age\":30}},\"items\":[1,2,3]}';\n    const result = tryParseJson(jsonString);\n\n    expect(result.success).toBe(true);\n    expect(result.data).toEqual({\n      user: {\n        name: \"test\",\n        details: {\n          age: 30,\n        },\n      },\n      items: [1, 2, 3],\n    });\n  });\n\n  test(\"should correctly parse empty objects and arrays\", () => {\n    expect(tryParseJson(\"{}\").success).toBe(true);\n    expect(tryParseJson(\"{}\").data).toEqual({});\n\n    expect(tryParseJson(\"[]\").success).toBe(true);\n    expect(tryParseJson(\"[]\").data).toEqual([]);\n  });\n\n  test(\"should return failure for non-JSON strings\", () => {\n    const nonJsonString = \"this is not json\";\n    const result = tryParseJson(nonJsonString);\n\n    expect(result.success).toBe(false);\n    expect(result.data).toBe(nonJsonString);\n  });\n\n  test(\"should return failure for malformed JSON\", () => {\n    const malformedJson = '{\"name\":\"test\",}';\n    const result = tryParseJson(malformedJson);\n\n    expect(result.success).toBe(false);\n    expect(result.data).toBe(malformedJson);\n  });\n\n  test(\"should return failure for strings with correct delimiters but invalid JSON\", () => {\n    const invalidJson = \"{name:test}\";\n    const result = tryParseJson(invalidJson);\n\n    expect(result.success).toBe(false);\n    expect(result.data).toBe(invalidJson);\n  });\n\n  test(\"should handle edge cases\", () => {\n    expect(tryParseJson(\"\").success).toBe(false);\n    expect(tryParseJson(\"\").data).toBe(\"\");\n\n    expect(tryParseJson(\"   \").success).toBe(false);\n    expect(tryParseJson(\"   \").data).toBe(\"   \");\n\n    expect(tryParseJson(\"null\").success).toBe(false);\n    expect(tryParseJson(\"null\").data).toBe(\"null\");\n\n    expect(tryParseJson('\"string\"').success).toBe(false);\n    expect(tryParseJson('\"string\"').data).toBe('\"string\"');\n\n    expect(tryParseJson(\"123\").success).toBe(false);\n    expect(tryParseJson(\"123\").data).toBe(\"123\");\n\n    expect(tryParseJson(\"true\").success).toBe(false);\n    expect(tryParseJson(\"true\").data).toBe(\"true\");\n  });\n});\n\ndescribe(\"updateValueAtPath\", () => {\n  // Basic functionality tests\n  test(\"returns the new value when path is empty\", () => {\n    expect(updateValueAtPath({ foo: \"bar\" }, [], \"newValue\")).toBe(\"newValue\");\n  });\n\n  test(\"initializes an empty object when input is null/undefined and path starts with a string\", () => {\n    expect(updateValueAtPath(null, [\"foo\"], \"bar\")).toEqual({\n      foo: \"bar\",\n    });\n    expect(updateValueAtPath(undefined, [\"foo\"], \"bar\")).toEqual({\n      foo: \"bar\",\n    });\n  });\n\n  test(\"initializes an empty array when input is null/undefined and path starts with a number\", () => {\n    expect(updateValueAtPath(null, [\"0\"], \"bar\")).toEqual([\"bar\"]);\n    expect(updateValueAtPath(undefined, [\"0\"], \"bar\")).toEqual([\"bar\"]);\n  });\n\n  // Object update tests\n  test(\"updates a simple object property\", () => {\n    const obj = { name: \"John\", age: 30 };\n    expect(updateValueAtPath(obj, [\"age\"], 31)).toEqual({\n      name: \"John\",\n      age: 31,\n    });\n  });\n\n  test(\"updates a nested object property\", () => {\n    const obj = { user: { name: \"John\", address: { city: \"New York\" } } };\n    expect(\n      updateValueAtPath(obj, [\"user\", \"address\", \"city\"], \"Boston\"),\n    ).toEqual({ user: { name: \"John\", address: { city: \"Boston\" } } });\n  });\n\n  test(\"creates missing object properties\", () => {\n    const obj = { user: { name: \"John\" } };\n    expect(\n      updateValueAtPath(obj, [\"user\", \"address\", \"city\"], \"Boston\"),\n    ).toEqual({ user: { name: \"John\", address: { city: \"Boston\" } } });\n  });\n\n  // Array update tests\n  test(\"updates an array item\", () => {\n    const arr = [1, 2, 3, 4];\n    expect(updateValueAtPath(arr, [\"2\"], 5)).toEqual([1, 2, 5, 4]);\n  });\n\n  test(\"extends an array when index is out of bounds\", () => {\n    const arr = [1, 2, 3];\n    const result = updateValueAtPath(arr, [\"5\"], \"new\") as JsonValue[];\n\n    // Check overall array structure\n    expect(result).toEqual([1, 2, 3, null, null, \"new\"]);\n\n    // Explicitly verify that indices 3 and 4 contain null, not undefined\n    expect(result[3]).toBe(null);\n    expect(result[4]).toBe(null);\n\n    // Verify these aren't \"holes\" in the array (important distinction)\n    expect(3 in result).toBe(true);\n    expect(4 in result).toBe(true);\n\n    // Verify the array has the correct length\n    expect(result.length).toBe(6);\n\n    // Verify the array doesn't have holes by checking every index exists\n    expect(result.every((_, index: number) => index in result)).toBe(true);\n  });\n\n  test(\"updates a nested array item\", () => {\n    const obj = { users: [{ name: \"John\" }, { name: \"Jane\" }] };\n    expect(updateValueAtPath(obj, [\"users\", \"1\", \"name\"], \"Janet\")).toEqual({\n      users: [{ name: \"John\" }, { name: \"Janet\" }],\n    });\n  });\n\n  // Error handling tests\n  test(\"returns original value when trying to update a primitive with a path\", () => {\n    const spy = jest.spyOn(console, \"error\").mockImplementation();\n    const result = updateValueAtPath(\"string\", [\"foo\"], \"bar\");\n    expect(result).toBe(\"string\");\n    expect(spy).toHaveBeenCalled();\n    spy.mockRestore();\n  });\n\n  test(\"returns original array when index is invalid\", () => {\n    const spy = jest.spyOn(console, \"error\").mockImplementation();\n    const arr = [1, 2, 3];\n    expect(updateValueAtPath(arr, [\"invalid\"], 4)).toEqual(arr);\n    expect(spy).toHaveBeenCalled();\n    spy.mockRestore();\n  });\n\n  test(\"returns original array when index is negative\", () => {\n    const spy = jest.spyOn(console, \"error\").mockImplementation();\n    const arr = [1, 2, 3];\n    expect(updateValueAtPath(arr, [\"-1\"], 4)).toEqual(arr);\n    expect(spy).toHaveBeenCalled();\n    spy.mockRestore();\n  });\n\n  test(\"handles sparse arrays correctly by filling holes with null\", () => {\n    // Create a sparse array by deleting an element\n    const sparseArr = [1, 2, 3];\n    delete sparseArr[1]; // Now sparseArr is [1, <1 empty item>, 3]\n\n    // Update a value beyond the array length\n    const result = updateValueAtPath(sparseArr, [\"5\"], \"new\") as JsonValue[];\n\n    // Check overall array structure\n    expect(result).toEqual([1, null, 3, null, null, \"new\"]);\n\n    // Explicitly verify that index 1 (the hole) contains null, not undefined\n    expect(result[1]).toBe(null);\n\n    // Verify this isn't a hole in the array\n    expect(1 in result).toBe(true);\n\n    // Verify all indices contain null (not undefined)\n    expect(result[1]).not.toBe(undefined);\n    expect(result[3]).toBe(null);\n    expect(result[4]).toBe(null);\n  });\n});\n\ndescribe(\"getValueAtPath\", () => {\n  test(\"returns the original value when path is empty\", () => {\n    const obj = { foo: \"bar\" };\n    expect(getValueAtPath(obj, [])).toBe(obj);\n  });\n\n  test(\"returns the value at a simple path\", () => {\n    const obj = { name: \"John\", age: 30 };\n    expect(getValueAtPath(obj, [\"name\"])).toBe(\"John\");\n  });\n\n  test(\"returns the value at a nested path\", () => {\n    const obj = { user: { name: \"John\", address: { city: \"New York\" } } };\n    expect(getValueAtPath(obj, [\"user\", \"address\", \"city\"])).toBe(\"New York\");\n  });\n\n  test(\"returns default value when path does not exist\", () => {\n    const obj = { user: { name: \"John\" } };\n    expect(getValueAtPath(obj, [\"user\", \"address\", \"city\"], \"Unknown\")).toBe(\n      \"Unknown\",\n    );\n  });\n\n  test(\"returns default value when input is null/undefined\", () => {\n    expect(getValueAtPath(null, [\"foo\"], \"default\")).toBe(\"default\");\n    expect(getValueAtPath(undefined, [\"foo\"], \"default\")).toBe(\"default\");\n  });\n\n  test(\"handles array indices correctly\", () => {\n    const arr = [\"a\", \"b\", \"c\"];\n    expect(getValueAtPath(arr, [\"1\"])).toBe(\"b\");\n  });\n\n  test(\"returns default value for out of bounds array indices\", () => {\n    const arr = [\"a\", \"b\", \"c\"];\n    expect(getValueAtPath(arr, [\"5\"], \"default\")).toBe(\"default\");\n  });\n\n  test(\"returns default value for invalid array indices\", () => {\n    const arr = [\"a\", \"b\", \"c\"];\n    expect(getValueAtPath(arr, [\"invalid\"], \"default\")).toBe(\"default\");\n  });\n\n  test(\"navigates through mixed object and array paths\", () => {\n    const obj = { users: [{ name: \"John\" }, { name: \"Jane\" }] };\n    expect(getValueAtPath(obj, [\"users\", \"1\", \"name\"])).toBe(\"Jane\");\n  });\n});\n\ndescribe(\"JsonSchemaType elicitation field support\", () => {\n  const sampleSchema: JsonSchemaType = {\n    type: \"object\",\n    title: \"User Info\",\n    description: \"User information form\",\n    properties: {\n      name: {\n        type: \"string\",\n        title: \"Full Name\",\n        description: \"Your full name\",\n        minLength: 2,\n        maxLength: 50,\n        pattern: \"^[A-Za-z\\\\s]+$\",\n      },\n      email: {\n        type: \"string\",\n        format: \"email\",\n        title: \"Email Address\",\n      },\n      age: {\n        type: \"integer\",\n        minimum: 18,\n        maximum: 120,\n        default: 25,\n      },\n      role: {\n        type: \"string\",\n        oneOf: [\n          { const: \"admin\", title: \"Administrator\" },\n          { const: \"user\", title: \"User\" },\n          { const: \"guest\", title: \"Guest\" },\n        ],\n      },\n    },\n    required: [\"name\", \"email\"],\n  };\n\n  test(\"should parse JsonSchemaType with elicitation fields\", () => {\n    const schemaString = JSON.stringify(sampleSchema);\n    const result = tryParseJson(schemaString);\n\n    expect(result.success).toBe(true);\n    expect(result.data).toEqual(sampleSchema);\n  });\n\n  test(\"should update schema properties with new validation fields\", () => {\n    const updated = updateValueAtPath(\n      sampleSchema,\n      [\"properties\", \"name\", \"minLength\"],\n      5,\n    );\n\n    expect(getValueAtPath(updated, [\"properties\", \"name\", \"minLength\"])).toBe(\n      5,\n    );\n  });\n\n  test(\"should handle oneOf with const and title fields\", () => {\n    const schema = {\n      type: \"string\",\n      oneOf: [\n        { const: \"option1\", title: \"Option 1\" },\n        { const: \"option2\", title: \"Option 2\" },\n      ],\n    };\n\n    expect(getValueAtPath(schema, [\"oneOf\", \"0\", \"const\"])).toBe(\"option1\");\n    expect(getValueAtPath(schema, [\"oneOf\", \"1\", \"title\"])).toBe(\"Option 2\");\n  });\n\n  test(\"should handle validation constraints\", () => {\n    const numberSchema = {\n      type: \"number\" as const,\n      minimum: 0,\n      maximum: 100,\n      default: 50,\n    };\n\n    expect(getValueAtPath(numberSchema, [\"minimum\"])).toBe(0);\n    expect(getValueAtPath(numberSchema, [\"maximum\"])).toBe(100);\n    expect(getValueAtPath(numberSchema, [\"default\"])).toBe(50);\n  });\n\n  test(\"should handle string format and pattern fields\", () => {\n    const stringSchema = {\n      type: \"string\" as const,\n      format: \"email\",\n      pattern: \"^[a-z]+@[a-z]+\\\\.[a-z]+$\",\n      minLength: 5,\n      maxLength: 100,\n    };\n\n    expect(getValueAtPath(stringSchema, [\"format\"])).toBe(\"email\");\n    expect(getValueAtPath(stringSchema, [\"pattern\"])).toBe(\n      \"^[a-z]+@[a-z]+\\\\.[a-z]+$\",\n    );\n    expect(getValueAtPath(stringSchema, [\"minLength\"])).toBe(5);\n  });\n\n  test(\"should handle title and description fields\", () => {\n    const schema = {\n      type: \"boolean\" as const,\n      title: \"Accept Terms\",\n      description: \"Do you accept the terms and conditions?\",\n      default: false,\n    };\n\n    expect(getValueAtPath(schema, [\"title\"])).toBe(\"Accept Terms\");\n    expect(getValueAtPath(schema, [\"description\"])).toBe(\n      \"Do you accept the terms and conditions?\",\n    );\n  });\n\n  test(\"should handle JSON Schema spec compliant oneOf with const for labeled enums\", () => {\n    // Example from JSON Schema spec: labeled enums using oneOf with const\n    const trafficLightSchema = {\n      type: \"string\" as const,\n      title: \"Traffic Light\",\n      description: \"Select a traffic light color\",\n      oneOf: [\n        { const: \"red\", title: \"Stop\" },\n        { const: \"amber\", title: \"Caution\" },\n        { const: \"green\", title: \"Go\" },\n      ],\n    };\n\n    // Verify the schema structure\n    expect(trafficLightSchema.type).toBe(\"string\");\n    expect(trafficLightSchema.oneOf).toHaveLength(3);\n\n    // Verify each oneOf option has const and title\n    expect(trafficLightSchema.oneOf[0].const).toBe(\"red\");\n    expect(trafficLightSchema.oneOf[0].title).toBe(\"Stop\");\n\n    expect(trafficLightSchema.oneOf[1].const).toBe(\"amber\");\n    expect(trafficLightSchema.oneOf[1].title).toBe(\"Caution\");\n\n    expect(trafficLightSchema.oneOf[2].const).toBe(\"green\");\n    expect(trafficLightSchema.oneOf[2].title).toBe(\"Go\");\n\n    // Test with JsonValue operations\n    const schemaAsJsonValue = trafficLightSchema as JsonValue;\n    expect(getValueAtPath(schemaAsJsonValue, [\"oneOf\", \"0\", \"const\"])).toBe(\n      \"red\",\n    );\n    expect(getValueAtPath(schemaAsJsonValue, [\"oneOf\", \"1\", \"title\"])).toBe(\n      \"Caution\",\n    );\n    expect(getValueAtPath(schemaAsJsonValue, [\"oneOf\", \"2\", \"const\"])).toBe(\n      \"green\",\n    );\n  });\n\n  test(\"should handle complex oneOf scenarios with mixed schema types\", () => {\n    const complexSchema = {\n      type: \"object\" as const,\n      title: \"User Preference\",\n      properties: {\n        theme: {\n          type: \"string\" as const,\n          oneOf: [\n            { const: \"light\", title: \"Light Mode\" },\n            { const: \"dark\", title: \"Dark Mode\" },\n            { const: \"auto\", title: \"Auto (System)\" },\n          ],\n        },\n        notifications: {\n          type: \"string\" as const,\n          oneOf: [\n            { const: \"all\", title: \"All Notifications\" },\n            { const: \"important\", title: \"Important Only\" },\n            { const: \"none\", title: \"None\" },\n          ],\n        },\n      },\n    };\n\n    expect(\n      getValueAtPath(complexSchema, [\n        \"properties\",\n        \"theme\",\n        \"oneOf\",\n        \"0\",\n        \"const\",\n      ]),\n    ).toBe(\"light\");\n    expect(\n      getValueAtPath(complexSchema, [\n        \"properties\",\n        \"theme\",\n        \"oneOf\",\n        \"1\",\n        \"title\",\n      ]),\n    ).toBe(\"Dark Mode\");\n    expect(\n      getValueAtPath(complexSchema, [\n        \"properties\",\n        \"notifications\",\n        \"oneOf\",\n        \"2\",\n        \"const\",\n      ]),\n    ).toBe(\"none\");\n  });\n});\n"
  },
  {
    "path": "client/src/utils/__tests__/oauthUtils.test.ts",
    "content": "import {\n  generateOAuthErrorDescription,\n  parseOAuthCallbackParams,\n  generateOAuthState,\n  getAuthorizationServerMetadataDiscoveryUrl,\n} from \"@/utils/oauthUtils.ts\";\n\ndescribe(\"parseOAuthCallbackParams\", () => {\n  it(\"Returns successful: true and code when present\", () => {\n    expect(parseOAuthCallbackParams(\"?code=fake-code\")).toEqual({\n      successful: true,\n      code: \"fake-code\",\n    });\n  });\n  it(\"Returns successful: false and error when error is present\", () => {\n    expect(parseOAuthCallbackParams(\"?error=access_denied\")).toEqual({\n      successful: false,\n      error: \"access_denied\",\n      error_description: null,\n      error_uri: null,\n    });\n  });\n  it(\"Returns optional error metadata fields when present\", () => {\n    const search =\n      \"?error=access_denied&\" +\n      \"error_description=User%20Denied%20Request&\" +\n      \"error_uri=https%3A%2F%2Fexample.com%2Ferror-docs\";\n    expect(parseOAuthCallbackParams(search)).toEqual({\n      successful: false,\n      error: \"access_denied\",\n      error_description: \"User Denied Request\",\n      error_uri: \"https://example.com/error-docs\",\n    });\n  });\n  it(\"Returns error when nothing present\", () => {\n    expect(parseOAuthCallbackParams(\"?\")).toEqual({\n      successful: false,\n      error: \"invalid_request\",\n      error_description: \"Missing code or error in response\",\n      error_uri: null,\n    });\n  });\n});\n\ndescribe(\"generateOAuthErrorDescription\", () => {\n  it(\"When only error is present\", () => {\n    expect(\n      generateOAuthErrorDescription({\n        successful: false,\n        error: \"invalid_request\",\n        error_description: null,\n        error_uri: null,\n      }),\n    ).toBe(\"Error: invalid_request.\");\n  });\n  it(\"When error description is present\", () => {\n    expect(\n      generateOAuthErrorDescription({\n        successful: false,\n        error: \"invalid_request\",\n        error_description: \"The request could not be completed as dialed\",\n        error_uri: null,\n      }),\n    ).toEqual(\n      \"Error: invalid_request.\\nDetails: The request could not be completed as dialed.\",\n    );\n  });\n  it(\"When all fields present\", () => {\n    expect(\n      generateOAuthErrorDescription({\n        successful: false,\n        error: \"invalid_request\",\n        error_description: \"The request could not be completed as dialed\",\n        error_uri: \"https://example.com/error-docs\",\n      }),\n    ).toEqual(\n      \"Error: invalid_request.\\nDetails: The request could not be completed as dialed.\\nMore info: https://example.com/error-docs.\",\n    );\n  });\n\n  describe(\"generateOAuthState\", () => {\n    it(\"Returns a string\", () => {\n      expect(generateOAuthState()).toBeDefined();\n      expect(generateOAuthState()).toHaveLength(64);\n    });\n  });\n});\n\ndescribe(\"getAuthorizationServerMetadataDiscoveryUrl\", () => {\n  it(\"uses root discovery URL for root authorization server URL\", () => {\n    expect(\n      getAuthorizationServerMetadataDiscoveryUrl(\"https://example.com\"),\n    ).toBe(\"https://example.com/.well-known/oauth-authorization-server\");\n  });\n\n  it(\"inserts tenant path for non-root authorization server URL\", () => {\n    expect(\n      getAuthorizationServerMetadataDiscoveryUrl(\"https://example.com/tenant1\"),\n    ).toBe(\n      \"https://example.com/.well-known/oauth-authorization-server/tenant1\",\n    );\n  });\n\n  it(\"strips trailing slash before appending tenant path\", () => {\n    expect(\n      getAuthorizationServerMetadataDiscoveryUrl(\n        \"https://example.com/tenant1/\",\n      ),\n    ).toBe(\n      \"https://example.com/.well-known/oauth-authorization-server/tenant1\",\n    );\n  });\n});\n"
  },
  {
    "path": "client/src/utils/__tests__/paramUtils.test.ts",
    "content": "import { cleanParams } from \"../paramUtils\";\nimport type { JsonSchemaType } from \"../jsonUtils\";\n\ndescribe(\"cleanParams\", () => {\n  it(\"should preserve required fields even when undefined\", () => {\n    const schema: JsonSchemaType = {\n      type: \"object\",\n      required: [\"requiredString\", \"requiredNumber\"],\n      properties: {\n        requiredString: { type: \"string\" },\n        requiredNumber: { type: \"number\" },\n        optionalString: { type: \"string\" },\n        optionalNumber: { type: \"number\" },\n      },\n    };\n\n    const params = {\n      requiredString: undefined,\n      requiredNumber: undefined,\n      optionalString: undefined,\n      optionalNumber: undefined,\n    };\n\n    const cleaned = cleanParams(params, schema);\n\n    expect(cleaned).toEqual({\n      requiredString: undefined,\n      requiredNumber: undefined,\n      // optionalString and optionalNumber should be omitted\n    });\n  });\n\n  it(\"should omit optional fields with empty strings\", () => {\n    const schema: JsonSchemaType = {\n      type: \"object\",\n      required: [],\n      properties: {\n        optionalString: { type: \"string\" },\n        optionalNumber: { type: \"number\" },\n      },\n    };\n\n    const params = {\n      optionalString: \"\",\n      optionalNumber: \"\",\n    };\n\n    const cleaned = cleanParams(params, schema);\n\n    expect(cleaned).toEqual({});\n  });\n\n  it(\"should omit optional fields with undefined values\", () => {\n    const schema: JsonSchemaType = {\n      type: \"object\",\n      required: [],\n      properties: {\n        optionalString: { type: \"string\" },\n        optionalNumber: { type: \"number\" },\n      },\n    };\n\n    const params = {\n      optionalString: undefined,\n      optionalNumber: undefined,\n    };\n\n    const cleaned = cleanParams(params, schema);\n\n    expect(cleaned).toEqual({});\n  });\n\n  it(\"should omit optional fields with null values\", () => {\n    const schema: JsonSchemaType = {\n      type: \"object\",\n      required: [],\n      properties: {\n        optionalString: { type: \"string\" },\n        optionalNumber: { type: \"number\" },\n      },\n    };\n\n    const params = {\n      optionalString: null,\n      optionalNumber: null,\n    };\n\n    const cleaned = cleanParams(params, schema);\n\n    expect(cleaned).toEqual({});\n  });\n\n  it(\"should preserve optional fields with meaningful values\", () => {\n    const schema: JsonSchemaType = {\n      type: \"object\",\n      required: [],\n      properties: {\n        optionalString: { type: \"string\" },\n        optionalNumber: { type: \"number\" },\n        optionalBoolean: { type: \"boolean\" },\n      },\n    };\n\n    const params = {\n      optionalString: \"hello\",\n      optionalNumber: 42,\n      optionalBoolean: false, // false is a meaningful value\n    };\n\n    const cleaned = cleanParams(params, schema);\n\n    expect(cleaned).toEqual({\n      optionalString: \"hello\",\n      optionalNumber: 42,\n      optionalBoolean: false,\n    });\n  });\n\n  it(\"should handle mixed required and optional fields\", () => {\n    const schema: JsonSchemaType = {\n      type: \"object\",\n      required: [\"requiredField\"],\n      properties: {\n        requiredField: { type: \"string\" },\n        optionalWithValue: { type: \"string\" },\n        optionalEmpty: { type: \"string\" },\n        optionalUndefined: { type: \"number\" },\n      },\n    };\n\n    const params = {\n      requiredField: \"\",\n      optionalWithValue: \"test\",\n      optionalEmpty: \"\",\n      optionalUndefined: undefined,\n    };\n\n    const cleaned = cleanParams(params, schema);\n\n    expect(cleaned).toEqual({\n      requiredField: \"\",\n      optionalWithValue: \"test\",\n    });\n  });\n\n  it(\"should handle schema without required array\", () => {\n    const schema: JsonSchemaType = {\n      type: \"object\",\n      properties: {\n        field1: { type: \"string\" },\n        field2: { type: \"number\" },\n      },\n    };\n\n    const params = {\n      field1: \"\",\n      field2: undefined,\n    };\n\n    const cleaned = cleanParams(params, schema);\n\n    expect(cleaned).toEqual({});\n  });\n\n  it(\"should preserve zero values for numbers\", () => {\n    const schema: JsonSchemaType = {\n      type: \"object\",\n      required: [],\n      properties: {\n        optionalNumber: { type: \"number\" },\n      },\n    };\n\n    const params = {\n      optionalNumber: 0,\n    };\n\n    const cleaned = cleanParams(params, schema);\n\n    expect(cleaned).toEqual({\n      optionalNumber: 0,\n    });\n  });\n\n  it(\"should handle the new undefined-first approach (no empty strings)\", () => {\n    const schema: JsonSchemaType = {\n      type: \"object\",\n      required: [\"requiredField\"],\n      properties: {\n        requiredField: { type: \"string\" },\n        optionalField: { type: \"string\" },\n      },\n    };\n\n    // New behavior: cleared fields are undefined, never empty strings\n    const params = {\n      requiredField: undefined, // cleared required field\n      optionalField: undefined, // cleared optional field\n    };\n\n    const cleaned = cleanParams(params, schema);\n\n    expect(cleaned).toEqual({\n      requiredField: undefined, // required field preserved as undefined\n      // optionalField omitted entirely\n    });\n  });\n\n  it(\"should preserve null values when field has default: null\", () => {\n    const schema: JsonSchemaType = {\n      type: \"object\",\n      required: [],\n      properties: {\n        optionalFieldWithNullDefault: { type: \"string\", default: null },\n        optionalFieldWithoutDefault: { type: \"string\" },\n      },\n    };\n\n    const params = {\n      optionalFieldWithNullDefault: null,\n      optionalFieldWithoutDefault: null,\n    };\n\n    const cleaned = cleanParams(params, schema);\n\n    expect(cleaned).toEqual({\n      optionalFieldWithNullDefault: null, // preserved because default: null\n      // optionalFieldWithoutDefault omitted\n    });\n  });\n\n  it(\"should preserve default values that match current value\", () => {\n    const schema: JsonSchemaType = {\n      type: \"object\",\n      required: [],\n      properties: {\n        fieldWithDefaultString: { type: \"string\", default: \"defaultValue\" },\n        fieldWithDefaultNumber: { type: \"number\", default: 42 },\n        fieldWithDefaultNull: { type: \"string\", default: null },\n        fieldWithDefaultBoolean: { type: \"boolean\", default: false },\n      },\n    };\n\n    const params = {\n      fieldWithDefaultString: \"defaultValue\",\n      fieldWithDefaultNumber: 42,\n      fieldWithDefaultNull: null,\n      fieldWithDefaultBoolean: false,\n    };\n\n    const cleaned = cleanParams(params, schema);\n\n    expect(cleaned).toEqual({\n      fieldWithDefaultString: \"defaultValue\",\n      fieldWithDefaultNumber: 42,\n      fieldWithDefaultNull: null,\n      fieldWithDefaultBoolean: false,\n    });\n  });\n\n  it(\"should omit values that do not match their default\", () => {\n    const schema: JsonSchemaType = {\n      type: \"object\",\n      required: [],\n      properties: {\n        fieldWithDefault: { type: \"string\", default: \"defaultValue\" },\n      },\n    };\n\n    const params = {\n      fieldWithDefault: null, // doesn't match default\n    };\n\n    const cleaned = cleanParams(params, schema);\n\n    expect(cleaned).toEqual({\n      // fieldWithDefault omitted because value (null) doesn't match default (\"defaultValue\")\n    });\n  });\n\n  it(\"should fix regression from issue #846 - tools with multiple null defaults\", () => {\n    // Reproduces the exact scenario from https://github.com/modelcontextprotocol/inspector/issues/846\n    // In v0.17.0, the cleanParams function would remove all null values,\n    // breaking tools that have parameters with explicit default: null\n    const schema: JsonSchemaType = {\n      type: \"object\",\n      required: [\"requiredString\"],\n      properties: {\n        optionalString: { type: [\"string\", \"null\"], default: null },\n        optionalNumber: { type: [\"number\", \"null\"], default: null },\n        optionalBoolean: { type: [\"boolean\", \"null\"], default: null },\n        requiredString: { type: \"string\" },\n      },\n    };\n\n    // When a user opens the tool in Inspector, fields initialize with their defaults\n    const params = {\n      optionalString: null, // initialized to default\n      optionalNumber: null, // initialized to default\n      optionalBoolean: null, // initialized to default\n      requiredString: \"test\",\n    };\n\n    const cleaned = cleanParams(params, schema);\n\n    // In v0.16, null defaults were preserved (working behavior)\n    // In v0.17.0, they were removed (regression)\n    // This fix restores the v0.16 behavior\n    expect(cleaned).toEqual({\n      optionalString: null,\n      optionalNumber: null,\n      optionalBoolean: null,\n      requiredString: \"test\",\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/utils/__tests__/schemaUtils.test.ts",
    "content": "import {\n  generateDefaultValue,\n  formatFieldLabel,\n  normalizeUnionType,\n  cacheToolOutputSchemas,\n  getToolOutputValidator,\n  validateToolOutput,\n  hasOutputSchema,\n} from \"../schemaUtils\";\nimport type { JsonSchemaType } from \"../jsonUtils\";\nimport type { Tool } from \"@modelcontextprotocol/sdk/types.js\";\n\ndescribe(\"generateDefaultValue\", () => {\n  test(\"generates default string\", () => {\n    const parentSchema = { type: \"object\" as const, required: [\"testProp\"] };\n    expect(\n      generateDefaultValue({ type: \"string\" }, \"testProp\", parentSchema),\n    ).toBe(\"\");\n  });\n\n  test(\"generates default number\", () => {\n    const parentSchema = { type: \"object\" as const, required: [\"testProp\"] };\n    expect(\n      generateDefaultValue({ type: \"number\" }, \"testProp\", parentSchema),\n    ).toBe(0);\n  });\n\n  test(\"generates default integer\", () => {\n    const parentSchema = { type: \"object\" as const, required: [\"testProp\"] };\n    expect(\n      generateDefaultValue({ type: \"integer\" }, \"testProp\", parentSchema),\n    ).toBe(0);\n  });\n\n  test(\"generates default boolean\", () => {\n    const parentSchema = { type: \"object\" as const, required: [\"testProp\"] };\n    expect(\n      generateDefaultValue({ type: \"boolean\" }, \"testProp\", parentSchema),\n    ).toBe(false);\n  });\n\n  test(\"generates undefined for optional array\", () => {\n    expect(generateDefaultValue({ type: \"array\" })).toBe(undefined);\n  });\n\n  test(\"generates empty object for optional root object\", () => {\n    expect(generateDefaultValue({ type: \"object\" })).toEqual({});\n  });\n\n  test(\"generates undefined for nested optional object\", () => {\n    // When called WITH propertyName and parentSchema, and the property is NOT required,\n    // nested optional objects should return undefined\n    const parentSchema = {\n      type: \"object\" as const,\n      required: [\"otherField\"],\n      properties: {\n        optionalObject: { type: \"object\" as const },\n        otherField: { type: \"string\" as const },\n      },\n    };\n    expect(\n      generateDefaultValue({ type: \"object\" }, \"optionalObject\", parentSchema),\n    ).toBe(undefined);\n  });\n\n  test(\"generates empty object for root-level object with all optional properties\", () => {\n    // Root-level schema with properties but no required array\n    // This is the exact scenario from PR #926 - elicitation with all optional fields\n    const schema: JsonSchemaType = {\n      type: \"object\",\n      properties: {\n        optionalField1: { type: \"string\" },\n        optionalField2: { type: \"number\" },\n      },\n      // No required array - all fields are optional\n    };\n    expect(generateDefaultValue(schema)).toEqual({});\n  });\n\n  test(\"generates default null for unknown types\", () => {\n    // @ts-expect-error Testing with invalid type\n    expect(generateDefaultValue({ type: \"unknown\" })).toBe(undefined);\n  });\n\n  test(\"generates empty array for required array\", () => {\n    const parentSchema = { required: [\"testArray\"] };\n    expect(\n      generateDefaultValue({ type: \"array\" }, \"testArray\", parentSchema),\n    ).toEqual([]);\n  });\n\n  test(\"generates undefined for non-required array\", () => {\n    const parentSchema = { required: [\"otherField\"] };\n    expect(\n      generateDefaultValue({ type: \"array\" }, \"testArray\", parentSchema),\n    ).toBe(undefined);\n  });\n\n  test(\"generates empty object for required object\", () => {\n    const parentSchema = { required: [\"testObject\"] };\n    expect(\n      generateDefaultValue({ type: \"object\" }, \"testObject\", parentSchema),\n    ).toEqual({});\n  });\n\n  test(\"generates undefined for non-required object\", () => {\n    const parentSchema = { required: [\"otherField\"] };\n    expect(\n      generateDefaultValue({ type: \"object\" }, \"testObject\", parentSchema),\n    ).toBe(undefined);\n  });\n\n  test(\"generates undefined for non-required primitive types\", () => {\n    expect(generateDefaultValue({ type: \"string\" })).toBe(undefined);\n    expect(generateDefaultValue({ type: \"number\" })).toBe(undefined);\n    expect(generateDefaultValue({ type: \"boolean\" })).toBe(undefined);\n  });\n\n  test(\"generates object with properties\", () => {\n    const schema: JsonSchemaType = {\n      type: \"object\",\n      required: [\"name\", \"age\", \"isActive\"],\n      properties: {\n        name: { type: \"string\" },\n        age: { type: \"number\" },\n        isActive: { type: \"boolean\" },\n      },\n    };\n    expect(generateDefaultValue(schema)).toEqual({\n      name: \"\",\n      age: 0,\n      isActive: false,\n    });\n  });\n\n  test(\"handles nested objects\", () => {\n    const schema: JsonSchemaType = {\n      type: \"object\",\n      required: [\"user\"],\n      properties: {\n        user: {\n          type: \"object\",\n          required: [\"name\", \"address\"],\n          properties: {\n            name: { type: \"string\" },\n            address: {\n              type: \"object\",\n              required: [\"city\"],\n              properties: {\n                city: { type: \"string\" },\n              },\n            },\n          },\n        },\n      },\n    };\n    expect(generateDefaultValue(schema)).toEqual({\n      user: {\n        name: \"\",\n        address: {\n          city: \"\",\n        },\n      },\n    });\n  });\n\n  test(\"uses schema default value when provided\", () => {\n    expect(generateDefaultValue({ type: \"string\", default: \"test\" })).toBe(\n      \"test\",\n    );\n  });\n});\n\ndescribe(\"formatFieldLabel\", () => {\n  test(\"formats camelCase\", () => {\n    expect(formatFieldLabel(\"firstName\")).toBe(\"First Name\");\n  });\n\n  test(\"formats snake_case\", () => {\n    expect(formatFieldLabel(\"first_name\")).toBe(\"First name\");\n  });\n\n  test(\"formats single word\", () => {\n    expect(formatFieldLabel(\"name\")).toBe(\"Name\");\n  });\n\n  test(\"formats mixed case with underscores\", () => {\n    expect(formatFieldLabel(\"user_firstName\")).toBe(\"User first Name\");\n  });\n\n  test(\"handles empty string\", () => {\n    expect(formatFieldLabel(\"\")).toBe(\"\");\n  });\n});\n\ndescribe(\"normalizeUnionType\", () => {\n  test(\"normalizes anyOf with string and null to string type\", () => {\n    const schema: JsonSchemaType = {\n      anyOf: [{ type: \"string\" }, { type: \"null\" }],\n      description: \"Optional string parameter\",\n    };\n\n    const normalized = normalizeUnionType(schema);\n\n    expect(normalized.type).toBe(\"string\");\n    expect(normalized.anyOf).toBeUndefined();\n    expect(normalized.description).toBe(\"Optional string parameter\");\n  });\n\n  test(\"normalizes anyOf with boolean and null to boolean type\", () => {\n    const schema: JsonSchemaType = {\n      anyOf: [{ type: \"boolean\" }, { type: \"null\" }],\n      description: \"Optional boolean parameter\",\n    };\n\n    const normalized = normalizeUnionType(schema);\n\n    expect(normalized.type).toBe(\"boolean\");\n    expect(normalized.anyOf).toBeUndefined();\n    expect(normalized.description).toBe(\"Optional boolean parameter\");\n    expect(normalized.nullable).toBeTruthy();\n  });\n\n  test(\"normalizes array type with string and null to string type\", () => {\n    const schema: JsonSchemaType = {\n      type: [\"string\", \"null\"],\n      description: \"Optional string parameter\",\n    };\n\n    const normalized = normalizeUnionType(schema);\n\n    expect(normalized.type).toBe(\"string\");\n    expect(normalized.description).toBe(\"Optional string parameter\");\n    expect(normalized.nullable).toBeTruthy();\n  });\n\n  test(\"normalizes array type with boolean and null to boolean type\", () => {\n    const schema: JsonSchemaType = {\n      type: [\"boolean\", \"null\"],\n      description: \"Optional boolean parameter\",\n    };\n\n    const normalized = normalizeUnionType(schema);\n\n    expect(normalized.type).toBe(\"boolean\");\n    expect(normalized.description).toBe(\"Optional boolean parameter\");\n    expect(normalized.nullable).toBeTruthy();\n  });\n\n  test(\"normalizes anyOf with number and null to number type\", () => {\n    const schema: JsonSchemaType = {\n      anyOf: [{ type: \"number\" }, { type: \"null\" }],\n      description: \"Optional number parameter\",\n    };\n\n    const normalized = normalizeUnionType(schema);\n\n    expect(normalized.type).toBe(\"number\");\n    expect(normalized.anyOf).toBeUndefined();\n    expect(normalized.description).toBe(\"Optional number parameter\");\n    expect(normalized.nullable).toBeTruthy();\n  });\n\n  test(\"normalizes anyOf with integer and null to integer type\", () => {\n    const schema: JsonSchemaType = {\n      anyOf: [{ type: \"integer\" }, { type: \"null\" }],\n      description: \"Optional integer parameter\",\n    };\n\n    const normalized = normalizeUnionType(schema);\n\n    expect(normalized.type).toBe(\"integer\");\n    expect(normalized.anyOf).toBeUndefined();\n    expect(normalized.description).toBe(\"Optional integer parameter\");\n    expect(normalized.nullable).toBeTruthy();\n  });\n\n  test(\"normalizes array type with number and null to number type\", () => {\n    const schema: JsonSchemaType = {\n      type: [\"number\", \"null\"],\n      description: \"Optional number parameter\",\n    };\n\n    const normalized = normalizeUnionType(schema);\n\n    expect(normalized.type).toBe(\"number\");\n    expect(normalized.description).toBe(\"Optional number parameter\");\n    expect(normalized.nullable).toBeTruthy();\n  });\n\n  test(\"normalizes array type with integer and null to integer type\", () => {\n    const schema: JsonSchemaType = {\n      type: [\"integer\", \"null\"],\n      description: \"Optional integer parameter\",\n    };\n\n    const normalized = normalizeUnionType(schema);\n\n    expect(normalized.type).toBe(\"integer\");\n    expect(normalized.description).toBe(\"Optional integer parameter\");\n    expect(normalized.nullable).toBeTruthy();\n  });\n\n  test(\"handles anyOf with reversed order (null first)\", () => {\n    const schema: JsonSchemaType = {\n      anyOf: [{ type: \"null\" }, { type: \"string\" }],\n    };\n\n    const normalized = normalizeUnionType(schema);\n\n    expect(normalized.type).toBe(\"string\");\n    expect(normalized.anyOf).toBeUndefined();\n    expect(normalized.nullable).toBeTruthy();\n  });\n\n  test(\"leaves non-union schemas unchanged\", () => {\n    const schema: JsonSchemaType = {\n      type: \"string\",\n      description: \"Regular string parameter\",\n    };\n\n    const normalized = normalizeUnionType(schema);\n\n    expect(normalized).toEqual(schema);\n  });\n\n  test(\"leaves anyOf with non-matching types unchanged\", () => {\n    const schema: JsonSchemaType = {\n      anyOf: [{ type: \"string\" }, { type: \"number\" }],\n    };\n\n    const normalized = normalizeUnionType(schema);\n\n    expect(normalized).toEqual(schema);\n    expect(normalized.nullable).toBeFalsy();\n  });\n\n  test(\"leaves anyOf with more than two types unchanged\", () => {\n    const schema: JsonSchemaType = {\n      anyOf: [{ type: \"string\" }, { type: \"number\" }, { type: \"null\" }],\n    };\n\n    const normalized = normalizeUnionType(schema);\n\n    expect(normalized).toEqual(schema);\n  });\n\n  test(\"leaves array type with non-matching types unchanged\", () => {\n    const schema: JsonSchemaType = {\n      type: [\"string\", \"number\"],\n    };\n\n    const normalized = normalizeUnionType(schema);\n\n    expect(normalized).toEqual(schema);\n  });\n\n  test(\"handles schemas without type or anyOf\", () => {\n    const schema: JsonSchemaType = {\n      description: \"Schema without type\",\n    };\n\n    const normalized = normalizeUnionType(schema);\n\n    expect(normalized).toEqual(schema);\n  });\n\n  test(\"preserves other properties when normalizing\", () => {\n    const schema: JsonSchemaType = {\n      anyOf: [{ type: \"string\" }, { type: \"null\" }],\n      description: \"Optional string\",\n      minLength: 1,\n      maxLength: 100,\n      pattern: \"^[a-z]+$\",\n    };\n\n    const normalized = normalizeUnionType(schema);\n\n    expect(normalized.type).toBe(\"string\");\n    expect(normalized.anyOf).toBeUndefined();\n    expect(normalized.description).toBe(\"Optional string\");\n    expect(normalized.minLength).toBe(1);\n    expect(normalized.maxLength).toBe(100);\n    expect(normalized.pattern).toBe(\"^[a-z]+$\");\n    expect(normalized.nullable).toBeTruthy();\n  });\n});\n\ndescribe(\"Output Schema Validation\", () => {\n  const mockTools: Tool[] = [\n    {\n      name: \"weatherTool\",\n      description: \"Get weather information\",\n      inputSchema: {\n        type: \"object\",\n        properties: {\n          city: { type: \"string\" },\n        },\n      },\n      outputSchema: {\n        type: \"object\",\n        properties: {\n          temperature: { type: \"number\" },\n          humidity: { type: \"number\" },\n        },\n        required: [\"temperature\", \"humidity\"],\n      },\n    },\n    {\n      name: \"noOutputSchema\",\n      description: \"Tool without output schema\",\n      inputSchema: {\n        type: \"object\",\n        properties: {},\n      },\n    },\n    {\n      name: \"complexOutputSchema\",\n      description: \"Tool with complex output schema\",\n      inputSchema: {\n        type: \"object\",\n        properties: {},\n      },\n      outputSchema: {\n        type: \"object\",\n        properties: {\n          user: {\n            type: \"object\",\n            properties: {\n              name: { type: \"string\" },\n              age: { type: \"number\" },\n            },\n            required: [\"name\"],\n          },\n          tags: {\n            type: \"array\",\n            items: { type: \"string\" },\n          },\n        },\n        required: [\"user\"],\n      },\n    },\n  ];\n\n  beforeEach(() => {\n    // Clear cache before each test\n    cacheToolOutputSchemas([]);\n  });\n\n  describe(\"cacheToolOutputSchemas\", () => {\n    test(\"caches validators for tools with output schemas\", () => {\n      cacheToolOutputSchemas(mockTools);\n\n      expect(hasOutputSchema(\"weatherTool\")).toBe(true);\n      expect(hasOutputSchema(\"complexOutputSchema\")).toBe(true);\n      expect(hasOutputSchema(\"noOutputSchema\")).toBe(false);\n    });\n\n    test(\"clears existing cache when called\", () => {\n      cacheToolOutputSchemas(mockTools);\n      expect(hasOutputSchema(\"weatherTool\")).toBe(true);\n\n      cacheToolOutputSchemas([]);\n      expect(hasOutputSchema(\"weatherTool\")).toBe(false);\n    });\n\n    test(\"handles invalid output schemas gracefully\", () => {\n      const toolsWithInvalidSchema: Tool[] = [\n        {\n          name: \"invalidSchemaTool\",\n          description: \"Tool with invalid schema\",\n          inputSchema: { type: \"object\", properties: {} },\n          outputSchema: {\n            // @ts-expect-error Testing with invalid type\n            type: \"invalid-type\",\n          },\n        },\n      ];\n\n      // Should not throw\n      expect(() =>\n        cacheToolOutputSchemas(toolsWithInvalidSchema),\n      ).not.toThrow();\n      expect(hasOutputSchema(\"invalidSchemaTool\")).toBe(false);\n    });\n  });\n\n  describe(\"validateToolOutput\", () => {\n    beforeEach(() => {\n      cacheToolOutputSchemas(mockTools);\n    });\n\n    test(\"validates correct structured content\", () => {\n      const result = validateToolOutput(\"weatherTool\", {\n        temperature: 25.5,\n        humidity: 60,\n      });\n\n      expect(result.isValid).toBe(true);\n      expect(result.error).toBeUndefined();\n    });\n\n    test(\"rejects invalid structured content\", () => {\n      const result = validateToolOutput(\"weatherTool\", {\n        temperature: \"25.5\", // Should be number\n        humidity: 60,\n      });\n\n      expect(result.isValid).toBe(false);\n      expect(result.error).toContain(\"should be number\");\n    });\n\n    test(\"rejects missing required fields\", () => {\n      const result = validateToolOutput(\"weatherTool\", {\n        temperature: 25.5,\n        // Missing humidity\n      });\n\n      expect(result.isValid).toBe(false);\n      expect(result.error).toContain(\"required\");\n    });\n\n    test(\"validates complex nested structures\", () => {\n      const validResult = validateToolOutput(\"complexOutputSchema\", {\n        user: {\n          name: \"John\",\n          age: 30,\n        },\n        tags: [\"tag1\", \"tag2\"],\n      });\n\n      expect(validResult.isValid).toBe(true);\n\n      const invalidResult = validateToolOutput(\"complexOutputSchema\", {\n        user: {\n          // Missing required 'name'\n          age: 30,\n        },\n      });\n\n      expect(invalidResult.isValid).toBe(false);\n    });\n\n    test(\"returns valid for tools without validators\", () => {\n      const result = validateToolOutput(\"nonExistentTool\", { any: \"data\" });\n\n      expect(result.isValid).toBe(true);\n      expect(result.error).toBeUndefined();\n    });\n\n    test(\"validates additional properties restriction\", () => {\n      const result = validateToolOutput(\"weatherTool\", {\n        temperature: 25.5,\n        humidity: 60,\n        extraField: \"should not be here\",\n      });\n\n      // This depends on whether additionalProperties is set to false in the schema\n      // If it is, this should fail\n      expect(result.isValid).toBe(true); // By default, additional properties are allowed\n    });\n  });\n\n  describe(\"getToolOutputValidator\", () => {\n    beforeEach(() => {\n      cacheToolOutputSchemas(mockTools);\n    });\n\n    test(\"returns validator for cached tool\", () => {\n      const validator = getToolOutputValidator(\"weatherTool\");\n      expect(validator).toBeDefined();\n      expect(typeof validator).toBe(\"function\");\n    });\n\n    test(\"returns undefined for tool without output schema\", () => {\n      const validator = getToolOutputValidator(\"noOutputSchema\");\n      expect(validator).toBeUndefined();\n    });\n\n    test(\"returns undefined for non-existent tool\", () => {\n      const validator = getToolOutputValidator(\"nonExistentTool\");\n      expect(validator).toBeUndefined();\n    });\n  });\n\n  describe(\"hasOutputSchema\", () => {\n    beforeEach(() => {\n      cacheToolOutputSchemas(mockTools);\n    });\n\n    test(\"returns true for tools with output schemas\", () => {\n      expect(hasOutputSchema(\"weatherTool\")).toBe(true);\n      expect(hasOutputSchema(\"complexOutputSchema\")).toBe(true);\n    });\n\n    test(\"returns false for tools without output schemas\", () => {\n      expect(hasOutputSchema(\"noOutputSchema\")).toBe(false);\n    });\n\n    test(\"returns false for non-existent tools\", () => {\n      expect(hasOutputSchema(\"nonExistentTool\")).toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/utils/__tests__/urlValidation.test.ts",
    "content": "import { validateRedirectUrl } from \"../urlValidation\";\n\ndescribe(\"validateRedirectUrl\", () => {\n  describe(\"valid URLs\", () => {\n    it(\"should allow HTTP URLs\", () => {\n      expect(() => validateRedirectUrl(\"http://example.com\")).not.toThrow();\n    });\n\n    it(\"should allow HTTPS URLs\", () => {\n      expect(() => validateRedirectUrl(\"https://example.com\")).not.toThrow();\n    });\n\n    it(\"should allow URLs with ports\", () => {\n      expect(() =>\n        validateRedirectUrl(\"https://example.com:8080\"),\n      ).not.toThrow();\n    });\n\n    it(\"should allow URLs with paths\", () => {\n      expect(() =>\n        validateRedirectUrl(\"https://example.com/path/to/auth\"),\n      ).not.toThrow();\n    });\n\n    it(\"should allow URLs with query parameters\", () => {\n      expect(() =>\n        validateRedirectUrl(\"https://example.com?param=value\"),\n      ).not.toThrow();\n    });\n  });\n\n  describe(\"invalid URLs - XSS vectors\", () => {\n    it(\"should block javascript: protocol\", () => {\n      expect(() => validateRedirectUrl(\"javascript:alert('XSS')\")).toThrow(\n        \"Authorization URL must be HTTP or HTTPS\",\n      );\n    });\n\n    it(\"should block javascript: with encoded characters\", () => {\n      expect(() =>\n        validateRedirectUrl(\"javascript:alert%28%27XSS%27%29\"),\n      ).toThrow(\"Authorization URL must be HTTP or HTTPS\");\n    });\n\n    it(\"should block data: protocol\", () => {\n      expect(() =>\n        validateRedirectUrl(\"data:text/html,<script>alert('XSS')</script>\"),\n      ).toThrow(\"Authorization URL must be HTTP or HTTPS\");\n    });\n\n    it(\"should block vbscript: protocol\", () => {\n      expect(() => validateRedirectUrl(\"vbscript:msgbox\")).toThrow(\n        \"Authorization URL must be HTTP or HTTPS\",\n      );\n    });\n\n    it(\"should block file: protocol\", () => {\n      expect(() => validateRedirectUrl(\"file:///etc/passwd\")).toThrow(\n        \"Authorization URL must be HTTP or HTTPS\",\n      );\n    });\n\n    it(\"should block about: protocol\", () => {\n      expect(() => validateRedirectUrl(\"about:blank\")).toThrow(\n        \"Authorization URL must be HTTP or HTTPS\",\n      );\n    });\n\n    it(\"should block custom protocols\", () => {\n      expect(() => validateRedirectUrl(\"custom://example\")).toThrow(\n        \"Authorization URL must be HTTP or HTTPS\",\n      );\n    });\n  });\n\n  describe(\"edge cases\", () => {\n    it(\"should handle malformed URLs\", () => {\n      expect(() => validateRedirectUrl(\"not a url\")).toThrow(\n        \"Invalid URL: not a url\",\n      );\n    });\n\n    it(\"should handle empty string\", () => {\n      expect(() => validateRedirectUrl(\"\")).toThrow(\"Invalid URL: \");\n    });\n\n    it(\"should handle URLs with unicode characters\", () => {\n      expect(() => validateRedirectUrl(\"https://例え.jp\")).not.toThrow();\n    });\n\n    it(\"should handle URLs with case variations\", () => {\n      expect(() => validateRedirectUrl(\"HTTPS://EXAMPLE.COM\")).not.toThrow();\n      expect(() => validateRedirectUrl(\"HtTpS://example.com\")).not.toThrow();\n    });\n\n    it(\"should handle protocol-relative URLs as invalid\", () => {\n      expect(() => validateRedirectUrl(\"//example.com\")).toThrow(\n        \"Invalid URL: //example.com\",\n      );\n    });\n\n    it(\"should handle URLs with authentication\", () => {\n      expect(() =>\n        validateRedirectUrl(\"https://user:pass@example.com\"),\n      ).not.toThrow();\n    });\n  });\n\n  describe(\"security considerations\", () => {\n    it(\"should not be fooled by whitespace\", () => {\n      expect(() => validateRedirectUrl(\" javascript:alert('XSS')\")).toThrow();\n      expect(() => validateRedirectUrl(\"javascript:alert('XSS') \")).toThrow();\n    });\n\n    it(\"should handle null bytes\", () => {\n      expect(() =>\n        validateRedirectUrl(\"java\\x00script:alert('XSS')\"),\n      ).toThrow();\n    });\n\n    it(\"should handle tab characters\", () => {\n      expect(() => validateRedirectUrl(\"java\\tscript:alert('XSS')\")).toThrow();\n    });\n\n    it(\"should handle newlines\", () => {\n      expect(() => validateRedirectUrl(\"java\\nscript:alert('XSS')\")).toThrow();\n    });\n\n    it(\"should handle mixed case protocols\", () => {\n      expect(() => validateRedirectUrl(\"JaVaScRiPt:alert('XSS')\")).toThrow(\n        \"Authorization URL must be HTTP or HTTPS\",\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/utils/configUtils.ts",
    "content": "import { InspectorConfig } from \"@/lib/configurationTypes\";\nimport {\n  DEFAULT_MCP_PROXY_LISTEN_PORT,\n  DEFAULT_INSPECTOR_CONFIG,\n} from \"@/lib/constants\";\n\nconst getSearchParam = (key: string): string | null => {\n  try {\n    const url = new URL(window.location.href);\n    return url.searchParams.get(key);\n  } catch {\n    return null;\n  }\n};\n\nexport const getMCPProxyAddress = (config: InspectorConfig): string => {\n  let proxyFullAddress = config.MCP_PROXY_FULL_ADDRESS.value as string;\n  if (proxyFullAddress) {\n    proxyFullAddress = proxyFullAddress.replace(/\\/+$/, \"\");\n    return proxyFullAddress;\n  }\n\n  // Check for proxy port from query params, fallback to default\n  const proxyPort =\n    getSearchParam(\"MCP_PROXY_PORT\") || DEFAULT_MCP_PROXY_LISTEN_PORT;\n\n  return `${window.location.protocol}//${window.location.hostname}:${proxyPort}`;\n};\n\nexport const getMCPServerRequestTimeout = (config: InspectorConfig): number => {\n  return config.MCP_SERVER_REQUEST_TIMEOUT.value as number;\n};\n\nexport const resetRequestTimeoutOnProgress = (\n  config: InspectorConfig,\n): boolean => {\n  return config.MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS.value as boolean;\n};\n\nexport const getMCPServerRequestMaxTotalTimeout = (\n  config: InspectorConfig,\n): number => {\n  return config.MCP_REQUEST_MAX_TOTAL_TIMEOUT.value as number;\n};\n\nexport const getMCPProxyAuthToken = (\n  config: InspectorConfig,\n): {\n  token: string;\n  header: string;\n} => {\n  return {\n    token: config.MCP_PROXY_AUTH_TOKEN.value as string,\n    header: \"X-MCP-Proxy-Auth\",\n  };\n};\nexport const getMCPTaskTtl = (config: InspectorConfig): number => {\n  return config.MCP_TASK_TTL.value as number;\n};\n\nexport const getInitialTransportType = ():\n  | \"stdio\"\n  | \"sse\"\n  | \"streamable-http\" => {\n  const param = getSearchParam(\"transport\");\n  if (param === \"stdio\" || param === \"sse\" || param === \"streamable-http\") {\n    return param;\n  }\n  return (\n    (localStorage.getItem(\"lastTransportType\") as\n      | \"stdio\"\n      | \"sse\"\n      | \"streamable-http\") || \"stdio\"\n  );\n};\n\nexport const getInitialSseUrl = (): string => {\n  const param = getSearchParam(\"serverUrl\");\n  if (param) return param;\n  return localStorage.getItem(\"lastSseUrl\") || \"http://localhost:3001/sse\";\n};\n\nexport const getInitialCommand = (): string => {\n  const param = getSearchParam(\"serverCommand\");\n  if (param) return param;\n  return localStorage.getItem(\"lastCommand\") || \"mcp-server-everything\";\n};\n\nexport const getInitialArgs = (): string => {\n  const param = getSearchParam(\"serverArgs\");\n  if (param) return param;\n  return localStorage.getItem(\"lastArgs\") || \"\";\n};\n\n// Returns a map of config key -> value from query params if present\nexport const getConfigOverridesFromQueryParams = (\n  defaultConfig: InspectorConfig,\n): Partial<InspectorConfig> => {\n  const url = new URL(window.location.href);\n  const overrides: Partial<InspectorConfig> = {};\n  for (const key of Object.keys(defaultConfig)) {\n    const param = url.searchParams.get(key);\n    if (param !== null) {\n      // Try to coerce to correct type based on default value\n      const defaultValue = defaultConfig[key as keyof InspectorConfig].value;\n      let value: string | number | boolean = param;\n      if (typeof defaultValue === \"number\") {\n        value = Number(param);\n      } else if (typeof defaultValue === \"boolean\") {\n        value = param === \"true\";\n      }\n      overrides[key as keyof InspectorConfig] = {\n        ...defaultConfig[key as keyof InspectorConfig],\n        value,\n      };\n    }\n  }\n  return overrides;\n};\n\nexport const initializeInspectorConfig = (\n  localStorageKey: string,\n): InspectorConfig => {\n  // Read persistent config from localStorage\n  const savedPersistentConfig = localStorage.getItem(localStorageKey);\n  // Read ephemeral config from sessionStorage\n  const savedEphemeralConfig = sessionStorage.getItem(\n    `${localStorageKey}_ephemeral`,\n  );\n\n  // Start with default config\n  let baseConfig = { ...DEFAULT_INSPECTOR_CONFIG };\n\n  // Apply saved persistent config\n  if (savedPersistentConfig) {\n    const parsedPersistentConfig = JSON.parse(savedPersistentConfig);\n    baseConfig = { ...baseConfig, ...parsedPersistentConfig };\n  }\n\n  // Apply saved ephemeral config\n  if (savedEphemeralConfig) {\n    const parsedEphemeralConfig = JSON.parse(savedEphemeralConfig);\n    baseConfig = { ...baseConfig, ...parsedEphemeralConfig };\n  }\n\n  // Ensure all config items have the latest labels/descriptions from defaults\n  for (const [key, value] of Object.entries(baseConfig)) {\n    baseConfig[key as keyof InspectorConfig] = {\n      ...value,\n      label: DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].label,\n      description:\n        DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].description,\n      is_session_item:\n        DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].is_session_item,\n    };\n  }\n\n  // Apply query param overrides\n  const overrides = getConfigOverridesFromQueryParams(DEFAULT_INSPECTOR_CONFIG);\n  return { ...baseConfig, ...overrides };\n};\n\nexport const saveInspectorConfig = (\n  localStorageKey: string,\n  config: InspectorConfig,\n): void => {\n  const persistentConfig: Partial<InspectorConfig> = {};\n  const ephemeralConfig: Partial<InspectorConfig> = {};\n\n  // Split config based on is_session_item flag\n  for (const [key, value] of Object.entries(config)) {\n    if (value.is_session_item) {\n      ephemeralConfig[key as keyof InspectorConfig] = value;\n    } else {\n      persistentConfig[key as keyof InspectorConfig] = value;\n    }\n  }\n\n  // Save persistent config to localStorage\n  localStorage.setItem(localStorageKey, JSON.stringify(persistentConfig));\n\n  // Save ephemeral config to sessionStorage\n  sessionStorage.setItem(\n    `${localStorageKey}_ephemeral`,\n    JSON.stringify(ephemeralConfig),\n  );\n};\n"
  },
  {
    "path": "client/src/utils/escapeUnicode.ts",
    "content": "// Utility function to escape Unicode characters\nexport function escapeUnicode(obj: unknown): string {\n  return JSON.stringify(\n    obj,\n    (_key: string, value) => {\n      if (typeof value === \"string\") {\n        // Replace non-ASCII characters with their Unicode escape sequences\n        return value.replace(/[^\\0-\\x7F]/g, (char) => {\n          return \"\\\\u\" + (\"0000\" + char.charCodeAt(0).toString(16)).slice(-4);\n        });\n      }\n      return value;\n    },\n    2,\n  );\n}\n"
  },
  {
    "path": "client/src/utils/jsonUtils.ts",
    "content": "export type JsonValue =\n  | string\n  | number\n  | boolean\n  | null\n  | undefined\n  | JsonValue[]\n  | { [key: string]: JsonValue };\n\nexport type JsonSchemaConst = {\n  const: JsonValue;\n  title?: string;\n  description?: string;\n};\n\nexport type JsonSchemaType = {\n  type?:\n    | \"string\"\n    | \"number\"\n    | \"integer\"\n    | \"boolean\"\n    | \"array\"\n    | \"object\"\n    | \"null\"\n    | (\n        | \"string\"\n        | \"number\"\n        | \"integer\"\n        | \"boolean\"\n        | \"array\"\n        | \"object\"\n        | \"null\"\n      )[];\n  title?: string;\n  description?: string;\n  required?: string[];\n  default?: JsonValue;\n  properties?: Record<string, JsonSchemaType>;\n  items?: JsonSchemaType;\n  // Array validation constraints\n  minItems?: number;\n  maxItems?: number;\n  minimum?: number;\n  maximum?: number;\n  minLength?: number;\n  maxLength?: number;\n  nullable?: boolean;\n  pattern?: string;\n  format?: string;\n  enum?: string[];\n  // Non-standard legacy support: titles for enum values\n  enumNames?: string[];\n  const?: JsonValue;\n  oneOf?: (JsonSchemaType | JsonSchemaConst)[];\n  anyOf?: (JsonSchemaType | JsonSchemaConst)[];\n  $ref?: string;\n};\n\nexport type JsonObject = { [key: string]: JsonValue };\n\nexport type DataType =\n  | \"string\"\n  | \"number\"\n  | \"bigint\"\n  | \"boolean\"\n  | \"symbol\"\n  | \"undefined\"\n  | \"object\"\n  | \"function\"\n  | \"array\"\n  | \"null\";\n\n/**\n * Determines the specific data type of a JSON value\n * @param value The JSON value to analyze\n * @returns The specific data type including \"array\" and \"null\" as distinct types\n */\nexport function getDataType(value: JsonValue): DataType {\n  if (Array.isArray(value)) return \"array\";\n  if (value === null) return \"null\";\n  return typeof value;\n}\n\n/**\n * Attempts to parse a string as JSON, only for objects and arrays\n * @param str The string to parse\n * @returns Object with success boolean and either parsed data or original string\n */\nexport function tryParseJson(str: string): {\n  success: boolean;\n  data: JsonValue;\n} {\n  const trimmed = str?.trim();\n  if (\n    trimmed &&\n    !(trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) &&\n    !(trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\"))\n  ) {\n    return { success: false, data: str };\n  }\n  try {\n    return { success: true, data: JSON.parse(str) };\n  } catch {\n    return { success: false, data: str };\n  }\n}\n\n/**\n * Updates a value at a specific path in a nested JSON structure\n * @param obj The original JSON value\n * @param path Array of keys/indices representing the path to the value\n * @param value The new value to set\n * @returns A new JSON value with the updated path\n */\nexport function updateValueAtPath(\n  obj: JsonValue,\n  path: string[],\n  value: JsonValue,\n): JsonValue {\n  if (path.length === 0) return value;\n\n  if (obj === null || obj === undefined) {\n    obj = !isNaN(Number(path[0])) ? [] : {};\n  }\n\n  if (Array.isArray(obj)) {\n    return updateArray(obj, path, value);\n  } else if (typeof obj === \"object\" && obj !== null) {\n    return updateObject(obj as JsonObject, path, value);\n  } else {\n    console.error(\n      `Cannot update path ${path.join(\".\")} in non-object/array value:`,\n      obj,\n    );\n    return obj;\n  }\n}\n\n/**\n * Updates an array at a specific path\n */\nfunction updateArray(\n  array: JsonValue[],\n  path: string[],\n  value: JsonValue,\n): JsonValue[] {\n  const [index, ...restPath] = path;\n  const arrayIndex = Number(index);\n\n  if (isNaN(arrayIndex)) {\n    console.error(`Invalid array index: ${index}`);\n    return array;\n  }\n\n  if (arrayIndex < 0) {\n    console.error(`Array index out of bounds: ${arrayIndex} < 0`);\n    return array;\n  }\n\n  let newArray: JsonValue[] = [];\n  for (let i = 0; i < array.length; i++) {\n    newArray[i] = i in array ? array[i] : null;\n  }\n\n  if (arrayIndex >= newArray.length) {\n    const extendedArray: JsonValue[] = new Array(arrayIndex).fill(null);\n    // Copy over the existing elements (now guaranteed to be dense)\n    for (let i = 0; i < newArray.length; i++) {\n      extendedArray[i] = newArray[i];\n    }\n    newArray = extendedArray;\n  }\n\n  if (restPath.length === 0) {\n    newArray[arrayIndex] = value;\n  } else {\n    newArray[arrayIndex] = updateValueAtPath(\n      newArray[arrayIndex],\n      restPath,\n      value,\n    );\n  }\n  return newArray;\n}\n\n/**\n * Updates an object at a specific path\n */\nfunction updateObject(\n  obj: JsonObject,\n  path: string[],\n  value: JsonValue,\n): JsonObject {\n  const [key, ...restPath] = path;\n\n  // Validate object key\n  if (typeof key !== \"string\") {\n    console.error(`Invalid object key: ${key}`);\n    return obj;\n  }\n\n  const newObj = { ...obj };\n\n  if (restPath.length === 0) {\n    newObj[key] = value;\n  } else {\n    // Ensure key exists\n    if (!(key in newObj)) {\n      newObj[key] = {};\n    }\n    newObj[key] = updateValueAtPath(newObj[key], restPath, value);\n  }\n  return newObj;\n}\n\n/**\n * Gets a value at a specific path in a nested JSON structure\n * @param obj The JSON value to traverse\n * @param path Array of keys/indices representing the path to the value\n * @param defaultValue Value to return if path doesn't exist\n * @returns The value at the path, or defaultValue if not found\n */\nexport function getValueAtPath(\n  obj: JsonValue,\n  path: string[],\n  defaultValue: JsonValue = null,\n): JsonValue {\n  if (path.length === 0) return obj;\n\n  const [first, ...rest] = path;\n\n  if (obj === null || obj === undefined) {\n    return defaultValue;\n  }\n\n  if (Array.isArray(obj)) {\n    const index = Number(first);\n    if (isNaN(index) || index < 0 || index >= obj.length) {\n      return defaultValue;\n    }\n    return getValueAtPath(obj[index], rest, defaultValue);\n  }\n\n  if (typeof obj === \"object\" && obj !== null) {\n    if (!(first in obj)) {\n      return defaultValue;\n    }\n    return getValueAtPath((obj as JsonObject)[first], rest, defaultValue);\n  }\n\n  return defaultValue;\n}\n"
  },
  {
    "path": "client/src/utils/metaUtils.ts",
    "content": "/**\n * Metadata helpers aligned with the official MCP specification.\n *\n * @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta\n */\n\nconst META_PREFIX_LABEL_REGEX = /^[a-z](?:[a-z\\d-]*[a-z\\d])?$/i;\nconst META_NAME_REGEX = /^[a-z\\d](?:[a-z\\d._-]*[a-z\\d])?$/i;\nconst RESERVED_NAMESPACE_LABELS = [\"modelcontextprotocol\", \"mcp\"];\n\nexport const RESERVED_NAMESPACE_MESSAGE =\n  'Keys using the \"modelcontextprotocol.*\" or \"mcp.*\" namespaces are reserved by MCP and cannot be used.';\n\nexport const META_NAME_RULES_MESSAGE =\n  \"Names must begin and end with an alphanumeric character and may only contain alphanumerics, hyphens (-), underscores (_), or dots (.) in between.\";\n\nexport const META_PREFIX_RULES_MESSAGE =\n  \"Prefixes must be dot-separated labels that start with a letter and end with a letter or digit (e.g. example.domain/).\";\n\n/**\n * Extracts the prefix portion (before the first slash) of a metadata key, if present.\n *\n * @param key - Raw metadata key entered by the user.\n * @returns The prefix segment (without the trailing slash) or null when no prefix exists.\n * @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta\n */\nconst getPrefixSegment = (key: string): string | null => {\n  const trimmedKey = key.trim();\n  const slashIndex = trimmedKey.indexOf(\"/\");\n  if (slashIndex === -1) {\n    return null;\n  }\n  return trimmedKey.slice(0, slashIndex);\n};\n\n/**\n * Normalizes a potential prefix segment by trimming whitespace, removing schemes,\n * and stripping trailing URL components so only the label portion remains.\n *\n * @param segment - The prefix segment extracted from the metadata key.\n * @returns A normalized string suitable for label parsing, or null when empty.\n * @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta\n */\nconst normalizeSegment = (segment: string): string | null => {\n  if (!segment) return null;\n  let normalized = segment.trim().toLowerCase();\n  if (!normalized) return null;\n\n  const schemeIndex = normalized.indexOf(\"://\");\n  if (schemeIndex !== -1) {\n    normalized = normalized.slice(schemeIndex + 3);\n  }\n\n  const stopChars = [\"?\", \"#\", \":\"];\n  let endIndex = normalized.length;\n  stopChars.forEach((char) => {\n    const idx = normalized.indexOf(char);\n    if (idx !== -1 && idx < endIndex) {\n      endIndex = idx;\n    }\n  });\n\n  return normalized.slice(0, endIndex) || null;\n};\n\n/**\n * Splits a normalized prefix into dot-separated labels and validates each label\n * against the MCP prefix rules (start with letter, end with letter/digit, interior alphanumerics or hyphens).\n *\n * @param segment - Normalized prefix string.\n * @returns Array of labels if valid, otherwise null.\n * @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta\n */\nconst splitLabels = (segment: string): string[] | null => {\n  const normalized = normalizeSegment(segment);\n  if (!normalized) return null;\n\n  const labels = normalized.split(\".\");\n  if (\n    labels.length === 0 ||\n    labels.some((label) => !label || !META_PREFIX_LABEL_REGEX.test(label))\n  ) {\n    return null;\n  }\n\n  return labels;\n};\n\n/**\n * Determines whether a metadata key is within the MCP-reserved namespace.\n *\n * @param key - Full metadata key entered by the user.\n * @returns True if the key's prefix belongs to a reserved namespace.\n * @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta\n */\nexport const isReservedMetaKey = (key: string): boolean => {\n  const trimmedKey = key.trim();\n  if (!trimmedKey) {\n    return false;\n  }\n\n  const candidateSegment = getPrefixSegment(trimmedKey) ?? trimmedKey;\n  const labels = splitLabels(candidateSegment);\n  if (!labels || labels.length < 2) {\n    return false;\n  }\n\n  for (let i = 0; i < labels.length - 1; i += 1) {\n    const current = labels[i];\n    const next = labels[i + 1];\n    if (\n      RESERVED_NAMESPACE_LABELS.includes(current) &&\n      META_PREFIX_LABEL_REGEX.test(next)\n    ) {\n      return true;\n    }\n  }\n\n  return false;\n};\n\n/**\n * Validates the optional prefix portion of a metadata key.\n *\n * @param key - Full metadata key entered by the user.\n * @returns True when the prefix is absent or satisfies the MCP label requirements.\n * @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta\n */\nexport const hasValidMetaPrefix = (key: string): boolean => {\n  const prefixSegment = getPrefixSegment(key);\n  if (prefixSegment === null) {\n    return true;\n  }\n\n  return splitLabels(prefixSegment) !== null;\n};\n\nconst extractMetaName = (key: string): string => {\n  const trimmedKey = key.trim();\n  if (!trimmedKey) return \"\";\n\n  const slashIndex = trimmedKey.lastIndexOf(\"/\");\n  if (slashIndex === -1) {\n    return trimmedKey;\n  }\n\n  return trimmedKey.slice(slashIndex + 1);\n};\n\n/**\n * Validates the \"name\" portion of a metadata key, regardless of whether a prefix exists.\n *\n * @param key - Full metadata key entered by the user.\n * @returns True if the name portion is valid per the MCP spec.\n * @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta\n */\nexport const hasValidMetaName = (key: string): boolean => {\n  const name = extractMetaName(key);\n  if (!name) return false;\n\n  return META_NAME_REGEX.test(name);\n};\n"
  },
  {
    "path": "client/src/utils/oauthUtils.ts",
    "content": "// The parsed query parameters returned by the Authorization Server\n// representing either a valid authorization_code or an error\n// ref: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-12#section-4.1.2\ntype CallbackParams =\n  | {\n      successful: true;\n      // The authorization code is generated by the authorization server.\n      code: string;\n    }\n  | {\n      successful: false;\n      // The OAuth 2.1 Error Code.\n      // Usually one of:\n      //    ```\n      //    invalid_request, unauthorized_client, access_denied, unsupported_response_type,\n      //    invalid_scope, server_error, temporarily_unavailable\n      //    ```\n      error: string;\n      // Human-readable ASCII text providing additional information, used to assist the\n      // developer in understanding the error that occurred.\n      error_description: string | null;\n      // A URI identifying a human-readable web page with information about the error,\n      // used to provide the client developer with additional information about the error.\n      error_uri: string | null;\n    };\n\n/**\n * Parses OAuth 2.1 callback parameters from a URL search string\n * @param location The URL search string (e.g., \"?code=abc123\" or \"?error=access_denied\")\n * @returns Parsed callback parameters with success/error information\n */\nexport const parseOAuthCallbackParams = (location: string): CallbackParams => {\n  const params = new URLSearchParams(location);\n\n  const code = params.get(\"code\");\n  if (code) {\n    return { successful: true, code };\n  }\n\n  const error = params.get(\"error\");\n  const error_description = params.get(\"error_description\");\n  const error_uri = params.get(\"error_uri\");\n\n  if (error) {\n    return { successful: false, error, error_description, error_uri };\n  }\n\n  return {\n    successful: false,\n    error: \"invalid_request\",\n    error_description: \"Missing code or error in response\",\n    error_uri: null,\n  };\n};\n\n/**\n * Generate a random state for the OAuth 2.0 flow.\n *\n * @returns A random state for the OAuth 2.0 flow.\n */\nexport const generateOAuthState = () => {\n  // Generate a random state\n  const array = new Uint8Array(32);\n  crypto.getRandomValues(array);\n  return Array.from(array, (byte) => byte.toString(16).padStart(2, \"0\")).join(\n    \"\",\n  );\n};\n\n/**\n * Generates a human-readable error description from OAuth callback error parameters\n * @param params OAuth error callback parameters containing error details\n * @returns Formatted multiline error message with error code, description, and optional URI\n */\nexport const generateOAuthErrorDescription = (\n  params: Extract<CallbackParams, { successful: false }>,\n): string => {\n  const error = params.error;\n  const errorDescription = params.error_description;\n  const errorUri = params.error_uri;\n\n  return [\n    `Error: ${error}.`,\n    errorDescription ? `Details: ${errorDescription}.` : \"\",\n    errorUri ? `More info: ${errorUri}.` : \"\",\n  ]\n    .filter(Boolean)\n    .join(\"\\n\");\n};\n\n/**\n * Returns the primary OAuth authorization server metadata discovery URL\n * for a given authorization server URL, including tenant path handling.\n */\nexport const getAuthorizationServerMetadataDiscoveryUrl = (\n  authorizationServerUrl: string | URL,\n): string => {\n  const url =\n    typeof authorizationServerUrl === \"string\"\n      ? new URL(authorizationServerUrl)\n      : authorizationServerUrl;\n  const hasPath = url.pathname !== \"/\";\n\n  if (!hasPath) {\n    return new URL(\"/.well-known/oauth-authorization-server\", url.origin).href;\n  }\n\n  // Strip trailing slash to avoid double slashes in tenant-aware discovery URLs.\n  const pathname = url.pathname.endsWith(\"/\")\n    ? url.pathname.slice(0, -1)\n    : url.pathname;\n\n  return new URL(\n    `/.well-known/oauth-authorization-server${pathname}`,\n    url.origin,\n  ).href;\n};\n"
  },
  {
    "path": "client/src/utils/paramUtils.ts",
    "content": "import type { JsonSchemaType } from \"./jsonUtils\";\n\n/**\n * Cleans parameters by removing undefined, null, and empty string values for optional fields\n * while preserving all values for required fields and fields with explicit default values.\n *\n * @param params - The parameters object to clean\n * @param schema - The JSON schema defining which fields are required\n * @returns Cleaned parameters object with optional empty fields omitted\n */\nexport function cleanParams(\n  params: Record<string, unknown>,\n  schema: JsonSchemaType,\n): Record<string, unknown> {\n  const cleaned: Record<string, unknown> = {};\n  const required = schema.required || [];\n  const properties = schema.properties || {};\n\n  for (const [key, value] of Object.entries(params)) {\n    const isFieldRequired = required.includes(key);\n    const fieldSchema = properties[key] as JsonSchemaType | undefined;\n\n    // Check if the field has an explicit default value\n    const hasDefault = fieldSchema && \"default\" in fieldSchema;\n    const defaultValue = hasDefault ? fieldSchema.default : undefined;\n\n    if (isFieldRequired) {\n      // Required fields: always include, even if empty string or falsy\n      cleaned[key] = value;\n    } else if (hasDefault && value === defaultValue) {\n      // Field has a default value and current value matches it - preserve it\n      // This is important for cases like default: null\n      cleaned[key] = value;\n    } else {\n      // Optional fields: only include if they have meaningful values\n      if (value !== undefined && value !== \"\" && value !== null) {\n        cleaned[key] = value;\n      }\n      // Empty strings, undefined, null for optional fields → omit completely\n    }\n  }\n\n  return cleaned;\n}\n"
  },
  {
    "path": "client/src/utils/schemaUtils.ts",
    "content": "import type { JsonValue, JsonSchemaType, JsonObject } from \"./jsonUtils\";\nimport Ajv from \"ajv\";\nimport type { ValidateFunction } from \"ajv\";\nimport type { Tool, JSONRPCMessage } from \"@modelcontextprotocol/sdk/types.js\";\nimport { isJSONRPCRequest } from \"@modelcontextprotocol/sdk/types.js\";\n\nconst ajv = new Ajv();\n\n// Cache for compiled validators\nconst toolOutputValidators = new Map<string, ValidateFunction>();\n\n/**\n * Compiles and caches output schema validators for a list of tools\n * Following the same pattern as SDK's Client.cacheToolOutputSchemas\n * @param tools Array of tools that may have output schemas\n */\nexport function cacheToolOutputSchemas(tools: Tool[]): void {\n  toolOutputValidators.clear();\n  for (const tool of tools) {\n    if (tool.outputSchema) {\n      try {\n        const validator = ajv.compile(tool.outputSchema);\n        toolOutputValidators.set(tool.name, validator);\n      } catch (error) {\n        console.warn(\n          `Failed to compile output schema for tool ${tool.name}:`,\n          error,\n        );\n      }\n    }\n  }\n}\n\n/**\n * Gets the cached output schema validator for a tool\n * Following the same pattern as SDK's Client.getToolOutputValidator\n * @param toolName Name of the tool\n * @returns The compiled validator function, or undefined if not found\n */\nexport function getToolOutputValidator(\n  toolName: string,\n): ValidateFunction | undefined {\n  return toolOutputValidators.get(toolName);\n}\n\n/**\n * Validates structured content against a tool's output schema\n * Returns validation result with detailed error messages\n * @param toolName Name of the tool\n * @param structuredContent The structured content to validate\n * @returns An object with isValid boolean and optional error message\n */\nexport function validateToolOutput(\n  toolName: string,\n  structuredContent: unknown,\n): { isValid: boolean; error?: string } {\n  const validator = getToolOutputValidator(toolName);\n  if (!validator) {\n    return { isValid: true }; // No validator means no schema to validate against\n  }\n\n  const isValid = validator(structuredContent);\n  if (!isValid) {\n    return {\n      isValid: false,\n      error: ajv.errorsText(validator.errors),\n    };\n  }\n\n  return { isValid: true };\n}\n\n/**\n * Checks if a tool has an output schema\n * @param toolName Name of the tool\n * @returns true if the tool has an output schema\n */\nexport function hasOutputSchema(toolName: string): boolean {\n  return toolOutputValidators.has(toolName);\n}\n\n/**\n * Generates a default value based on a JSON schema type\n * @param schema The JSON schema definition\n * @param propertyName Optional property name for checking if it's required in parent schema\n * @param parentSchema Optional parent schema to check required array\n * @returns A default value matching the schema type\n */\nexport function generateDefaultValue(\n  schema: JsonSchemaType,\n  propertyName?: string,\n  parentSchema?: JsonSchemaType,\n): JsonValue {\n  if (\"default\" in schema && schema.default !== undefined) {\n    return schema.default;\n  }\n\n  // Check if this property is required in the parent schema\n  const isRequired =\n    propertyName && parentSchema\n      ? isPropertyRequired(propertyName, parentSchema)\n      : false;\n  const isRootSchema = propertyName === undefined && parentSchema === undefined;\n\n  switch (schema.type) {\n    case \"string\":\n      return isRequired ? \"\" : undefined;\n    case \"number\":\n    case \"integer\":\n      return isRequired ? 0 : undefined;\n    case \"boolean\":\n      return isRequired ? false : undefined;\n    case \"array\":\n      return isRequired ? [] : undefined;\n    case \"object\": {\n      if (!schema.properties) {\n        return isRequired || isRootSchema ? {} : undefined;\n      }\n\n      const obj: JsonObject = {};\n      // Include required properties OR optional properties that declare a default\n      Object.entries(schema.properties).forEach(([key, prop]) => {\n        const hasExplicitDefault =\n          \"default\" in prop && (prop as JsonSchemaType).default !== undefined;\n        if (isPropertyRequired(key, schema) || hasExplicitDefault) {\n          const value = generateDefaultValue(prop, key, schema);\n          if (value !== undefined) {\n            obj[key] = value;\n          }\n        }\n      });\n\n      if (Object.keys(obj).length === 0) {\n        return isRequired || isRootSchema ? {} : undefined;\n      }\n      return obj;\n    }\n    case \"null\":\n      return null;\n    default:\n      return undefined;\n  }\n}\n\n/**\n * Helper function to check if a property is required in a schema\n * @param propertyName The name of the property to check\n * @param schema The parent schema containing the required array\n * @returns true if the property is required, false otherwise\n */\nexport function isPropertyRequired(\n  propertyName: string,\n  schema: JsonSchemaType,\n): boolean {\n  return schema.required?.includes(propertyName) ?? false;\n}\n\n/**\n * Resolves $ref references in JSON schema\n * @param schema The schema that may contain $ref\n * @param rootSchema The root schema to resolve references against\n * @param visitedRefs Optional set of visited $ref paths to detect circular references\n * @returns The resolved schema without $ref\n */\nexport function resolveRef(\n  schema: JsonSchemaType,\n  rootSchema: JsonSchemaType,\n  visitedRefs: Set<string> = new Set(),\n): JsonSchemaType {\n  if (!schema) return schema;\n\n  if (!(\"$ref\" in schema) || !schema.$ref) {\n    // Recursively resolve $ref in anyOf (and other nested structures)\n    if (schema.anyOf && Array.isArray(schema.anyOf)) {\n      const resolvedAnyOf = schema.anyOf.map((item) => {\n        if (typeof item === \"object\" && item !== null) {\n          return resolveRef(item, rootSchema, visitedRefs);\n        }\n        return item;\n      });\n      return {\n        ...schema,\n        anyOf: resolvedAnyOf,\n      };\n    }\n    return schema;\n  }\n\n  const ref = schema.$ref;\n\n  // Handle all #/ formats (#/properties/, #/$defs/, etc.)\n  if (ref.startsWith(\"#/\")) {\n    // Check for circular reference\n    if (visitedRefs.has(ref)) {\n      console.warn(`Circular reference detected: ${ref}`);\n      return schema;\n    }\n\n    // Add current ref to visited set\n    visitedRefs.add(ref);\n\n    const path = ref.substring(2).split(\"/\");\n    let current: unknown = rootSchema;\n\n    for (const segment of path) {\n      if (\n        current &&\n        typeof current === \"object\" &&\n        current !== null &&\n        segment in current\n      ) {\n        current = (current as Record<string, unknown>)[segment];\n      } else {\n        // If reference cannot be resolved, return the original schema\n        visitedRefs.delete(ref); // Clean up on failure\n        console.warn(`Could not resolve $ref: ${ref}`);\n        return schema;\n      }\n    }\n\n    const resolved = current as JsonSchemaType;\n\n    // Recursively resolve nested structures (anyOf, oneOf, items, properties)\n    return resolveRef(resolved, rootSchema, visitedRefs);\n  }\n\n  // For other types of references, return the original schema\n  console.warn(`Unsupported $ref format: ${ref}`);\n  return schema;\n}\n\n/**\n * Normalizes union types (like string|null from FastMCP) to simple types for form rendering\n * @param schema The JSON schema to normalize\n * @returns A normalized schema or the original schema\n */\nexport function normalizeUnionType(schema: JsonSchemaType): JsonSchemaType {\n  // Handle anyOf with exactly 2 items (type and null) - unified handling\n  // Preserves enum and other properties automatically\n  if (\n    schema.anyOf &&\n    schema.anyOf.length === 2 &&\n    schema.anyOf.some((t) => (t as JsonSchemaType).type === \"null\")\n  ) {\n    const nonNullItem = schema.anyOf.find((t) => {\n      const item = t as JsonSchemaType;\n      return item?.type !== \"null\";\n    }) as JsonSchemaType;\n\n    // Only process if non-null item has type or enum\n    if (nonNullItem?.type || nonNullItem?.enum) {\n      return {\n        ...schema,\n        ...nonNullItem,\n        type: nonNullItem?.type || (nonNullItem?.enum ? \"string\" : undefined),\n        nullable: true,\n        anyOf: undefined,\n      };\n    }\n  }\n\n  // Handle array type with exactly string and null\n  if (\n    Array.isArray(schema.type) &&\n    schema.type.length === 2 &&\n    schema.type.includes(\"string\") &&\n    schema.type.includes(\"null\")\n  ) {\n    return { ...schema, type: \"string\", nullable: true };\n  }\n\n  // Handle array type with exactly boolean and null\n  if (\n    Array.isArray(schema.type) &&\n    schema.type.length === 2 &&\n    schema.type.includes(\"boolean\") &&\n    schema.type.includes(\"null\")\n  ) {\n    return { ...schema, type: \"boolean\", nullable: true };\n  }\n\n  // Handle array type with exactly number and null\n  if (\n    Array.isArray(schema.type) &&\n    schema.type.length === 2 &&\n    schema.type.includes(\"number\") &&\n    schema.type.includes(\"null\")\n  ) {\n    return { ...schema, type: \"number\", nullable: true };\n  }\n\n  // Handle array type with exactly integer and null\n  if (\n    Array.isArray(schema.type) &&\n    schema.type.length === 2 &&\n    schema.type.includes(\"integer\") &&\n    schema.type.includes(\"null\")\n  ) {\n    return { ...schema, type: \"integer\", nullable: true };\n  }\n\n  return schema;\n}\n\n/**\n * Formats a field key into a human-readable label\n * @param key The field key to format\n * @returns A formatted label string\n */\nexport function formatFieldLabel(key: string): string {\n  return key\n    .replace(/([A-Z])/g, \" $1\") // Insert space before capital letters\n    .replace(/_/g, \" \") // Replace underscores with spaces\n    .replace(/^\\w/, (c) => c.toUpperCase()); // Capitalize first letter\n}\n\n/**\n * Resolves `$ref` references in a JSON-RPC \"elicitation/create\" message's `requestedSchema` field\n * @param message The JSON-RPC message that may contain $ref references\n * @returns A new message with resolved $ref references, or the original message if no resolution is needed\n */\nexport function resolveRefsInMessage(message: JSONRPCMessage): JSONRPCMessage {\n  if (!isJSONRPCRequest(message) || !message.params?.requestedSchema) {\n    return message;\n  }\n\n  const requestedSchema = message.params.requestedSchema as JsonSchemaType;\n\n  if (!requestedSchema?.properties) {\n    return message;\n  }\n\n  const resolvedMessage = {\n    ...message,\n    params: {\n      ...message.params,\n      requestedSchema: {\n        ...requestedSchema,\n        properties: Object.fromEntries(\n          Object.entries(requestedSchema.properties).map(\n            ([key, propSchema]) => {\n              const resolved = resolveRef(propSchema, requestedSchema);\n              const normalized = normalizeUnionType(resolved);\n              return [key, normalized];\n            },\n          ),\n        ),\n      },\n    },\n  };\n\n  return resolvedMessage;\n}\n"
  },
  {
    "path": "client/src/utils/urlValidation.ts",
    "content": "/**\n * Validates that a URL is safe for redirection.\n * Only allows HTTP and HTTPS protocols to prevent XSS attacks.\n *\n * @param url - The URL string to validate\n * @throws Error if the URL has an unsafe protocol\n */\nexport function validateRedirectUrl(url: string | URL): void {\n  try {\n    const parsedUrl = new URL(url);\n    if (parsedUrl.protocol !== \"http:\" && parsedUrl.protocol !== \"https:\") {\n      throw new Error(\"Authorization URL must be HTTP or HTTPS\");\n    }\n  } catch (error) {\n    if (\n      error instanceof Error &&\n      error.message === \"Authorization URL must be HTTP or HTTPS\"\n    ) {\n      throw error;\n    }\n    // If URL parsing fails, it's also invalid\n    throw new Error(`Invalid URL: ${url}`);\n  }\n}\n"
  },
  {
    "path": "client/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "client/tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nimport animate from \"tailwindcss-animate\";\nexport default {\n  darkMode: [\"class\"],\n  content: [\"./index.html\", \"./src/**/*.{js,ts,jsx,tsx}\"],\n  theme: {\n    extend: {\n      borderRadius: {\n        lg: \"var(--radius)\",\n        md: \"calc(var(--radius) - 2px)\",\n        sm: \"calc(var(--radius) - 4px)\",\n      },\n      colors: {\n        background: \"hsl(var(--background))\",\n        foreground: \"hsl(var(--foreground))\",\n        card: {\n          DEFAULT: \"hsl(var(--card))\",\n          foreground: \"hsl(var(--card-foreground))\",\n        },\n        popover: {\n          DEFAULT: \"hsl(var(--popover))\",\n          foreground: \"hsl(var(--popover-foreground))\",\n        },\n        primary: {\n          DEFAULT: \"hsl(var(--primary))\",\n          foreground: \"hsl(var(--primary-foreground))\",\n        },\n        secondary: {\n          DEFAULT: \"hsl(var(--secondary))\",\n          foreground: \"hsl(var(--secondary-foreground))\",\n        },\n        muted: {\n          DEFAULT: \"hsl(var(--muted))\",\n          foreground: \"hsl(var(--muted-foreground))\",\n        },\n        accent: {\n          DEFAULT: \"hsl(var(--accent))\",\n          foreground: \"hsl(var(--accent-foreground))\",\n        },\n        destructive: {\n          DEFAULT: \"hsl(var(--destructive))\",\n          foreground: \"hsl(var(--destructive-foreground))\",\n        },\n        border: \"hsl(var(--border))\",\n        input: \"hsl(var(--input))\",\n        ring: \"hsl(var(--ring))\",\n        chart: {\n          1: \"hsl(var(--chart-1))\",\n          2: \"hsl(var(--chart-2))\",\n          3: \"hsl(var(--chart-3))\",\n          4: \"hsl(var(--chart-4))\",\n          5: \"hsl(var(--chart-5))\",\n        },\n      },\n    },\n  },\n  plugins: [animate],\n};\n"
  },
  {
    "path": "client/tsconfig.app.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    },\n\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"isolatedModules\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"resolveJsonModule\": true,\n    \"types\": [\"jest\", \"@testing-library/jest-dom\", \"node\"]\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.test.ts\", \"src/**/*.test.tsx\", \"src/**/__tests__/**\"]\n}\n"
  },
  {
    "path": "client/tsconfig.jest.json",
    "content": "{\n  \"extends\": \"./tsconfig.app.json\",\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\",\n    \"esModuleInterop\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"node\"\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "client/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.app.json\" },\n    { \"path\": \"./tsconfig.node.json\" }\n  ],\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  }\n}\n"
  },
  {
    "path": "client/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"lib\": [\"ES2023\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"isolatedModules\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "client/vite.config.ts",
    "content": "import react from \"@vitejs/plugin-react\";\nimport path from \"path\";\nimport { defineConfig } from \"vite\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [react()],\n  server: {\n    host: true,\n  },\n  resolve: {\n    alias: {\n      \"@\": path.resolve(__dirname, \"./src\"),\n    },\n  },\n  build: {\n    minify: false,\n    rollupOptions: {\n      output: {\n        manualChunks: undefined,\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@modelcontextprotocol/inspector\",\n  \"version\": \"0.21.1\",\n  \"description\": \"Model Context Protocol inspector\",\n  \"license\": \"SEE LICENSE IN LICENSE\",\n  \"author\": \"Model Context Protocol a Series of LF Projects, LLC.\",\n  \"homepage\": \"https://modelcontextprotocol.io\",\n  \"bugs\": \"https://github.com/modelcontextprotocol/inspector/issues\",\n  \"type\": \"module\",\n  \"bin\": {\n    \"mcp-inspector\": \"cli/build/cli.js\"\n  },\n  \"files\": [\n    \"client/bin\",\n    \"client/dist\",\n    \"server/build\",\n    \"server/static\",\n    \"cli/build\"\n  ],\n  \"workspaces\": [\n    \"client\",\n    \"server\",\n    \"cli\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build-server && npm run build-client && npm run build-cli\",\n    \"build-server\": \"cd server && npm run build\",\n    \"build-client\": \"cd client && npm run build\",\n    \"build-cli\": \"cd cli && npm run build\",\n    \"clean\": \"rimraf ./node_modules ./client/node_modules ./cli/node_modules ./build ./client/dist ./server/build ./cli/build ./package-lock.json && npm install\",\n    \"dev\": \"node client/bin/start.js --dev\",\n    \"dev:windows\": \"node client/bin/start.js --dev\",\n    \"dev:sdk\": \"npm run link:sdk && concurrently \\\"npm run dev\\\" \\\"cd sdk && npm run build:esm:w\\\"\",\n    \"link:sdk\": \"(test -d sdk || ln -sf ${MCP_SDK:-$PWD/../typescript-sdk} sdk) && (cd sdk && npm link && (test -d node_modules || npm i)) && npm link @modelcontextprotocol/sdk\",\n    \"unlink:sdk\": \"(cd sdk && npm unlink -g) && rm sdk && npm unlink @modelcontextprotocol/sdk\",\n    \"start\": \"node client/bin/start.js\",\n    \"start-server\": \"cd server && npm run start\",\n    \"start-client\": \"cd client && npm run preview\",\n    \"test\": \"npm run prettier-check && cd client && npm test\",\n    \"test-cli\": \"cd cli && npm run test\",\n    \"test:e2e\": \"MCP_AUTO_OPEN_ENABLED=false npm run test:e2e --workspace=client\",\n    \"prettier-fix\": \"prettier --write .\",\n    \"prettier-check\": \"prettier --check .\",\n    \"lint\": \"prettier --check . && cd client && npm run lint\",\n    \"prepare\": \"husky && npm run build\",\n    \"publish-all\": \"npm publish --workspaces --access public && npm publish --access public\",\n    \"update-version\": \"node scripts/update-version.js\",\n    \"check-version\": \"node scripts/check-version-consistency.js\"\n  },\n  \"dependencies\": {\n    \"@modelcontextprotocol/inspector-cli\": \"^0.21.1\",\n    \"@modelcontextprotocol/inspector-client\": \"^0.21.1\",\n    \"@modelcontextprotocol/inspector-server\": \"^0.21.1\",\n    \"@modelcontextprotocol/sdk\": \"^1.25.2\",\n    \"concurrently\": \"^9.2.0\",\n    \"node-fetch\": \"^3.3.2\",\n    \"open\": \"^10.2.0\",\n    \"shell-quote\": \"^1.8.3\",\n    \"spawn-rx\": \"^5.1.2\",\n    \"ts-node\": \"^10.9.2\",\n    \"zod\": \"^3.25.76\"\n  },\n  \"devDependencies\": {\n    \"@playwright/test\": \"^1.54.1\",\n    \"@types/jest\": \"^29.5.14\",\n    \"@types/node\": \"^22.17.0\",\n    \"@types/shell-quote\": \"^1.7.5\",\n    \"husky\": \"^9.1.7\",\n    \"jest-fixed-jsdom\": \"^0.0.9\",\n    \"lint-staged\": \"^16.1.5\",\n    \"playwright\": \"^1.56.1\",\n    \"prettier\": \"^3.7.1\",\n    \"rimraf\": \"^6.0.1\",\n    \"typescript\": \"^5.4.2\"\n  },\n  \"overrides\": {\n    \"get-intrinsic\": \"1.3.0\"\n  },\n  \"engines\": {\n    \"node\": \">=22.7.5\"\n  },\n  \"lint-staged\": {\n    \"**/*.{js,ts,jsx,tsx,json,md}\": [\n      \"prettier --write\"\n    ]\n  }\n}\n"
  },
  {
    "path": "sample-config.json",
    "content": "{\n  \"mcpServers\": {\n    \"everything\": {\n      \"command\": \"npx\",\n      \"args\": [\"@modelcontextprotocol/server-everything\"],\n      \"env\": {\n        \"HELLO\": \"Hello MCP!\"\n      }\n    },\n    \"myserver\": {\n      \"command\": \"node\",\n      \"args\": [\"build/index.js\", \"arg1\", \"arg2\"],\n      \"env\": {\n        \"KEY\": \"value\",\n        \"KEY2\": \"value2\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "scripts/README.md",
    "content": "# Version Management Scripts\n\nThis directory contains scripts for managing version consistency across the monorepo.\n\n## Scripts\n\n### update-version.js\n\nUpdates the version across all package.json files in the monorepo and updates package-lock.json.\n\n**Usage:**\n\n```bash\nnpm run update-version <new-version>\n# Example:\nnpm run update-version 0.14.3\n```\n\nThis script will:\n\n1. Update the version in all package.json files (root, client, server, cli)\n2. Update workspace dependencies in the root package.json\n3. Run `npm install` to update package-lock.json\n4. Provide next steps for committing and tagging\n\n### check-version-consistency.js\n\nChecks that all packages have consistent versions and that package-lock.json is up to date.\n\n**Usage:**\n\n```bash\nnpm run check-version\n```\n\nThis script checks:\n\n1. All package.json files have the same version\n2. Workspace dependencies in root package.json match the current version\n3. package-lock.json version matches package.json\n4. Workspace packages in package-lock.json have the correct versions\n\nThis check runs automatically in CI on every PR and push to main.\n\n## CI Integration\n\nThe version consistency check is integrated into the GitHub Actions workflow (`.github/workflows/main.yml`) and will fail the build if:\n\n- Package versions are inconsistent\n- package-lock.json is out of sync\n\n## Common Workflows\n\n### Bumping version for a release:\n\n```bash\n# Update to new version\nnpm run update-version 0.15.0\n\n# Verify everything is correct\nnpm run check-version\n\n# Commit the changes\ngit add -A\ngit commit -m \"chore: bump version to 0.15.0\"\n\n# Create a tag\ngit tag 0.15.0\n\n# Push changes and tag\ngit push && git push --tags\n```\n"
  },
  {
    "path": "scripts/check-version-consistency.js",
    "content": "#!/usr/bin/env node\n\nimport fs from \"fs\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n/**\n * Checks version consistency across all package.json files in the monorepo\n * Exits with code 1 if versions are inconsistent\n * Usage: node scripts/check-version-consistency.js\n */\n\nconsole.log(\"🔍 Checking version consistency across packages...\\n\");\n\n// List of package.json files to check\nconst packagePaths = [\n  \"package.json\",\n  \"client/package.json\",\n  \"server/package.json\",\n  \"cli/package.json\",\n];\n\nconst versions = new Map();\nconst errors = [];\n\n// Read version from each package.json\npackagePaths.forEach((packagePath) => {\n  const fullPath = path.join(__dirname, \"..\", packagePath);\n\n  if (!fs.existsSync(fullPath)) {\n    console.warn(`⚠️  Skipping ${packagePath} - file not found`);\n    return;\n  }\n\n  try {\n    const packageJson = JSON.parse(fs.readFileSync(fullPath, \"utf8\"));\n    const version = packageJson.version;\n    const packageName = packageJson.name || packagePath;\n\n    versions.set(packagePath, {\n      name: packageName,\n      version: version,\n      dependencies: packageJson.dependencies || {},\n    });\n\n    console.log(`📦 ${packagePath}:`);\n    console.log(`   Name: ${packageName}`);\n    console.log(`   Version: ${version}`);\n  } catch (error) {\n    errors.push(`Failed to read ${packagePath}: ${error.message}`);\n  }\n});\n\nif (errors.length > 0) {\n  console.error(\"\\n❌ Errors occurred while reading package files:\");\n  errors.forEach((error) => console.error(`   - ${error}`));\n  process.exit(1);\n}\n\n// Check if all versions match\nconst allVersions = Array.from(versions.values()).map((v) => v.version);\nconst uniqueVersions = [...new Set(allVersions)];\n\nconsole.log(\"\\n📊 Version Summary:\");\nconsole.log(`   Total packages: ${versions.size}`);\nconsole.log(`   Unique versions: ${uniqueVersions.length}`);\n\nif (uniqueVersions.length > 1) {\n  console.error(\"\\n❌ Version mismatch detected!\");\n  console.error(\"   Found versions: \" + uniqueVersions.join(\", \"));\n\n  console.error(\"\\n   Package versions:\");\n  versions.forEach((info, path) => {\n    console.error(`   - ${path}: ${info.version}`);\n  });\n} else {\n  console.log(`   ✅ All packages are at version: ${uniqueVersions[0]}`);\n}\n\n// Check workspace dependencies in root package.json\nconst rootPackage = versions.get(\"package.json\");\nif (rootPackage) {\n  console.log(\"\\n🔗 Checking workspace dependencies...\");\n  const expectedVersion = rootPackage.version;\n  let dependencyErrors = false;\n\n  Object.entries(rootPackage.dependencies).forEach(([dep, version]) => {\n    if (dep.startsWith(\"@modelcontextprotocol/inspector-\")) {\n      const expectedDepVersion = `^${expectedVersion}`;\n      if (version !== expectedDepVersion) {\n        console.error(\n          `   ❌ ${dep}: ${version} (expected ${expectedDepVersion})`,\n        );\n        dependencyErrors = true;\n      } else {\n        console.log(`   ✅ ${dep}: ${version}`);\n      }\n    }\n  });\n\n  if (dependencyErrors) {\n    errors.push(\"Workspace dependency versions do not match package versions\");\n  }\n}\n\n// Check if package-lock.json is up to date\nconsole.log(\"\\n🔒 Checking package-lock.json...\");\nconst lockPath = path.join(__dirname, \"..\", \"package-lock.json\");\nlet lockFileError = false;\n\nif (!fs.existsSync(lockPath)) {\n  console.error(\"   ❌ package-lock.json not found\");\n  lockFileError = true;\n} else {\n  try {\n    const lockFile = JSON.parse(fs.readFileSync(lockPath, \"utf8\"));\n    const lockVersion = lockFile.version;\n    const expectedVersion = rootPackage?.version || uniqueVersions[0];\n\n    if (lockVersion !== expectedVersion) {\n      console.error(\n        `   ❌ package-lock.json version (${lockVersion}) does not match package.json version (${expectedVersion})`,\n      );\n      lockFileError = true;\n    } else {\n      console.log(`   ✅ package-lock.json version matches: ${lockVersion}`);\n    }\n\n    // Check workspace package versions in lock file\n    if (lockFile.packages) {\n      const workspacePackages = [\n        { path: \"client\", name: \"@modelcontextprotocol/inspector-client\" },\n        { path: \"server\", name: \"@modelcontextprotocol/inspector-server\" },\n        { path: \"cli\", name: \"@modelcontextprotocol/inspector-cli\" },\n      ];\n\n      workspacePackages.forEach(({ path, name }) => {\n        const lockPkgPath = lockFile.packages[path];\n        if (lockPkgPath && lockPkgPath.version !== expectedVersion) {\n          console.error(\n            `   ❌ ${name} in lock file: ${lockPkgPath.version} (expected ${expectedVersion})`,\n          );\n          lockFileError = true;\n        }\n      });\n    }\n  } catch (error) {\n    console.error(`   ❌ Failed to parse package-lock.json: ${error.message}`);\n    lockFileError = true;\n  }\n}\n\n// Final result\nconsole.log(\"\\n🎯 Result:\");\nif (uniqueVersions.length === 1 && errors.length === 0 && !lockFileError) {\n  console.log(\"   ✅ Version consistency check passed!\");\n  process.exit(0);\n} else {\n  console.error(\"   ❌ Version consistency check failed!\");\n  if (uniqueVersions.length > 1) {\n    console.error(\"   - Package versions are not consistent\");\n  }\n  if (errors.length > 0) {\n    console.error(\"   - \" + errors.join(\"\\n   - \"));\n  }\n  if (lockFileError) {\n    console.error(\"   - package-lock.json is out of sync\");\n  }\n  console.error(\n    '\\n💡 Run \"npm run update-version <new-version>\" to fix version inconsistencies',\n  );\n  console.error('   or run \"npm install\" to update package-lock.json');\n  process.exit(1);\n}\n"
  },
  {
    "path": "scripts/update-version.js",
    "content": "#!/usr/bin/env node\n\nimport fs from \"fs\";\nimport path from \"path\";\nimport { execSync } from \"child_process\";\nimport { fileURLToPath } from \"url\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n/**\n * Updates version across all package.json files in the monorepo\n * Usage: node scripts/update-version.js <new-version>\n * Example: node scripts/update-version.js 0.14.2\n */\n\nconst newVersion = process.argv[2];\n\nif (!newVersion) {\n  console.error(\"❌ Please provide a version number\");\n  console.error(\"Usage: node scripts/update-version.js <new-version>\");\n  console.error(\"Example: node scripts/update-version.js 0.14.2\");\n  process.exit(1);\n}\n\n// Validate version format\nconst versionRegex = /^\\d+\\.\\d+\\.\\d+(-[\\w.]+)?$/;\nif (!versionRegex.test(newVersion)) {\n  console.error(\n    \"❌ Invalid version format. Please use semantic versioning (e.g., 1.2.3 or 1.2.3-beta.1)\",\n  );\n  process.exit(1);\n}\n\nconsole.log(`🔄 Updating all packages to version ${newVersion}...`);\n\n// List of package.json files to update\nconst packagePaths = [\n  \"package.json\",\n  \"client/package.json\",\n  \"server/package.json\",\n  \"cli/package.json\",\n];\n\nconst updatedFiles = [];\n\n// Update version in each package.json\npackagePaths.forEach((packagePath) => {\n  const fullPath = path.join(__dirname, \"..\", packagePath);\n\n  if (!fs.existsSync(fullPath)) {\n    console.warn(`⚠️  Skipping ${packagePath} - file not found`);\n    return;\n  }\n\n  try {\n    const packageJson = JSON.parse(fs.readFileSync(fullPath, \"utf8\"));\n    const oldVersion = packageJson.version;\n    packageJson.version = newVersion;\n\n    // Update workspace dependencies in root package.json\n    if (packagePath === \"package.json\" && packageJson.dependencies) {\n      Object.keys(packageJson.dependencies).forEach((dep) => {\n        if (dep.startsWith(\"@modelcontextprotocol/inspector-\")) {\n          packageJson.dependencies[dep] = `^${newVersion}`;\n        }\n      });\n    }\n\n    fs.writeFileSync(fullPath, JSON.stringify(packageJson, null, 2) + \"\\n\");\n    updatedFiles.push(packagePath);\n    console.log(\n      `✅ Updated ${packagePath} from ${oldVersion} to ${newVersion}`,\n    );\n  } catch (error) {\n    console.error(`❌ Failed to update ${packagePath}:`, error.message);\n    process.exit(1);\n  }\n});\n\nconsole.log(\"\\n📝 Summary:\");\nconsole.log(`Updated ${updatedFiles.length} files to version ${newVersion}`);\n\n// Update package-lock.json\nconsole.log(\"\\n🔒 Updating package-lock.json...\");\ntry {\n  execSync(\"npm install\", { stdio: \"inherit\" });\n  console.log(\"✅ package-lock.json updated successfully\");\n} catch (error) {\n  console.error(\"❌ Failed to update package-lock.json:\", error.message);\n  console.error('Please run \"npm install\" manually');\n  process.exit(1);\n}\n\nconsole.log(\"\\n✨ Version update complete!\");\nconsole.log(\"\\nNext steps:\");\nconsole.log(\"1. Review the changes: git diff\");\nconsole.log(\n  '2. Commit the changes: git add -A && git commit -m \"chore: bump version to ' +\n    newVersion +\n    '\"',\n);\nconsole.log(\"3. Create a git tag: git tag v\" + newVersion);\nconsole.log(\"4. Push changes and tag: git push && git push --tags\");\n"
  },
  {
    "path": "server/LICENSE",
    "content": "The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 (\"Apache-2.0\"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.\n\nContributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License.\n\nNo rights beyond those granted by the applicable original license are conveyed for such contributions.\n\n---\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to the Licensor for inclusion in the Work by the copyright\n      owner or by an individual or Legal Entity authorized to submit on behalf\n      of the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n---\n\nMIT License\n\nCopyright (c) 2024-2025 Model Context Protocol a Series of LF Projects, LLC.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n---\n\nCreative Commons Attribution 4.0 International (CC-BY-4.0)\n\nDocumentation in this project (excluding specifications) is licensed under\nCC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for\nthe full license text.\n"
  },
  {
    "path": "server/package.json",
    "content": "{\n  \"name\": \"@modelcontextprotocol/inspector-server\",\n  \"version\": \"0.21.1\",\n  \"description\": \"Server-side application for the Model Context Protocol inspector\",\n  \"license\": \"SEE LICENSE IN LICENSE\",\n  \"author\": \"Model Context Protocol a Series of LF Projects, LLC.\",\n  \"homepage\": \"https://modelcontextprotocol.io\",\n  \"bugs\": \"https://github.com/modelcontextprotocol/inspector/issues\",\n  \"type\": \"module\",\n  \"bin\": {\n    \"mcp-inspector-server\": \"build/index.js\"\n  },\n  \"files\": [\n    \"build\",\n    \"static\",\n    \"LICENSE\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsc && shx cp -R static build\",\n    \"start\": \"node build/index.js\",\n    \"dev\": \"tsx watch --clear-screen=false src/index.ts\",\n    \"dev:windows\": \"tsx watch --clear-screen=false src/index.ts < NUL\"\n  },\n  \"devDependencies\": {\n    \"@types/cors\": \"^2.8.19\",\n    \"@types/express\": \"^5.0.0\",\n    \"@types/shell-quote\": \"^1.7.5\",\n    \"@types/ws\": \"^8.5.12\",\n    \"tsx\": \"^4.19.0\",\n    \"typescript\": \"^5.6.2\"\n  },\n  \"dependencies\": {\n    \"@modelcontextprotocol/sdk\": \"^1.25.2\",\n    \"cors\": \"^2.8.5\",\n    \"express\": \"^5.1.0\",\n    \"shell-quote\": \"^1.8.3\",\n    \"shx\": \"^0.3.4\",\n    \"spawn-rx\": \"^5.1.2\",\n    \"ws\": \"^8.18.0\",\n    \"zod\": \"^3.25.76\",\n    \"express-rate-limit\": \"^8.2.1\"\n  }\n}\n"
  },
  {
    "path": "server/src/index.ts",
    "content": "#!/usr/bin/env node\n\nimport cors from \"cors\";\nimport { parseArgs } from \"node:util\";\nimport { parse as shellParseArgs } from \"shell-quote\";\nimport nodeFetch, { Headers as NodeHeaders } from \"node-fetch\";\n\n// Type-compatible wrappers for node-fetch to work with browser-style types\nconst fetch = nodeFetch;\nconst Headers = NodeHeaders;\n\nimport {\n  SSEClientTransport,\n  SseError,\n} from \"@modelcontextprotocol/sdk/client/sse.js\";\nimport {\n  StdioClientTransport,\n  getDefaultEnvironment,\n} from \"@modelcontextprotocol/sdk/client/stdio.js\";\nimport {\n  StreamableHTTPClientTransport,\n  StreamableHTTPError,\n} from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { SSEServerTransport } from \"@modelcontextprotocol/sdk/server/sse.js\";\nimport { Transport } from \"@modelcontextprotocol/sdk/shared/transport.js\";\nimport express from \"express\";\nimport rateLimit from \"express-rate-limit\";\nimport { findActualExecutable } from \"spawn-rx\";\nimport mcpProxy from \"./mcpProxy.js\";\nimport { randomUUID, randomBytes, timingSafeEqual } from \"node:crypto\";\nimport { fileURLToPath } from \"url\";\nimport { dirname, join } from \"path\";\nimport { readFileSync } from \"fs\";\n\nconst DEFAULT_MCP_PROXY_LISTEN_PORT = \"6277\";\n\nconst sandboxRateLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000, // 15 minutes\n  max: 100, // limit each IP to 100 /sandbox requests per windowMs\n});\n\nconst defaultEnvironment = {\n  ...getDefaultEnvironment(),\n  ...(process.env.MCP_ENV_VARS ? JSON.parse(process.env.MCP_ENV_VARS) : {}),\n};\n\nconst { values } = parseArgs({\n  args: process.argv.slice(2),\n  options: {\n    env: { type: \"string\", default: \"\" },\n    args: { type: \"string\", default: \"\" },\n    command: { type: \"string\", default: \"\" },\n    transport: { type: \"string\", default: \"\" },\n    \"server-url\": { type: \"string\", default: \"\" },\n  },\n});\n\n/**\n * Helper function to detect 401 Unauthorized errors from various transport types.\n * StreamableHTTPClientTransport throws a generic Error with \"HTTP 401\" in the message\n * when there's no authProvider configured, while SSEClientTransport throws SseError.\n */\nconst is401Error = (error: unknown): boolean => {\n  if (error instanceof SseError && error.code === 401) return true;\n  if (error instanceof StreamableHTTPError && error.code === 401) return true;\n  if (\n    error instanceof Error &&\n    (error.message.includes(\"HTTP 401\") || error.message.includes(\"(401)\"))\n  )\n    return true;\n  return false;\n};\n\n// Function to get HTTP headers.\nconst getHttpHeaders = (req: express.Request): Record<string, string> => {\n  const headers: Record<string, string> = {};\n\n  // Iterate over all headers in the request\n  for (const key in req.headers) {\n    const lowerKey = key.toLowerCase();\n\n    // Check if the header is one we want to forward\n    if (\n      lowerKey.startsWith(\"mcp-\") ||\n      lowerKey === \"authorization\" ||\n      lowerKey === \"last-event-id\"\n    ) {\n      // Exclude the proxy's own authentication header and the Client <-> Proxy session ID header\n      if (lowerKey !== \"x-mcp-proxy-auth\" && lowerKey !== \"mcp-session-id\") {\n        const value = req.headers[key];\n\n        if (typeof value === \"string\") {\n          // If the value is a string, use it directly\n          headers[key] = value;\n        } else if (Array.isArray(value)) {\n          // If the value is an array, use the last element\n          const lastValue = value.at(-1);\n          if (lastValue !== undefined) {\n            headers[key] = lastValue;\n          }\n        }\n        // If value is undefined, it's skipped, which is correct.\n      }\n    }\n  }\n\n  // Handle the custom auth header separately. We expect `x-custom-auth-header`\n  // to be a string containing the name of the actual authentication header.\n  const customAuthHeaderName = req.headers[\"x-custom-auth-header\"];\n  if (typeof customAuthHeaderName === \"string\") {\n    const lowerCaseHeaderName = customAuthHeaderName.toLowerCase();\n    const value = req.headers[lowerCaseHeaderName];\n\n    if (typeof value === \"string\") {\n      headers[customAuthHeaderName] = value;\n    } else if (Array.isArray(value)) {\n      // If the actual auth header was sent multiple times, use the last value.\n      const lastValue = value.at(-1);\n      if (lastValue !== undefined) {\n        headers[customAuthHeaderName] = lastValue;\n      }\n    }\n  }\n\n  // Handle multiple custom headers (new approach)\n  if (req.headers[\"x-custom-auth-headers\"] !== undefined) {\n    try {\n      const customHeaderNames = JSON.parse(\n        req.headers[\"x-custom-auth-headers\"] as string,\n      ) as string[];\n      if (Array.isArray(customHeaderNames)) {\n        customHeaderNames.forEach((headerName) => {\n          const lowerCaseHeaderName = headerName.toLowerCase();\n          if (req.headers[lowerCaseHeaderName] !== undefined) {\n            const value = req.headers[lowerCaseHeaderName];\n            headers[headerName] = Array.isArray(value)\n              ? value[value.length - 1]\n              : value;\n          }\n        });\n      }\n    } catch (error) {\n      console.warn(\"Failed to parse x-custom-auth-headers:\", error);\n    }\n  }\n  return headers;\n};\n\n/**\n * Updates a headers object in-place, preserving the original Accept header.\n * This is necessary to ensure that transports holding a reference to the headers\n * object see the updates.\n * @param currentHeaders The headers object to update.\n * @param newHeaders The new headers to apply.\n */\nconst updateHeadersInPlace = (\n  currentHeaders: Record<string, string>,\n  newHeaders: Record<string, string>,\n) => {\n  // Preserve the Accept header, which is set at transport creation and\n  // is not present in subsequent client requests.\n  const accept = currentHeaders[\"Accept\"];\n\n  // Clear the old headers and apply the new ones.\n  Object.keys(currentHeaders).forEach((key) => delete currentHeaders[key]);\n  Object.assign(currentHeaders, newHeaders);\n\n  // Restore the Accept header.\n  if (accept) {\n    currentHeaders[\"Accept\"] = accept;\n  }\n};\n\nconst app = express();\napp.use(cors());\napp.use((req, res, next) => {\n  res.header(\"Access-Control-Expose-Headers\", \"mcp-session-id\");\n  next();\n});\n\nconst webAppTransports: Map<string, Transport> = new Map<string, Transport>(); // Web app transports by web app sessionId\nconst serverTransports: Map<string, Transport> = new Map<string, Transport>(); // Server Transports by web app sessionId\nconst sessionHeaderHolders: Map<string, { headers: HeadersInit }> = new Map(); // For dynamic header updates\n\n// Use provided token from environment or generate a new one\nconst sessionToken =\n  process.env.MCP_PROXY_AUTH_TOKEN || randomBytes(32).toString(\"hex\");\nconst authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH;\n\n// Origin validation middleware to prevent DNS rebinding attacks\nconst originValidationMiddleware = (\n  req: express.Request,\n  res: express.Response,\n  next: express.NextFunction,\n) => {\n  const origin = req.headers.origin;\n\n  // Default origins based on CLIENT_PORT or use environment variable\n  const clientPort = process.env.CLIENT_PORT || \"6274\";\n  const defaultOrigin = `http://localhost:${clientPort}`;\n  const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(\",\") || [\n    defaultOrigin,\n  ];\n\n  if (origin && !allowedOrigins.includes(origin)) {\n    console.error(`Invalid origin: ${origin}`);\n    res.status(403).json({\n      error: \"Forbidden - invalid origin\",\n      message:\n        \"Request blocked to prevent DNS rebinding attacks. Configure allowed origins via environment variable.\",\n    });\n    return;\n  }\n  next();\n};\n\nconst authMiddleware = (\n  req: express.Request,\n  res: express.Response,\n  next: express.NextFunction,\n) => {\n  if (authDisabled) {\n    return next();\n  }\n\n  const sendUnauthorized = () => {\n    res.status(401).json({\n      error: \"Unauthorized\",\n      message:\n        \"Authentication required. Use the session token shown in the console when starting the server.\",\n    });\n  };\n\n  const authHeader = req.headers[\"x-mcp-proxy-auth\"];\n  const authHeaderValue = Array.isArray(authHeader)\n    ? authHeader[0]\n    : authHeader;\n\n  if (!authHeaderValue || !authHeaderValue.startsWith(\"Bearer \")) {\n    sendUnauthorized();\n    return;\n  }\n\n  const providedToken = authHeaderValue.substring(7); // Remove 'Bearer ' prefix\n  const expectedToken = sessionToken;\n\n  // Convert to buffers for timing-safe comparison\n  const providedBuffer = Buffer.from(providedToken);\n  const expectedBuffer = Buffer.from(expectedToken);\n\n  // Check length first to prevent timing attacks\n  if (providedBuffer.length !== expectedBuffer.length) {\n    sendUnauthorized();\n    return;\n  }\n\n  // Perform timing-safe comparison\n  if (!timingSafeEqual(providedBuffer, expectedBuffer)) {\n    sendUnauthorized();\n    return;\n  }\n\n  next();\n};\n\n/**\n * Converts a Node.js ReadableStream to a web-compatible ReadableStream\n * This is necessary for the EventSource polyfill which expects web streams\n */\nconst createWebReadableStream = (nodeStream: any): ReadableStream => {\n  let closed = false;\n  return new ReadableStream({\n    start(controller) {\n      nodeStream.on(\"data\", (chunk: any) => {\n        if (!closed) {\n          controller.enqueue(chunk);\n        }\n      });\n      nodeStream.on(\"end\", () => {\n        if (!closed) {\n          closed = true;\n          controller.close();\n        }\n      });\n      nodeStream.on(\"error\", (err: any) => {\n        if (!closed) {\n          closed = true;\n          controller.error(err);\n        }\n      });\n    },\n    cancel() {\n      closed = true;\n      nodeStream.destroy();\n    },\n  });\n};\n\n/**\n * Creates a `fetch` function that merges dynamic session headers with the\n * headers from the actual request, ensuring that request-specific headers like\n * `Content-Type` are preserved. For SSE requests, it also converts Node.js\n * streams to web-compatible streams.\n */\nconst createCustomFetch = (headerHolder: { headers: HeadersInit }) => {\n  return async (\n    input: RequestInfo | URL,\n    init?: RequestInit,\n  ): Promise<Response> => {\n    // Determine the headers from the original request/init.\n    // The SDK may pass a Request object or a URL and an init object.\n    const originalHeaders =\n      input instanceof Request ? input.headers : init?.headers;\n\n    // Start with our dynamic session headers.\n    const finalHeaders = new Headers(headerHolder.headers);\n\n    // Merge the SDK's request-specific headers, letting them overwrite.\n    // This is crucial for preserving Content-Type on POST requests.\n    new Headers(originalHeaders).forEach((value, key) => {\n      finalHeaders.set(key, value);\n    });\n\n    // Convert Headers to a plain object for node-fetch compatibility\n    const headersObject: Record<string, string> = {};\n    finalHeaders.forEach((value, key) => {\n      headersObject[key] = value;\n    });\n\n    // Get the response from node-fetch (cast input and init to handle type differences)\n    const response = await fetch(\n      input as any,\n      { ...init, headers: headersObject } as any,\n    );\n\n    // Check if this is an SSE request by looking at the Accept header\n    const acceptHeader = finalHeaders.get(\"Accept\");\n    const isSSE = acceptHeader?.includes(\"text/event-stream\");\n\n    if (isSSE && response.body) {\n      // For SSE requests, we need to convert the Node.js stream to a web ReadableStream\n      // because the EventSource polyfill expects web-compatible streams\n      const webStream = createWebReadableStream(response.body);\n\n      // Create a new response with the web-compatible stream\n      // Convert node-fetch headers to plain object for web Response compatibility\n      const responseHeaders: Record<string, string> = {};\n      response.headers.forEach((value: string, key: string) => {\n        responseHeaders[key] = value;\n      });\n\n      return new Response(webStream, {\n        status: response.status,\n        statusText: response.statusText,\n        headers: responseHeaders,\n      }) as Response;\n    }\n\n    // For non-SSE requests, return the response as-is (cast to handle type differences)\n    return response as unknown as Response;\n  };\n};\n\nconst createTransport = async (\n  req: express.Request,\n): Promise<{\n  transport: Transport;\n  headerHolder?: { headers: HeadersInit };\n}> => {\n  const query = req.query;\n  console.log(\"Query parameters:\", JSON.stringify(query));\n\n  const transportType = query.transportType as string;\n\n  if (transportType === \"stdio\") {\n    const command = (query.command as string).trim();\n    const origArgs = shellParseArgs(query.args as string) as string[];\n    const queryEnv = query.env ? JSON.parse(query.env as string) : {};\n    const env = { ...defaultEnvironment, ...process.env, ...queryEnv };\n\n    const { cmd, args } = findActualExecutable(command, origArgs);\n\n    console.log(`STDIO transport: command=${cmd}, args=${args}`);\n\n    const transport = new StdioClientTransport({\n      command: cmd,\n      args,\n      env,\n      stderr: \"pipe\",\n    });\n\n    await transport.start();\n    return { transport };\n  } else if (transportType === \"sse\") {\n    const url = query.url as string;\n\n    const headers = getHttpHeaders(req);\n    headers[\"Accept\"] = \"text/event-stream\";\n    const headerHolder = { headers };\n\n    console.log(\n      `SSE transport: url=${url}, headers=${JSON.stringify(headers)}`,\n    );\n\n    const transport = new SSEClientTransport(new URL(url), {\n      eventSourceInit: {\n        fetch: createCustomFetch(headerHolder),\n      },\n      requestInit: {\n        headers: headerHolder.headers,\n      },\n    });\n    await transport.start();\n    return { transport, headerHolder };\n  } else if (transportType === \"streamable-http\") {\n    const headers = getHttpHeaders(req);\n    headers[\"Accept\"] = \"text/event-stream, application/json\";\n    const headerHolder = { headers };\n\n    const transport = new StreamableHTTPClientTransport(\n      new URL(query.url as string),\n      {\n        // Pass a custom fetch to inject the latest headers on each request\n        fetch: createCustomFetch(headerHolder),\n      },\n    );\n    await transport.start();\n    return { transport, headerHolder };\n  } else {\n    console.error(`Invalid transport type: ${transportType}`);\n    throw new Error(\"Invalid transport type specified\");\n  }\n};\n\napp.get(\n  \"/mcp\",\n  originValidationMiddleware,\n  authMiddleware,\n  async (req, res) => {\n    const sessionId = req.headers[\"mcp-session-id\"] as string;\n    console.log(`Received GET message for sessionId ${sessionId}`);\n\n    const headerHolder = sessionHeaderHolders.get(sessionId);\n    if (headerHolder) {\n      updateHeadersInPlace(\n        headerHolder.headers as Record<string, string>,\n        getHttpHeaders(req),\n      );\n    }\n\n    try {\n      const transport = webAppTransports.get(\n        sessionId,\n      ) as StreamableHTTPServerTransport;\n      if (!transport) {\n        res.status(404).end(\"Session not found\");\n        return;\n      } else {\n        await transport.handleRequest(req, res);\n      }\n    } catch (error) {\n      console.error(\"Error in /mcp route:\", error);\n      res.status(500).json(error);\n    }\n  },\n);\n\napp.post(\n  \"/mcp\",\n  originValidationMiddleware,\n  authMiddleware,\n  async (req, res) => {\n    const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n\n    if (sessionId) {\n      console.log(`Received POST message for sessionId ${sessionId}`);\n      const headerHolder = sessionHeaderHolders.get(sessionId);\n      if (headerHolder) {\n        updateHeadersInPlace(\n          headerHolder.headers as Record<string, string>,\n          getHttpHeaders(req),\n        );\n      }\n\n      try {\n        const transport = webAppTransports.get(\n          sessionId,\n        ) as StreamableHTTPServerTransport;\n        if (!transport) {\n          res.status(404).end(\"Transport not found for sessionId \" + sessionId);\n        } else {\n          await (transport as StreamableHTTPServerTransport).handleRequest(\n            req,\n            res,\n          );\n        }\n      } catch (error) {\n        console.error(\"Error in /mcp route:\", error);\n        res.status(500).json(error);\n      }\n    } else {\n      console.log(\"New StreamableHttp connection request\");\n      try {\n        const { transport: serverTransport, headerHolder } =\n          await createTransport(req);\n\n        const webAppTransport = new StreamableHTTPServerTransport({\n          sessionIdGenerator: randomUUID,\n          onsessioninitialized: (sessionId) => {\n            webAppTransports.set(sessionId, webAppTransport);\n            serverTransports.set(sessionId, serverTransport!); // eslint-disable-line @typescript-eslint/no-non-null-assertion\n            if (headerHolder) {\n              sessionHeaderHolders.set(sessionId, headerHolder);\n            }\n            console.log(\"Client <-> Proxy  sessionId: \" + sessionId);\n          },\n          onsessionclosed: (sessionId) => {\n            webAppTransports.delete(sessionId);\n            serverTransports.delete(sessionId);\n            sessionHeaderHolders.delete(sessionId);\n          },\n        });\n        console.log(\"Created StreamableHttp client transport\");\n\n        await webAppTransport.start();\n\n        mcpProxy({\n          transportToClient: webAppTransport,\n          transportToServer: serverTransport,\n        });\n\n        await (webAppTransport as StreamableHTTPServerTransport).handleRequest(\n          req,\n          res,\n          req.body,\n        );\n      } catch (error) {\n        if (is401Error(error)) {\n          console.error(\n            \"Received 401 Unauthorized from MCP server:\",\n            error instanceof Error ? error.message : error,\n          );\n          res.status(401).json(error);\n          return;\n        }\n        console.error(\"Error in /mcp POST route:\", error);\n        res.status(500).json(error);\n      }\n    }\n  },\n);\n\napp.delete(\n  \"/mcp\",\n  originValidationMiddleware,\n  authMiddleware,\n  async (req, res) => {\n    const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n    console.log(`Received DELETE message for sessionId ${sessionId}`);\n    if (sessionId) {\n      try {\n        const serverTransport = serverTransports.get(\n          sessionId,\n        ) as StreamableHTTPClientTransport;\n        if (!serverTransport) {\n          res.status(404).end(\"Transport not found for sessionId \" + sessionId);\n        } else {\n          await serverTransport.terminateSession();\n          await serverTransport.close();\n          webAppTransports.delete(sessionId);\n          serverTransports.delete(sessionId);\n          sessionHeaderHolders.delete(sessionId);\n          console.log(`Transports removed for sessionId ${sessionId}`);\n        }\n        res.status(200).end();\n      } catch (error) {\n        console.error(\"Error in /mcp route:\", error);\n        res.status(500).json(error);\n      }\n    }\n  },\n);\n\napp.get(\n  \"/stdio\",\n  originValidationMiddleware,\n  authMiddleware,\n  async (req, res) => {\n    try {\n      console.log(\"New STDIO connection request\");\n      const { transport: serverTransport } = await createTransport(req);\n\n      const proxyFullAddress = (req.query.proxyFullAddress as string) || \"\";\n      const prefix = proxyFullAddress || \"\";\n      const endpoint = `${prefix}/message`;\n\n      const webAppTransport = new SSEServerTransport(endpoint, res);\n      webAppTransports.set(webAppTransport.sessionId, webAppTransport);\n      console.log(\"Created client transport\");\n\n      serverTransports.set(webAppTransport.sessionId, serverTransport);\n      console.log(\"Created server transport\");\n\n      await webAppTransport.start();\n\n      (serverTransport as StdioClientTransport).stderr!.on(\"data\", (chunk) => {\n        if (chunk.toString().includes(\"MODULE_NOT_FOUND\")) {\n          // Server command not found, remove transports\n          const message = \"Command not found, transports removed\";\n          webAppTransport.send({\n            jsonrpc: \"2.0\",\n            method: \"notifications/message\",\n            params: {\n              level: \"emergency\",\n              logger: \"proxy\",\n              data: {\n                message,\n              },\n            },\n          });\n          webAppTransport.close();\n          serverTransport.close();\n          webAppTransports.delete(webAppTransport.sessionId);\n          serverTransports.delete(webAppTransport.sessionId);\n          sessionHeaderHolders.delete(webAppTransport.sessionId);\n          console.error(message);\n        } else {\n          // Inspect message and attempt to assign a RFC 5424 Syslog Protocol level\n          let level;\n          let message = chunk.toString().trim();\n          let ucMsg = chunk.toString().toUpperCase();\n          if (ucMsg.includes(\"DEBUG\")) {\n            level = \"debug\";\n          } else if (ucMsg.includes(\"INFO\")) {\n            level = \"info\";\n          } else if (ucMsg.includes(\"NOTICE\")) {\n            level = \"notice\";\n          } else if (ucMsg.includes(\"WARN\")) {\n            level = \"warning\";\n          } else if (ucMsg.includes(\"ERROR\")) {\n            level = \"error\";\n          } else if (ucMsg.includes(\"CRITICAL\")) {\n            level = \"critical\";\n          } else if (ucMsg.includes(\"ALERT\")) {\n            level = \"alert\";\n          } else if (ucMsg.includes(\"EMERGENCY\")) {\n            level = \"emergency\";\n          } else if (ucMsg.includes(\"SIGINT\")) {\n            message = \"SIGINT received. Server shutdown.\";\n            level = \"emergency\";\n          } else if (ucMsg.includes(\"SIGHUP\")) {\n            message = \"SIGHUP received. Server shutdown.\";\n            level = \"emergency\";\n          } else if (ucMsg.includes(\"SIGTERM\")) {\n            message = \"SIGTERM received. Server shutdown.\";\n            level = \"emergency\";\n          } else {\n            level = \"info\";\n          }\n          webAppTransport.send({\n            jsonrpc: \"2.0\",\n            method: \"notifications/message\",\n            params: {\n              level,\n              logger: \"stdio\",\n              data: {\n                message,\n              },\n            },\n          });\n        }\n      });\n\n      mcpProxy({\n        transportToClient: webAppTransport,\n        transportToServer: serverTransport,\n      });\n    } catch (error) {\n      if (is401Error(error)) {\n        console.error(\n          \"Received 401 Unauthorized from MCP server. Authentication failure.\",\n        );\n        res.status(401).json(error);\n        return;\n      }\n      console.error(\"Error in /stdio route:\", error);\n      res.status(500).json(error);\n    }\n  },\n);\n\napp.get(\n  \"/sse\",\n  originValidationMiddleware,\n  authMiddleware,\n  async (req, res) => {\n    try {\n      console.log(\n        \"New SSE connection request. NOTE: The SSE transport is deprecated and has been replaced by StreamableHttp\",\n      );\n      const { transport: serverTransport, headerHolder } =\n        await createTransport(req);\n\n      const proxyFullAddress = (req.query.proxyFullAddress as string) || \"\";\n      const prefix = proxyFullAddress || \"\";\n      const endpoint = `${prefix}/message`;\n\n      const webAppTransport = new SSEServerTransport(endpoint, res);\n      webAppTransports.set(webAppTransport.sessionId, webAppTransport);\n      console.log(\"Created client transport\");\n\n      serverTransports.set(webAppTransport.sessionId, serverTransport!); // eslint-disable-line @typescript-eslint/no-non-null-assertion\n      if (headerHolder) {\n        sessionHeaderHolders.set(webAppTransport.sessionId, headerHolder);\n      }\n      console.log(\"Created server transport\");\n\n      await webAppTransport.start();\n\n      mcpProxy({\n        transportToClient: webAppTransport,\n        transportToServer: serverTransport,\n      });\n    } catch (error) {\n      if (is401Error(error)) {\n        console.error(\n          \"Received 401 Unauthorized from MCP server. Authentication failure.\",\n        );\n        res.status(401).json(error);\n        return;\n      } else if (error instanceof SseError && error.code === 404) {\n        console.error(\n          \"Received 404 not found from MCP server. Does the MCP server support SSE?\",\n        );\n        res.status(404).json(error);\n        return;\n      } else if (JSON.stringify(error).includes(\"ECONNREFUSED\")) {\n        console.error(\"Connection refused. Is the MCP server running?\");\n        res.status(500).json(error);\n      }\n      console.error(\"Error in /sse route:\", error);\n      res.status(500).json(error);\n    }\n  },\n);\n\napp.post(\n  \"/message\",\n  originValidationMiddleware,\n  authMiddleware,\n  async (req, res) => {\n    try {\n      const sessionId = req.query.sessionId as string;\n      console.log(`Received POST message for sessionId ${sessionId}`);\n\n      const headerHolder = sessionHeaderHolders.get(sessionId);\n      if (headerHolder) {\n        updateHeadersInPlace(\n          headerHolder.headers as Record<string, string>,\n          getHttpHeaders(req),\n        );\n      }\n\n      const transport = webAppTransports.get(sessionId) as SSEServerTransport;\n      if (!transport) {\n        res.status(404).end(\"Session not found\");\n        return;\n      }\n      await transport.handlePostMessage(req, res);\n    } catch (error) {\n      console.error(\"Error in /message route:\", error);\n      res.status(500).json(error);\n    }\n  },\n);\n\napp.get(\"/health\", (req, res) => {\n  res.json({\n    status: \"ok\",\n  });\n});\n\napp.get(\"/config\", originValidationMiddleware, authMiddleware, (req, res) => {\n  try {\n    res.json({\n      defaultEnvironment,\n      defaultCommand: values.command,\n      defaultArgs: values.args,\n      defaultTransport: values.transport,\n      defaultServerUrl: values[\"server-url\"],\n    });\n  } catch (error) {\n    console.error(\"Error in /config route:\", error);\n    res.status(500).json(error);\n  }\n});\n\napp.get(\n  \"/sandbox\",\n  sandboxRateLimiter as express.RequestHandler,\n  (req, res) => {\n    const __filename = fileURLToPath(import.meta.url);\n    const __dirname = dirname(__filename);\n    const filePath = join(__dirname, \"..\", \"static\", \"sandbox_proxy.html\");\n    let sandboxHtml;\n\n    try {\n      sandboxHtml = readFileSync(filePath, \"utf-8\");\n    } catch (e) {\n      sandboxHtml = \"MCP Apps sandbox not loaded: \" + e;\n    }\n\n    res.set(\"Cache-Control\", \"no-cache, no-store, max-age=0\");\n    res.send(sandboxHtml);\n  },\n);\n\nconst PORT = parseInt(\n  process.env.SERVER_PORT || DEFAULT_MCP_PROXY_LISTEN_PORT,\n  10,\n);\nconst HOST = process.env.HOST || \"localhost\";\n\nconst server = app.listen(PORT, HOST);\nserver.on(\"listening\", () => {\n  console.log(`⚙️ Proxy server listening on ${HOST}:${PORT}`);\n  if (!authDisabled) {\n    console.log(\n      `🔑 Session token: ${sessionToken}\\n   ` +\n        `Use this token to authenticate requests or set DANGEROUSLY_OMIT_AUTH=true to disable auth`,\n    );\n  } else {\n    console.log(\n      `⚠️  WARNING: Authentication is disabled. This is not recommended.`,\n    );\n  }\n});\nserver.on(\"error\", (err) => {\n  if (err.message.includes(`EADDRINUSE`)) {\n    console.error(`❌  Proxy Server PORT IS IN USE at port ${PORT} ❌ `);\n  } else {\n    console.error(err.message);\n  }\n  process.exit(1);\n});\n"
  },
  {
    "path": "server/src/mcpProxy.ts",
    "content": "import { Transport } from \"@modelcontextprotocol/sdk/shared/transport.js\";\nimport { isJSONRPCRequest } from \"@modelcontextprotocol/sdk/types.js\";\n\nfunction onClientError(error: Error) {\n  console.error(\"Error from inspector client:\", error);\n}\n\nfunction onServerError(error: Error) {\n  if (error?.cause && JSON.stringify(error.cause).includes(\"ECONNREFUSED\")) {\n    console.error(\"Connection refused. Is the MCP server running?\");\n  } else if (error.message && error.message.includes(\"404\")) {\n    console.error(\"Error accessing endpoint (HTTP 404)\");\n  } else {\n    console.error(\"Error from MCP server:\", error);\n  }\n}\n\nexport default function mcpProxy({\n  transportToClient,\n  transportToServer,\n}: {\n  transportToClient: Transport;\n  transportToServer: Transport;\n}) {\n  let transportToClientClosed = false;\n  let transportToServerClosed = false;\n\n  let reportedServerSession = false;\n\n  transportToClient.onmessage = (message) => {\n    transportToServer.send(message).catch((error) => {\n      // Send error response back to client if it was a request (has id) and connection is still open\n      if (isJSONRPCRequest(message) && !transportToClientClosed) {\n        const errorResponse = {\n          jsonrpc: \"2.0\" as const,\n          id: message.id,\n          error: {\n            code: -32001,\n            message: error.cause\n              ? `${error.message} (cause: ${error.cause})`\n              : error.message,\n            data: error,\n          },\n        };\n        transportToClient.send(errorResponse).catch(onClientError);\n      }\n    });\n  };\n\n  transportToServer.onmessage = (message) => {\n    if (!reportedServerSession) {\n      if (transportToServer.sessionId) {\n        // Can only report for StreamableHttp\n        console.error(\n          \"Proxy  <-> Server sessionId: \" + transportToServer.sessionId,\n        );\n      }\n      reportedServerSession = true;\n    }\n    transportToClient.send(message).catch(onClientError);\n  };\n\n  transportToClient.onclose = () => {\n    if (transportToServerClosed) {\n      return;\n    }\n\n    transportToClientClosed = true;\n    transportToServer.close().catch(onServerError);\n  };\n\n  transportToServer.onclose = () => {\n    if (transportToClientClosed) {\n      return;\n    }\n    transportToServerClosed = true;\n    transportToClient.close().catch(onClientError);\n  };\n\n  transportToClient.onerror = onClientError;\n  transportToServer.onerror = onServerError;\n}\n"
  },
  {
    "path": "server/static/sandbox_proxy.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"color-scheme\" content=\"light dark\" />\n    <!-- CSP is set via HTTP headers by serve.ts (based on ?csp= query param).\n       The inner iframe inherits this CSP since we use document.write(). -->\n    <title>MCP-UI Proxy</title>\n    <style>\n      html,\n      body {\n        margin: 0;\n        height: 100vh;\n        width: 100vw;\n        /* Transparent background allows parent page to show through */\n        background-color: transparent;\n      }\n      body {\n        display: flex;\n        flex-direction: column;\n      }\n      * {\n        box-sizing: border-box;\n      }\n      iframe {\n        background-color: transparent;\n        border: 0px none transparent;\n        padding: 0px;\n        overflow: hidden;\n        flex-grow: 1;\n        /* Inherit color scheme from parent for consistent transparency */\n        color-scheme: inherit;\n      }\n    </style>\n  </head>\n  <body>\n    <script>\n      function buildAllowAttribute(permissions) {\n        if (!permissions) return \"\";\n\n        const allowList = [];\n        if (permissions[\"camera\"]) allowList.push(\"camera\");\n        if (permissions[\"microphone\"]) allowList.push(\"microphone\");\n        if (permissions[\"geolocation\"]) allowList.push(\"geolocation\");\n        if (permissions[\"clipboardWrite\"]) allowList.push(\"clipboard-write\");\n\n        return allowList.join(\"; \");\n      }\n\n      const ALLOWED_REFERRER_PATTERN =\n        /^http:\\/\\/(localhost|127\\.0\\.0\\.1)(:|\\/|$)/;\n\n      if (window.self === window.top) {\n        throw new Error(\"This file is only to be used in an iframe sandbox.\");\n      }\n\n      if (!document.referrer) {\n        throw new Error(\"No referrer, cannot validate embedding site.\");\n      }\n\n      if (!document.referrer.match(ALLOWED_REFERRER_PATTERN)) {\n        throw new Error(\n          `Embedding domain not allowed in referrer ${document.referrer}. (Consider updating the validation logic to allow your domain.)`,\n        );\n      }\n\n      // Extract the expected host origin from the referrer for origin validation.\n      // This is the origin we expect all parent messages to come from.\n      const EXPECTED_HOST_ORIGIN = new URL(document.referrer).origin;\n\n      const OWN_ORIGIN = new URL(window.location.href).origin;\n\n      // Security self-test: verify iframe isolation is working correctly.\n      // This MUST throw a SecurityError -- if `window.top` is accessible, the sandbox\n      // configuration is dangerously broken and untrusted content could escape.\n      try {\n        window.top?.alert(\n          \"If you see this, the sandbox is not setup securely.\",\n        );\n        throw \"FAIL\";\n      } catch (e) {\n        if (e === \"FAIL\") {\n          throw new Error(\"The sandbox is not setup securely.\");\n        }\n\n        // Expected: SecurityError confirms proper sandboxing.\n      }\n\n      // Double-iframe sandbox architecture: THIS file is the outer sandbox proxy\n      // iframe on a separate origin. It creates an inner iframe for untrusted HTML\n      // content. Per the specification, the Host and the Sandbox MUST have different\n      // origins.\n      const inner = document.createElement(\"iframe\");\n      inner.style = \"width:100%; height:100%; border:none;\";\n      inner.setAttribute(\n        \"sandbox\",\n        \"allow-scripts allow-same-origin allow-forms\",\n      );\n      // Note: allow attribute is set later when receiving sandbox-resource-ready notification\n      // based on the permissions requested by the app\n      document.body.appendChild(inner);\n\n      const RESOURCE_READY_NOTIFICATION =\n        \"ui/notifications/sandbox-resource-ready\";\n      const PROXY_READY_NOTIFICATION = \"ui/notifications/sandbox-proxy-ready\";\n\n      // Message relay: This Sandbox (outer iframe) acts as a bidirectional bridge,\n      // forwarding messages between:\n      //\n      //   Host (parent window) ↔ Sandbox (outer frame) ↔ View (inner iframe)\n      //\n      // Reason: the parent window and inner iframe have different origins and can't\n      // communicate directly, so the outer iframe forwards messages in both\n      // directions to connect them.\n      //\n      // Special case: The \"ui/notifications/sandbox-proxy-ready\" message is\n      // intercepted here (not relayed) because the Sandbox uses it to configure and\n      // load the inner iframe with the view HTML content.\n      //\n      // Security: CSP is enforced via HTTP headers on sandbox.html (set by serve.ts\n      // based on ?csp= query param). This is tamper-proof unlike meta tags.\n\n      window.addEventListener(\"message\", async (event) => {\n        if (event.source === window.parent) {\n          // Validate that messages from parent come from the expected host origin.\n          // This prevents malicious pages from sending messages to this sandbox.\n          if (event.origin !== EXPECTED_HOST_ORIGIN) {\n            console.error(\n              \"[Sandbox] Rejecting message from unexpected origin:\",\n              event.origin,\n              \"expected:\",\n              EXPECTED_HOST_ORIGIN,\n            );\n            return;\n          }\n\n          if (event.data && event.data.method === RESOURCE_READY_NOTIFICATION) {\n            const { html, sandbox, permissions } = event.data.params;\n            if (typeof sandbox === \"string\") {\n              inner.setAttribute(\"sandbox\", sandbox);\n            }\n            // Set Permission Policy allow attribute if permissions are requested\n            const allowAttribute = buildAllowAttribute(permissions);\n            if (allowAttribute) {\n              console.log(\"[Sandbox] Setting allow attribute:\", allowAttribute);\n              inner.setAttribute(\"allow\", allowAttribute);\n            }\n            if (typeof html === \"string\") {\n              // Use document.write instead of srcdoc (which the CesiumJS Map won't work with)\n              const doc =\n                inner.contentDocument || inner.contentWindow?.document;\n              if (doc) {\n                doc.open();\n                doc.write(html);\n                doc.close();\n              } else {\n                // Fallback to srcdoc if document is not accessible\n                console.warn(\n                  \"[Sandbox] document.write not available, falling back to srcdoc\",\n                );\n                inner.srcdoc = html;\n              }\n            }\n          } else {\n            if (inner && inner.contentWindow) {\n              inner.contentWindow.postMessage(event.data, \"*\");\n            }\n          }\n        } else if (event.source === inner.contentWindow) {\n          if (event.origin !== OWN_ORIGIN) {\n            console.error(\n              \"[Sandbox] Rejecting message from inner iframe with unexpected origin:\",\n              event.origin,\n              \"expected:\",\n              OWN_ORIGIN,\n            );\n            return;\n          }\n          // Relay messages from inner frame to parent window.\n          // Use specific origin instead of \"*\" to prevent message interception.\n          window.parent.postMessage(event.data, EXPECTED_HOST_ORIGIN);\n        }\n      });\n\n      // Notify the Host that the Sandbox is ready to receive view HTML.\n      // Use specific origin instead of \"*\" to ensure only the expected host receives this.\n      window.parent.postMessage(\n        {\n          jsonrpc: \"2.0\",\n          method: PROXY_READY_NOTIFICATION,\n          params: {},\n        },\n        EXPECTED_HOST_ORIGIN,\n      );\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "server/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"Node16\",\n    \"moduleResolution\": \"Node16\",\n    \"outDir\": \"./build\",\n    \"rootDir\": \"./src\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"resolveJsonModule\": true\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"packages\", \"**/*.spec.ts\"]\n}\n"
  }
]