Merge branch 'main' into feat/prompt-layout
This commit is contained in:
commit
1cf3474c93
52 changed files with 3095 additions and 602 deletions
35
.dockerignore
Normal file
35
.dockerignore
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Version control
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Node.js
|
||||
node_modules
|
||||
npm-debug.log
|
||||
|
||||
# Build artifacts
|
||||
client/dist
|
||||
client/build
|
||||
server/dist
|
||||
server/build
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.test
|
||||
.env.production
|
||||
|
||||
# Editor files
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
0
.git-blame-ignore-revs
Normal file
0
.git-blame-ignore-revs
Normal file
78
.github/workflows/e2e_tests.yml
vendored
Normal file
78
.github/workflows/e2e_tests.yml
vendored
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
name: Playwright Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
# Installing Playright dependencies can take quite awhile, and also depends on GitHub CI load.
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwoff1
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
id: setup_node
|
||||
with:
|
||||
node-version-file: package.json
|
||||
cache: npm
|
||||
|
||||
# Cache Playwright browsers
|
||||
- name: Cache Playwright browsers
|
||||
id: cache-playwright
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright # The default Playwright cache path
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }} # Cache key based on OS and package-lock.json
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright dependencies
|
||||
run: npx playwright install-deps
|
||||
|
||||
- name: Install Playwright and browsers unless cached
|
||||
run: npx playwright install --with-deps
|
||||
if: steps.cache-playwright.outputs.cache-hit != 'true'
|
||||
|
||||
- name: Run Playwright tests
|
||||
id: playwright-tests
|
||||
run: npm run test:e2e
|
||||
|
||||
- name: Upload Playwright Report and Screenshots
|
||||
uses: actions/upload-artifact@v4
|
||||
if: steps.playwright-tests.conclusion != 'skipped'
|
||||
with:
|
||||
name: playwright-report
|
||||
path: |
|
||||
client/playwright-report/
|
||||
client/test-results/
|
||||
client/results.json
|
||||
retention-days: 2
|
||||
|
||||
- name: Publish Playwright Test Summary
|
||||
uses: daun/playwright-report-summary@v3
|
||||
if: steps.playwright-tests.conclusion != 'skipped'
|
||||
with:
|
||||
create-comment: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
report-file: client/results.json
|
||||
comment-title: "🎭 Playwright E2E Test Results"
|
||||
job-summary: true
|
||||
icon-style: "emojis"
|
||||
custom-info: |
|
||||
**Test Environment:** Ubuntu Latest, Node.js ${{ steps.setup_node.outputs.node-version }}
|
||||
**Browsers:** Chromium, Firefox
|
||||
|
||||
📊 [View Detailed HTML Report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) (download artifacts)
|
||||
test-command: "npm run test:e2e"
|
||||
56
.github/workflows/main.yml
vendored
56
.github/workflows/main.yml
vendored
|
|
@ -19,13 +19,16 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version-file: package.json
|
||||
cache: npm
|
||||
|
||||
# Working around https://github.com/npm/cli/issues/4828
|
||||
# - run: npm ci
|
||||
- run: npm install --no-package-lock
|
||||
|
||||
- name: Check version consistency
|
||||
run: npm run check-version
|
||||
|
||||
- name: Check linting
|
||||
working-directory: ./client
|
||||
run: npm run lint
|
||||
|
|
@ -50,7 +53,7 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version-file: package.json
|
||||
cache: npm
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
|
|
@ -62,3 +65,52 @@ jobs:
|
|||
- run: npm run publish-all
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
publish-github-container-registry:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'release'
|
||||
environment: release
|
||||
needs: build
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: ghcr.io/${{ github.repository }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
|
|
|||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -9,3 +9,7 @@ client/tsconfig.app.tsbuildinfo
|
|||
client/tsconfig.node.tsbuildinfo
|
||||
cli/build
|
||||
test-output
|
||||
client/playwright-report/
|
||||
client/results.json
|
||||
client/test-results/
|
||||
|
||||
|
|
|
|||
1
.node-version
Normal file
1
.node-version
Normal file
|
|
@ -0,0 +1 @@
|
|||
22.x.x
|
||||
|
|
@ -30,4 +30,4 @@ The project is organized as a monorepo with workspaces:
|
|||
|
||||
- `client/`: React frontend with Vite, TypeScript and Tailwind
|
||||
- `server/`: Express backend with TypeScript
|
||||
- `bin/`: CLI scripts
|
||||
- `cli/`: Command-line interface for testing and invoking MCP server methods directly
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ Thanks for your interest in contributing! This guide explains how to get involve
|
|||
1. Fork the repository and clone it locally
|
||||
2. Install dependencies with `npm install`
|
||||
3. Run `npm run dev` to start both client and server in development mode
|
||||
4. Use the web UI at http://127.0.0.1:6274 to interact with the inspector
|
||||
4. Use the web UI at http://localhost:6274 to interact with the inspector
|
||||
|
||||
## Development Process & Pull Requests
|
||||
|
||||
1. Create a new branch for your changes
|
||||
2. Make your changes following existing code style and conventions. You can run `npm run prettier-check` and `npm run prettier-fix` as applicable.
|
||||
3. Test changes locally by running `npm test`
|
||||
3. Test changes locally by running `npm test` and `npm run test:e2e`
|
||||
4. Update documentation as needed
|
||||
5. Use clear commit messages explaining your changes
|
||||
6. Verify all changes work as expected
|
||||
|
|
|
|||
52
Dockerfile
Normal file
52
Dockerfile
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Build stage
|
||||
FROM node:24-slim AS builder
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files for installation
|
||||
COPY package*.json ./
|
||||
COPY .npmrc ./
|
||||
COPY client/package*.json ./client/
|
||||
COPY server/package*.json ./server/
|
||||
COPY cli/package*.json ./cli/
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci --ignore-scripts
|
||||
|
||||
# Copy source files
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM node:24-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files for production
|
||||
COPY package*.json ./
|
||||
COPY .npmrc ./
|
||||
COPY client/package*.json ./client/
|
||||
COPY server/package*.json ./server/
|
||||
COPY cli/package*.json ./cli/
|
||||
|
||||
# Install only production dependencies
|
||||
RUN npm ci --omit=dev --ignore-scripts
|
||||
|
||||
# Copy built files from builder stage
|
||||
COPY --from=builder /app/client/dist ./client/dist
|
||||
COPY --from=builder /app/client/bin ./client/bin
|
||||
COPY --from=builder /app/server/build ./server/build
|
||||
COPY --from=builder /app/cli/build ./cli/build
|
||||
|
||||
# Set default port values as environment variables
|
||||
ENV CLIENT_PORT=6274
|
||||
ENV SERVER_PORT=6277
|
||||
|
||||
# Document which ports the application uses internally
|
||||
EXPOSE ${CLIENT_PORT} ${SERVER_PORT}
|
||||
|
||||
# Use ENTRYPOINT with CMD for arguments
|
||||
ENTRYPOINT ["npm", "start"]
|
||||
66
README.md
66
README.md
|
|
@ -137,17 +137,64 @@ The inspector supports bearer token authentication for SSE connections. Enter yo
|
|||
|
||||
The MCP Inspector includes a proxy server that can run and communicate with local MCP processes. The proxy server should not be exposed to untrusted networks as it has permissions to spawn local processes and can connect to any specified MCP server.
|
||||
|
||||
#### Authentication
|
||||
|
||||
The MCP Inspector proxy server requires authentication by default. When starting the server, a random session token is generated and printed to the console:
|
||||
|
||||
```
|
||||
🔑 Session token: 3a1c267fad21f7150b7d624c160b7f09b0b8c4f623c7107bbf13378f051538d4
|
||||
|
||||
🔗 Open inspector with token pre-filled:
|
||||
http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=3a1c267fad21f7150b7d624c160b7f09b0b8c4f623c7107bbf13378f051538d4
|
||||
```
|
||||
|
||||
This token must be included as a Bearer token in the Authorization header for all requests to the server. The inspector will automatically open your browser with the token pre-filled in the URL.
|
||||
|
||||
**Automatic browser opening** - The inspector now automatically opens your browser with the token pre-filled in the URL when authentication is enabled.
|
||||
|
||||
**Alternative: Manual configuration** - If you already have the inspector open:
|
||||
|
||||
1. Click the "Configuration" button in the sidebar
|
||||
2. Find "Proxy Session Token" and enter the token displayed in the proxy console
|
||||
3. Click "Save" to apply the configuration
|
||||
|
||||
The token will be saved in your browser's local storage for future use.
|
||||
|
||||
If you need to disable authentication (NOT RECOMMENDED), you can set the `DANGEROUSLY_OMIT_AUTH` environment variable:
|
||||
|
||||
```bash
|
||||
DANGEROUSLY_OMIT_AUTH=true npm start
|
||||
```
|
||||
|
||||
#### Local-only Binding
|
||||
|
||||
By default, both the MCP Inspector proxy server and client bind only to `localhost` to prevent network access. This ensures they are not accessible from other devices on the network. If you need to bind to all interfaces for development purposes, you can override this with the `HOST` environment variable:
|
||||
|
||||
```bash
|
||||
HOST=0.0.0.0 npm start
|
||||
```
|
||||
|
||||
**Warning:** Only bind to all interfaces in trusted network environments, as this exposes the proxy server's ability to execute local processes and both services to network access.
|
||||
|
||||
#### DNS Rebinding Protection
|
||||
|
||||
To prevent DNS rebinding attacks, the MCP Inspector validates the `Origin` header on incoming requests. By default, only requests from the client origin are allowed (respects `CLIENT_PORT` if set, defaulting to port 6274). You can configure additional allowed origins by setting the `ALLOWED_ORIGINS` environment variable (comma-separated list):
|
||||
|
||||
```bash
|
||||
ALLOWED_ORIGINS=http://localhost:6274,http://localhost:8000 npm start
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
The MCP Inspector supports the following configuration settings. To change them, click on the `Configuration` button in the MCP Inspector UI:
|
||||
|
||||
| Setting | Description | Default |
|
||||
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| `MCP_SERVER_REQUEST_TIMEOUT` | Timeout for requests to the MCP server (ms) | 10000 |
|
||||
| `MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS` | Reset timeout on progress notifications | true |
|
||||
| `MCP_REQUEST_MAX_TOTAL_TIMEOUT` | Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications) | 60000 |
|
||||
| `MCP_PROXY_FULL_ADDRESS` | Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577 | "" |
|
||||
| `MCP_AUTO_OPEN_ENABLED` | Enable automatic browser opening when inspector starts. Only as environment var, not configurable in browser. | true |
|
||||
| Setting | Description | Default |
|
||||
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| `MCP_SERVER_REQUEST_TIMEOUT` | Timeout for requests to the MCP server (ms) | 10000 |
|
||||
| `MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS` | Reset timeout on progress notifications | true |
|
||||
| `MCP_REQUEST_MAX_TOTAL_TIMEOUT` | Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications) | 60000 |
|
||||
| `MCP_PROXY_FULL_ADDRESS` | Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577 | "" |
|
||||
| `MCP_AUTO_OPEN_ENABLED` | Enable automatic browser opening when inspector starts (works with authentication enabled). Only as environment var, not configurable in browser. | true |
|
||||
|
||||
These settings can be adjusted in real-time through the UI and will persist across sessions.
|
||||
|
||||
|
|
@ -252,9 +299,12 @@ npx @modelcontextprotocol/inspector --cli node build/index.js --method resources
|
|||
# List available prompts
|
||||
npx @modelcontextprotocol/inspector --cli node build/index.js --method prompts/list
|
||||
|
||||
# Connect to a remote MCP server
|
||||
# Connect to a remote MCP server (default is SSE transport)
|
||||
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com
|
||||
|
||||
# Connect to a remote MCP server (with Streamable HTTP transport)
|
||||
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --transport http
|
||||
|
||||
# Call a tool on a remote server
|
||||
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --method tools/call --tool-name remotetool --tool-arg param=value
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector-cli",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"description": "CLI for the Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
},
|
||||
"devDependencies": {},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
"@modelcontextprotocol/sdk": "^1.13.1",
|
||||
"commander": "^13.1.0",
|
||||
"spawn-rx": "^5.1.2"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,14 @@ console.log(`${colors.BLUE}- Resource-related options (--uri)${colors.NC}`);
|
|||
console.log(
|
||||
`${colors.BLUE}- Prompt-related options (--prompt-name, --prompt-args)${colors.NC}`,
|
||||
);
|
||||
console.log(`${colors.BLUE}- Logging options (--log-level)${colors.NC}\n`);
|
||||
console.log(`${colors.BLUE}- Logging options (--log-level)${colors.NC}`);
|
||||
console.log(
|
||||
`${colors.BLUE}- Transport types (--transport http/sse/stdio)${colors.NC}`,
|
||||
);
|
||||
console.log(
|
||||
`${colors.BLUE}- Transport inference from URL suffixes (/mcp, /sse)${colors.NC}`,
|
||||
);
|
||||
console.log(`\n`);
|
||||
|
||||
// Get directory paths
|
||||
const SCRIPTS_DIR = __dirname;
|
||||
|
|
@ -62,9 +69,11 @@ if (!fs.existsSync(OUTPUT_DIR)) {
|
|||
}
|
||||
|
||||
// Create a temporary directory for test files
|
||||
const TEMP_DIR = fs.mkdirSync(path.join(os.tmpdir(), "mcp-inspector-tests"), {
|
||||
recursive: true,
|
||||
});
|
||||
const TEMP_DIR = path.join(os.tmpdir(), "mcp-inspector-tests");
|
||||
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
||||
|
||||
// Track servers for cleanup
|
||||
let runningServers = [];
|
||||
|
||||
process.on("exit", () => {
|
||||
try {
|
||||
|
|
@ -74,6 +83,21 @@ process.on("exit", () => {
|
|||
`${colors.RED}Failed to remove temp directory: ${err.message}${colors.NC}`,
|
||||
);
|
||||
}
|
||||
|
||||
runningServers.forEach((server) => {
|
||||
try {
|
||||
process.kill(-server.pid);
|
||||
} catch (e) {}
|
||||
});
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
runningServers.forEach((server) => {
|
||||
try {
|
||||
process.kill(-server.pid);
|
||||
} catch (e) {}
|
||||
});
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Use the existing sample config file
|
||||
|
|
@ -121,6 +145,11 @@ async function runBasicTest(testName, ...args) {
|
|||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
console.log(`${colors.YELLOW}Test timed out: ${testName}${colors.NC}`);
|
||||
child.kill();
|
||||
}, 10000);
|
||||
|
||||
// Pipe stdout and stderr to the output file
|
||||
child.stdout.pipe(outputStream);
|
||||
child.stderr.pipe(outputStream);
|
||||
|
|
@ -135,6 +164,7 @@ async function runBasicTest(testName, ...args) {
|
|||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
clearTimeout(timeout);
|
||||
outputStream.end();
|
||||
|
||||
if (code === 0) {
|
||||
|
|
@ -201,6 +231,13 @@ async function runErrorTest(testName, ...args) {
|
|||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
console.log(
|
||||
`${colors.YELLOW}Error test timed out: ${testName}${colors.NC}`,
|
||||
);
|
||||
child.kill();
|
||||
}, 10000);
|
||||
|
||||
// Pipe stdout and stderr to the output file
|
||||
child.stdout.pipe(outputStream);
|
||||
child.stderr.pipe(outputStream);
|
||||
|
|
@ -215,6 +252,7 @@ async function runErrorTest(testName, ...args) {
|
|||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
clearTimeout(timeout);
|
||||
outputStream.end();
|
||||
|
||||
// For error tests, we expect a non-zero exit code
|
||||
|
|
@ -611,6 +649,79 @@ async function runTests() {
|
|||
"debug",
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${colors.YELLOW}=== Running HTTP Transport Tests ===${colors.NC}`,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`${colors.BLUE}Starting server-everything in streamableHttp mode.${colors.NC}`,
|
||||
);
|
||||
const httpServer = spawn(
|
||||
"npx",
|
||||
["@modelcontextprotocol/server-everything", "streamableHttp"],
|
||||
{
|
||||
detached: true,
|
||||
stdio: "ignore",
|
||||
},
|
||||
);
|
||||
runningServers.push(httpServer);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
|
||||
// Test 25: HTTP transport inferred from URL ending with /mcp
|
||||
await runBasicTest(
|
||||
"http_transport_inferred",
|
||||
"http://127.0.0.1:3001/mcp",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 26: HTTP transport with explicit --transport http flag
|
||||
await runBasicTest(
|
||||
"http_transport_with_explicit_flag",
|
||||
"http://127.0.0.1:3001",
|
||||
"--transport",
|
||||
"http",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 27: HTTP transport with suffix and --transport http flag
|
||||
await runBasicTest(
|
||||
"http_transport_with_explicit_flag_and_suffix",
|
||||
"http://127.0.0.1:3001/mcp",
|
||||
"--transport",
|
||||
"http",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Test 28: SSE transport given to HTTP server (should fail)
|
||||
await runErrorTest(
|
||||
"sse_transport_given_to_http_server",
|
||||
"http://127.0.0.1:3001",
|
||||
"--transport",
|
||||
"sse",
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
);
|
||||
|
||||
// Kill HTTP server
|
||||
try {
|
||||
process.kill(-httpServer.pid);
|
||||
console.log(
|
||||
`${colors.BLUE}HTTP server killed, waiting for port to be released...${colors.NC}`,
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(
|
||||
`${colors.RED}Error killing HTTP server: ${e.message}${colors.NC}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Print test summary
|
||||
console.log(`\n${colors.YELLOW}=== Test Summary ===${colors.NC}`);
|
||||
console.log(`${colors.GREEN}Passed: ${PASSED_TESTS}${colors.NC}`);
|
||||
|
|
|
|||
|
|
@ -29,9 +29,13 @@ type Args = {
|
|||
logLevel?: LogLevel;
|
||||
toolName?: string;
|
||||
toolArg?: Record<string, string>;
|
||||
transport?: "sse" | "stdio" | "http";
|
||||
};
|
||||
|
||||
function createTransportOptions(target: string[]): TransportOptions {
|
||||
function createTransportOptions(
|
||||
target: string[],
|
||||
transport?: "sse" | "stdio" | "http",
|
||||
): TransportOptions {
|
||||
if (target.length === 0) {
|
||||
throw new Error(
|
||||
"Target is required. Specify a URL or a command to execute.",
|
||||
|
|
@ -50,8 +54,30 @@ function createTransportOptions(target: string[]): TransportOptions {
|
|||
throw new Error("Arguments cannot be passed to a URL-based MCP server.");
|
||||
}
|
||||
|
||||
let transportType: "sse" | "stdio" | "http";
|
||||
if (transport) {
|
||||
if (!isUrl && transport !== "stdio") {
|
||||
throw new Error("Only stdio transport can be used with local commands.");
|
||||
}
|
||||
if (isUrl && transport === "stdio") {
|
||||
throw new Error("stdio transport cannot be used with URLs.");
|
||||
}
|
||||
transportType = transport;
|
||||
} else if (isUrl) {
|
||||
const url = new URL(command);
|
||||
if (url.pathname.endsWith("/mcp")) {
|
||||
transportType = "http";
|
||||
} else if (url.pathname.endsWith("/sse")) {
|
||||
transportType = "sse";
|
||||
} else {
|
||||
transportType = "sse";
|
||||
}
|
||||
} else {
|
||||
transportType = "stdio";
|
||||
}
|
||||
|
||||
return {
|
||||
transportType: isUrl ? "sse" : "stdio",
|
||||
transportType,
|
||||
command: isUrl ? undefined : command,
|
||||
args: isUrl ? undefined : commandArgs,
|
||||
url: isUrl ? command : undefined,
|
||||
|
|
@ -59,7 +85,7 @@ function createTransportOptions(target: string[]): TransportOptions {
|
|||
}
|
||||
|
||||
async function callMethod(args: Args): Promise<void> {
|
||||
const transportOptions = createTransportOptions(args.target);
|
||||
const transportOptions = createTransportOptions(args.target, args.transport);
|
||||
const transport = createTransport(transportOptions);
|
||||
const client = new Client({
|
||||
name: "inspector-cli",
|
||||
|
|
@ -214,6 +240,22 @@ function parseArgs(): Args {
|
|||
|
||||
return value as LogLevel;
|
||||
},
|
||||
)
|
||||
//
|
||||
// Transport options
|
||||
//
|
||||
.option(
|
||||
"--transport <type>",
|
||||
"Transport type (sse, http, or stdio). Auto-detected from URL: /mcp → http, /sse → sse, commands → stdio",
|
||||
(value: string) => {
|
||||
const validTransports = ["sse", "http", "stdio"];
|
||||
if (!validTransports.includes(value)) {
|
||||
throw new Error(
|
||||
`Invalid transport type: ${value}. Valid types are: ${validTransports.join(", ")}`,
|
||||
);
|
||||
}
|
||||
return value as "sse" | "http" | "stdio";
|
||||
},
|
||||
);
|
||||
|
||||
// Parse only the arguments before --
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ import {
|
|||
getDefaultEnvironment,
|
||||
StdioClientTransport,
|
||||
} from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
||||
import { findActualExecutable } from "spawn-rx";
|
||||
|
||||
export type TransportOptions = {
|
||||
transportType: "sse" | "stdio";
|
||||
transportType: "sse" | "stdio" | "http";
|
||||
command?: string;
|
||||
args?: string[];
|
||||
url?: string;
|
||||
|
|
@ -15,11 +16,22 @@ export type TransportOptions = {
|
|||
|
||||
function createSSETransport(options: TransportOptions): Transport {
|
||||
const baseUrl = new URL(options.url ?? "");
|
||||
const sseUrl = new URL("/sse", baseUrl);
|
||||
const sseUrl = baseUrl.pathname.endsWith("/sse")
|
||||
? baseUrl
|
||||
: new URL("/sse", baseUrl);
|
||||
|
||||
return new SSEClientTransport(sseUrl);
|
||||
}
|
||||
|
||||
function createHTTPTransport(options: TransportOptions): Transport {
|
||||
const baseUrl = new URL(options.url ?? "");
|
||||
const mcpUrl = baseUrl.pathname.endsWith("/mcp")
|
||||
? baseUrl
|
||||
: new URL("/mcp", baseUrl);
|
||||
|
||||
return new StreamableHTTPClientTransport(mcpUrl);
|
||||
}
|
||||
|
||||
function createStdioTransport(options: TransportOptions): Transport {
|
||||
let args: string[] = [];
|
||||
|
||||
|
|
@ -67,6 +79,10 @@ export function createTransport(options: TransportOptions): Transport {
|
|||
return createSSETransport(options);
|
||||
}
|
||||
|
||||
if (transportType === "http") {
|
||||
return createHTTPTransport(options);
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported transport type: ${transportType}`);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import open from "open";
|
||||
import { join, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import handler from "serve-handler";
|
||||
|
|
@ -39,19 +40,23 @@ const server = http.createServer((request, response) => {
|
|||
return handler(request, response, handlerOptions);
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 6274;
|
||||
const port = parseInt(process.env.CLIENT_PORT || "6274", 10);
|
||||
const host = process.env.HOST || "localhost";
|
||||
server.on("listening", () => {
|
||||
console.log(
|
||||
`🔍 MCP Inspector is up and running at http://127.0.0.1:${port} 🚀`,
|
||||
);
|
||||
const url = process.env.INSPECTOR_URL || `http://${host}:${port}`;
|
||||
console.log(`\n🚀 MCP Inspector is up and running at:\n ${url}\n`);
|
||||
if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") {
|
||||
console.log(`🌐 Opening browser...`);
|
||||
open(url);
|
||||
}
|
||||
});
|
||||
server.on("error", (err) => {
|
||||
if (err.message.includes(`EADDRINUSE`)) {
|
||||
console.error(
|
||||
`❌ MCP Inspector PORT IS IN USE at http://127.0.0.1:${port} ❌ `,
|
||||
`❌ MCP Inspector PORT IS IN USE at http://${host}:${port} ❌ `,
|
||||
);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
server.listen(port);
|
||||
server.listen(port, host);
|
||||
|
|
|
|||
|
|
@ -2,15 +2,202 @@
|
|||
|
||||
import open from "open";
|
||||
import { resolve, dirname } from "path";
|
||||
import { spawnPromise } from "spawn-rx";
|
||||
import { spawnPromise, spawn } from "spawn-rx";
|
||||
import { fileURLToPath } from "url";
|
||||
import { randomBytes } from "crypto";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277";
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms, true));
|
||||
}
|
||||
|
||||
function getClientUrl(port, authDisabled, sessionToken, serverPort) {
|
||||
const host = process.env.HOST || "localhost";
|
||||
const baseUrl = `http://${host}:${port}`;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (serverPort && serverPort !== DEFAULT_MCP_PROXY_LISTEN_PORT) {
|
||||
params.set("MCP_PROXY_PORT", serverPort);
|
||||
}
|
||||
if (!authDisabled) {
|
||||
params.set("MCP_PROXY_AUTH_TOKEN", sessionToken);
|
||||
}
|
||||
return params.size > 0 ? `${baseUrl}/?${params.toString()}` : baseUrl;
|
||||
}
|
||||
|
||||
async function startDevServer(serverOptions) {
|
||||
const { SERVER_PORT, CLIENT_PORT, sessionToken, envVars, abort } =
|
||||
serverOptions;
|
||||
const serverCommand = "npx";
|
||||
const serverArgs = ["tsx", "watch", "--clear-screen=false", "src/index.ts"];
|
||||
const isWindows = process.platform === "win32";
|
||||
|
||||
const spawnOptions = {
|
||||
cwd: resolve(__dirname, "../..", "server"),
|
||||
env: {
|
||||
...process.env,
|
||||
SERVER_PORT,
|
||||
CLIENT_PORT,
|
||||
MCP_PROXY_TOKEN: sessionToken,
|
||||
MCP_ENV_VARS: JSON.stringify(envVars),
|
||||
},
|
||||
signal: abort.signal,
|
||||
echoOutput: true,
|
||||
};
|
||||
|
||||
// For Windows, we need to use stdin: 'ignore' to simulate < NUL
|
||||
if (isWindows) {
|
||||
spawnOptions.stdin = "ignore";
|
||||
}
|
||||
|
||||
const server = spawn(serverCommand, serverArgs, spawnOptions);
|
||||
|
||||
// Give server time to start
|
||||
const serverOk = await Promise.race([
|
||||
new Promise((resolve) => {
|
||||
server.subscribe({
|
||||
complete: () => resolve(false),
|
||||
error: () => resolve(false),
|
||||
next: () => {}, // We're using echoOutput
|
||||
});
|
||||
}),
|
||||
delay(3000).then(() => true),
|
||||
]);
|
||||
|
||||
return { server, serverOk };
|
||||
}
|
||||
|
||||
async function startProdServer(serverOptions) {
|
||||
const {
|
||||
SERVER_PORT,
|
||||
CLIENT_PORT,
|
||||
sessionToken,
|
||||
envVars,
|
||||
abort,
|
||||
command,
|
||||
mcpServerArgs,
|
||||
} = serverOptions;
|
||||
const inspectorServerPath = resolve(
|
||||
__dirname,
|
||||
"../..",
|
||||
"server",
|
||||
"build",
|
||||
"index.js",
|
||||
);
|
||||
|
||||
const server = spawnPromise(
|
||||
"node",
|
||||
[
|
||||
inspectorServerPath,
|
||||
...(command ? [`--env`, command] : []),
|
||||
...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []),
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
SERVER_PORT,
|
||||
CLIENT_PORT,
|
||||
MCP_PROXY_TOKEN: sessionToken,
|
||||
MCP_ENV_VARS: JSON.stringify(envVars),
|
||||
},
|
||||
signal: abort.signal,
|
||||
echoOutput: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Make sure server started before starting client
|
||||
const serverOk = await Promise.race([server, delay(2 * 1000)]);
|
||||
|
||||
return { server, serverOk };
|
||||
}
|
||||
|
||||
async function startDevClient(clientOptions) {
|
||||
const {
|
||||
CLIENT_PORT,
|
||||
SERVER_PORT,
|
||||
authDisabled,
|
||||
sessionToken,
|
||||
abort,
|
||||
cancelled,
|
||||
} = clientOptions;
|
||||
const clientCommand = "npx";
|
||||
const host = process.env.HOST || "localhost";
|
||||
const clientArgs = ["vite", "--port", CLIENT_PORT, "--host", host];
|
||||
|
||||
const client = spawn(clientCommand, clientArgs, {
|
||||
cwd: resolve(__dirname, ".."),
|
||||
env: { ...process.env, CLIENT_PORT },
|
||||
signal: abort.signal,
|
||||
echoOutput: true,
|
||||
});
|
||||
|
||||
const url = getClientUrl(
|
||||
CLIENT_PORT,
|
||||
authDisabled,
|
||||
sessionToken,
|
||||
SERVER_PORT,
|
||||
);
|
||||
|
||||
// Give vite time to start before opening or logging the URL
|
||||
setTimeout(() => {
|
||||
console.log(`\n🚀 MCP Inspector is up and running at:\n ${url}\n`);
|
||||
if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") {
|
||||
console.log("🌐 Opening browser...");
|
||||
open(url);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
client.subscribe({
|
||||
complete: resolve,
|
||||
error: (err) => {
|
||||
if (!cancelled || process.env.DEBUG) {
|
||||
console.error("Client error:", err);
|
||||
}
|
||||
resolve(null);
|
||||
},
|
||||
next: () => {}, // We're using echoOutput
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function startProdClient(clientOptions) {
|
||||
const {
|
||||
CLIENT_PORT,
|
||||
SERVER_PORT,
|
||||
authDisabled,
|
||||
sessionToken,
|
||||
abort,
|
||||
cancelled,
|
||||
} = clientOptions;
|
||||
const inspectorClientPath = resolve(
|
||||
__dirname,
|
||||
"../..",
|
||||
"client",
|
||||
"bin",
|
||||
"client.js",
|
||||
);
|
||||
|
||||
const url = getClientUrl(
|
||||
CLIENT_PORT,
|
||||
authDisabled,
|
||||
sessionToken,
|
||||
SERVER_PORT,
|
||||
);
|
||||
|
||||
await spawnPromise("node", [inspectorClientPath], {
|
||||
env: {
|
||||
...process.env,
|
||||
CLIENT_PORT,
|
||||
INSPECTOR_URL: url,
|
||||
},
|
||||
signal: abort.signal,
|
||||
echoOutput: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Parse command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
|
|
@ -18,6 +205,7 @@ async function main() {
|
|||
const mcpServerArgs = [];
|
||||
let command = null;
|
||||
let parsingFlags = true;
|
||||
let isDev = false;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
|
|
@ -27,6 +215,11 @@ async function main() {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (parsingFlags && arg === "--dev") {
|
||||
isDev = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsingFlags && arg === "-e" && i + 1 < args.length) {
|
||||
const envVar = args[++i];
|
||||
const equalsIndex = envVar.indexOf("=");
|
||||
|
|
@ -38,34 +231,25 @@ async function main() {
|
|||
} else {
|
||||
envVars[envVar] = "";
|
||||
}
|
||||
} else if (!command) {
|
||||
} else if (!command && !isDev) {
|
||||
command = arg;
|
||||
} else {
|
||||
} else if (!isDev) {
|
||||
mcpServerArgs.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
const inspectorServerPath = resolve(
|
||||
__dirname,
|
||||
"../..",
|
||||
"server",
|
||||
"build",
|
||||
"index.js",
|
||||
);
|
||||
|
||||
// Path to the client entry point
|
||||
const inspectorClientPath = resolve(
|
||||
__dirname,
|
||||
"../..",
|
||||
"client",
|
||||
"bin",
|
||||
"client.js",
|
||||
);
|
||||
|
||||
const CLIENT_PORT = process.env.CLIENT_PORT ?? "6274";
|
||||
const SERVER_PORT = process.env.SERVER_PORT ?? "6277";
|
||||
const SERVER_PORT = process.env.SERVER_PORT ?? DEFAULT_MCP_PROXY_LISTEN_PORT;
|
||||
|
||||
console.log("Starting MCP inspector...");
|
||||
console.log(
|
||||
isDev
|
||||
? "Starting MCP inspector in development mode..."
|
||||
: "Starting MCP inspector...",
|
||||
);
|
||||
|
||||
// Generate session token for authentication
|
||||
const sessionToken = randomBytes(32).toString("hex");
|
||||
const authDisabled = !!process.env.DANGEROUSLY_OMIT_AUTH;
|
||||
|
||||
const abort = new AbortController();
|
||||
|
||||
|
|
@ -74,40 +258,42 @@ async function main() {
|
|||
cancelled = true;
|
||||
abort.abort();
|
||||
});
|
||||
let server, serverOk;
|
||||
try {
|
||||
server = spawnPromise(
|
||||
"node",
|
||||
[
|
||||
inspectorServerPath,
|
||||
...(command ? [`--env`, command] : []),
|
||||
...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []),
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
PORT: SERVER_PORT,
|
||||
MCP_ENV_VARS: JSON.stringify(envVars),
|
||||
},
|
||||
signal: abort.signal,
|
||||
echoOutput: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Make sure server started before starting client
|
||||
serverOk = await Promise.race([server, delay(2 * 1000)]);
|
||||
let server, serverOk;
|
||||
|
||||
try {
|
||||
const serverOptions = {
|
||||
SERVER_PORT,
|
||||
CLIENT_PORT,
|
||||
sessionToken,
|
||||
envVars,
|
||||
abort,
|
||||
command,
|
||||
mcpServerArgs,
|
||||
};
|
||||
|
||||
const result = isDev
|
||||
? await startDevServer(serverOptions)
|
||||
: await startProdServer(serverOptions);
|
||||
|
||||
server = result.server;
|
||||
serverOk = result.serverOk;
|
||||
} catch (error) {}
|
||||
|
||||
if (serverOk) {
|
||||
try {
|
||||
if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") {
|
||||
open(`http://127.0.0.1:${CLIENT_PORT}`);
|
||||
}
|
||||
await spawnPromise("node", [inspectorClientPath], {
|
||||
env: { ...process.env, PORT: CLIENT_PORT },
|
||||
signal: abort.signal,
|
||||
echoOutput: true,
|
||||
});
|
||||
const clientOptions = {
|
||||
CLIENT_PORT,
|
||||
SERVER_PORT,
|
||||
authDisabled,
|
||||
sessionToken,
|
||||
abort,
|
||||
cancelled,
|
||||
};
|
||||
|
||||
await (isDev
|
||||
? startDevClient(clientOptions)
|
||||
: startProdClient(clientOptions));
|
||||
} catch (e) {
|
||||
if (!cancelled || process.env.DEBUG) throw e;
|
||||
}
|
||||
|
|
|
|||
18
client/e2e/global-teardown.js
Normal file
18
client/e2e/global-teardown.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { rimraf } from "rimraf";
|
||||
|
||||
async function globalTeardown() {
|
||||
if (!process.env.CI) {
|
||||
console.log("Cleaning up test-results directory...");
|
||||
// Add a small delay to ensure all Playwright files are written
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
await rimraf("./e2e/test-results");
|
||||
console.log("Test-results directory cleaned up.");
|
||||
}
|
||||
}
|
||||
|
||||
export default globalTeardown;
|
||||
|
||||
// Call the function when this script is run directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
globalTeardown().catch(console.error);
|
||||
}
|
||||
113
client/e2e/transport-type-dropdown.spec.ts
Normal file
113
client/e2e/transport-type-dropdown.spec.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
|
||||
// Adjust the URL if your dev server runs on a different port
|
||||
const APP_URL = "http://localhost:6274/";
|
||||
|
||||
test.describe("Transport Type Dropdown", () => {
|
||||
test("should have options for STDIO, SSE, and Streamable HTTP", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(APP_URL);
|
||||
|
||||
// Wait for the Transport Type dropdown to be visible
|
||||
const selectTrigger = page.getByLabel("Transport Type");
|
||||
await expect(selectTrigger).toBeVisible();
|
||||
|
||||
// Open the dropdown
|
||||
await selectTrigger.click();
|
||||
|
||||
// Check for the three options
|
||||
await expect(page.getByRole("option", { name: "STDIO" })).toBeVisible();
|
||||
await expect(page.getByRole("option", { name: "SSE" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("option", { name: "Streamable HTTP" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show Command and Arguments fields and hide URL field when Transport Type is STDIO", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(APP_URL);
|
||||
|
||||
// Wait for the Transport Type dropdown to be visible
|
||||
const selectTrigger = page.getByLabel("Transport Type");
|
||||
await expect(selectTrigger).toBeVisible();
|
||||
|
||||
// Open the dropdown and select STDIO
|
||||
await selectTrigger.click();
|
||||
await page.getByRole("option", { name: "STDIO" }).click();
|
||||
|
||||
// Wait for the form to update
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Check that Command and Arguments fields are visible
|
||||
await expect(page.locator("#command-input")).toBeVisible();
|
||||
await expect(page.locator("#arguments-input")).toBeVisible();
|
||||
|
||||
// Check that URL field is not visible
|
||||
await expect(page.locator("#sse-url-input")).not.toBeVisible();
|
||||
|
||||
// Also verify the labels are present
|
||||
await expect(page.getByText("Command")).toBeVisible();
|
||||
await expect(page.getByText("Arguments")).toBeVisible();
|
||||
await expect(page.getByText("URL")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("should show URL field and hide Command and Arguments fields when Transport Type is SSE", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(APP_URL);
|
||||
|
||||
// Wait for the Transport Type dropdown to be visible
|
||||
const selectTrigger = page.getByLabel("Transport Type");
|
||||
await expect(selectTrigger).toBeVisible();
|
||||
|
||||
// Open the dropdown and select SSE
|
||||
await selectTrigger.click();
|
||||
await page.getByRole("option", { name: "SSE" }).click();
|
||||
|
||||
// Wait for the form to update
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Check that URL field is visible
|
||||
await expect(page.locator("#sse-url-input")).toBeVisible();
|
||||
|
||||
// Check that Command and Arguments fields are not visible
|
||||
await expect(page.locator("#command-input")).not.toBeVisible();
|
||||
await expect(page.locator("#arguments-input")).not.toBeVisible();
|
||||
|
||||
// Also verify the labels are present/absent
|
||||
await expect(page.getByText("URL")).toBeVisible();
|
||||
await expect(page.getByText("Command")).not.toBeVisible();
|
||||
await expect(page.getByText("Arguments")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("should show URL field and hide Command and Arguments fields when Transport Type is Streamable HTTP", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(APP_URL);
|
||||
|
||||
// Wait for the Transport Type dropdown to be visible
|
||||
const selectTrigger = page.getByLabel("Transport Type");
|
||||
await expect(selectTrigger).toBeVisible();
|
||||
|
||||
// Open the dropdown and select Streamable HTTP
|
||||
await selectTrigger.click();
|
||||
await page.getByRole("option", { name: "Streamable HTTP" }).click();
|
||||
|
||||
// Wait for the form to update
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Check that URL field is visible
|
||||
await expect(page.locator("#sse-url-input")).toBeVisible();
|
||||
|
||||
// Check that Command and Arguments fields are not visible
|
||||
await expect(page.locator("#command-input")).not.toBeVisible();
|
||||
await expect(page.locator("#arguments-input")).not.toBeVisible();
|
||||
|
||||
// Also verify the labels are present/absent
|
||||
await expect(page.getByText("URL")).toBeVisible();
|
||||
await expect(page.getByText("Command")).not.toBeVisible();
|
||||
await expect(page.getByText("Arguments")).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
<title>MCP Inspector</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="root" class="w-full"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ module.exports = {
|
|||
"/node_modules/",
|
||||
"/dist/",
|
||||
"/bin/",
|
||||
"/e2e/",
|
||||
"\\.config\\.(js|ts|cjs|mjs)$",
|
||||
],
|
||||
// Exclude the same patterns from coverage reports
|
||||
|
|
@ -28,6 +29,7 @@ module.exports = {
|
|||
"/node_modules/",
|
||||
"/dist/",
|
||||
"/bin/",
|
||||
"/e2e/",
|
||||
"\\.config\\.(js|ts|cjs|mjs)$",
|
||||
],
|
||||
};
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue