diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml
index a4fd6c52..6e916eba 100644
--- a/.github/workflows/claude.yml
+++ b/.github/workflows/claude.yml
@@ -18,10 +18,6 @@ jobs:
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
- ) &&
- (
- github.actor == 'ihrpr' ||
- github.actor == 'olaservo'
)
runs-on: ubuntu-latest
permissions:
@@ -29,6 +25,7 @@ jobs:
pull-requests: read
issues: read
id-token: write
+ actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -41,24 +38,16 @@ jobs:
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
- # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
- # model: "claude-opus-4-20250514"
+ # Allow Claude to read CI results on PRs
+ additional_permissions: |
+ actions: read
- # Optional: Customize the trigger phrase (default: @claude)
- # trigger_phrase: "/claude"
+ # Trigger when assigned to an issue
+ assignee_trigger: "claude"
- # Optional: Trigger when specific user is assigned to an issue
- # assignee_trigger: "claude-bot"
+ # Allow Claude to run bash
+ # This should be safe given the repo is already public
+ allowed_tools: "Bash"
- # Optional: Allow Claude to run specific commands
- # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
-
- # Optional: Add custom instructions for Claude to customize its behavior for your project
- # custom_instructions: |
- # Follow our coding standards
- # Ensure all new code has tests
- # Use TypeScript for new files
-
- # Optional: Custom environment variables for Claude
- # claude_env: |
- # NODE_ENV: test
+ custom_instructions: |
+ If posting a comment to GitHub, give a concise summary of the comment at the top and put all the details in a block.
diff --git a/.gitignore b/.gitignore
index eeee17fe..176b0d4c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,8 +9,11 @@ client/tsconfig.app.tsbuildinfo
client/tsconfig.node.tsbuildinfo
cli/build
test-output
+tool-test-output
# symlinked by `npm run link:sdk`:
sdk
client/playwright-report/
client/results.json
client/test-results/
+client/e2e/test-results/
+mcp.json
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100644
index 00000000..0a3afdcb
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,2 @@
+npx lint-staged
+git update-index --again
diff --git a/.prettierignore b/.prettierignore
index c8824c9a..167a7cb4 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -2,3 +2,5 @@ packages
server/build
CODE_OF_CONDUCT.md
SECURITY.md
+mcp.json
+.claude/settings.local.json
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index f36fb8bb..d66091d1 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
# Build stage
-FROM node:24-slim AS builder
+FROM node:current-alpine3.22 AS builder
# Set working directory
WORKDIR /app
@@ -49,4 +49,4 @@ ENV SERVER_PORT=6277
EXPOSE ${CLIENT_PORT} ${SERVER_PORT}
# Use ENTRYPOINT with CMD for arguments
-ENTRYPOINT ["npm", "start"]
\ No newline at end of file
+ENTRYPOINT ["npm", "start"]
diff --git a/README.md b/README.md
index 6a671f1c..fc563f91 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,14 @@ npx @modelcontextprotocol/inspector
The server will start up and the UI will be accessible at `http://localhost:6274`.
+### Docker Container
+
+You can also start it in a Docker container with the following command:
+
+```bash
+docker run --rm --network host -p 6274:6274 -p 6277:6277 ghcr.io/modelcontextprotocol/inspector:latest
+```
+
### From an MCP server repository
To inspect an MCP server implementation, there's no need to clone this repo. Instead, use `npx`. For example, if your server is built at `build/index.js`:
@@ -166,6 +174,16 @@ If you need to disable authentication (NOT RECOMMENDED), you can set the `DANGER
DANGEROUSLY_OMIT_AUTH=true npm start
```
+---
+
+**🚨 WARNING 🚨**
+
+Disabling authentication with `DANGEROUSLY_OMIT_AUTH` is incredibly dangerous! Disabling auth leaves your machine open to attack not just when exposed to the public internet, but also **via your web browser**. Meaning, visiting a malicious website OR viewing a malicious advertizement could allow an attacker to remotely compromise your computer. Do not disable this feature unless you truly understand the risks.
+
+Read more about the risks of this vulnerability on Oligo's blog: [Critical RCE Vulnerability in Anthropic MCP Inspector - CVE-2025-49596](https://www.oligo.security/blog/critical-rce-vulnerability-in-anthropic-mcp-inspector-cve-2025-49596)
+
+---
+
You can also set the token via the `MCP_PROXY_AUTH_TOKEN` environment variable when starting the server:
```bash
@@ -234,6 +252,78 @@ Example server configuration file:
}
```
+#### Transport Types in Config Files
+
+The inspector automatically detects the transport type from your config file. You can specify different transport types:
+
+**STDIO (default):**
+
+```json
+{
+ "mcpServers": {
+ "my-stdio-server": {
+ "type": "stdio",
+ "command": "npx",
+ "args": ["@modelcontextprotocol/server-everything"]
+ }
+ }
+}
+```
+
+**SSE (Server-Sent Events):**
+
+```json
+{
+ "mcpServers": {
+ "my-sse-server": {
+ "type": "sse",
+ "url": "http://localhost:3000/sse"
+ }
+ }
+}
+```
+
+**Streamable HTTP:**
+
+```json
+{
+ "mcpServers": {
+ "my-http-server": {
+ "type": "streamable-http",
+ "url": "http://localhost:3000/mcp"
+ }
+ }
+}
+```
+
+#### Default Server Selection
+
+You can launch the inspector without specifying a server name if your config has:
+
+1. **A single server** - automatically selected:
+
+```bash
+# Automatically uses "my-server" if it's the only one
+npx @modelcontextprotocol/inspector --config mcp.json
+```
+
+2. **A server named "default-server"** - automatically selected:
+
+```json
+{
+ "mcpServers": {
+ "default-server": {
+ "command": "npx",
+ "args": ["@modelcontextprotocol/server-everything"]
+ },
+ "other-server": {
+ "command": "node",
+ "args": ["other.js"]
+ }
+ }
+}
+```
+
> **Tip:** You can easily generate this configuration format using the **Server Entry** and **Servers File** buttons in the Inspector UI, as described in the Servers File Export section above.
You can also set the initial `transport` type, `serverUrl`, `serverCommand`, and `serverArgs` via query params, for example:
@@ -305,6 +395,9 @@ npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/lis
# Call a specific tool
npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/call --tool-name mytool --tool-arg key=value --tool-arg another=value2
+# Call a tool with JSON arguments
+npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/call --tool-name mytool --tool-arg 'options={"format": "json", "max_tokens": 100}'
+
# List available resources
npx @modelcontextprotocol/inspector --cli node build/index.js --method resources/list
@@ -317,6 +410,9 @@ npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com
# Connect to a remote MCP server (with Streamable HTTP transport)
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --transport http --method tools/list
+# Connect to a remote MCP server (with custom headers)
+npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --transport http --method tools/list --header "X-API-Key: your-api-key"
+
# Call a tool on a remote server
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --method tools/call --tool-name remotetool --tool-arg param=value
diff --git a/cli/package.json b/cli/package.json
index bf344733..e274bbdf 100644
--- a/cli/package.json
+++ b/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/inspector-cli",
- "version": "0.16.2",
+ "version": "0.16.5",
"description": "CLI for the Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -17,11 +17,13 @@
"scripts": {
"build": "tsc",
"postbuild": "node scripts/make-executable.js",
- "test": "node scripts/cli-tests.js"
+ "test": "node scripts/cli-tests.js && node scripts/cli-tool-tests.js",
+ "test:cli": "node scripts/cli-tests.js",
+ "test:cli-tools": "node scripts/cli-tool-tests.js"
},
"devDependencies": {},
"dependencies": {
- "@modelcontextprotocol/sdk": "^1.17.0",
+ "@modelcontextprotocol/sdk": "^1.17.3",
"commander": "^13.1.0",
"spawn-rx": "^5.1.2"
}
diff --git a/cli/scripts/cli-tests.js b/cli/scripts/cli-tests.js
index 68ce3885..554a5262 100755
--- a/cli/scripts/cli-tests.js
+++ b/cli/scripts/cli-tests.js
@@ -37,9 +37,6 @@ console.log(`${colors.BLUE}- Environment variables (-e)${colors.NC}`);
console.log(`${colors.BLUE}- Config file (--config)${colors.NC}`);
console.log(`${colors.BLUE}- Server selection (--server)${colors.NC}`);
console.log(`${colors.BLUE}- Method selection (--method)${colors.NC}`);
-console.log(
- `${colors.BLUE}- Tool-related options (--tool-name, --tool-arg)${colors.NC}`,
-);
console.log(`${colors.BLUE}- Resource-related options (--uri)${colors.NC}`);
console.log(
`${colors.BLUE}- Prompt-related options (--prompt-name, --prompt-args)${colors.NC}`,
@@ -120,6 +117,85 @@ try {
const invalidConfigPath = path.join(TEMP_DIR, "invalid-config.json");
fs.writeFileSync(invalidConfigPath, '{\n "mcpServers": {\n "invalid": {');
+// Create config files with different transport types for testing
+const sseConfigPath = path.join(TEMP_DIR, "sse-config.json");
+fs.writeFileSync(
+ sseConfigPath,
+ JSON.stringify(
+ {
+ mcpServers: {
+ "test-sse": {
+ type: "sse",
+ url: "http://localhost:3000/sse",
+ note: "Test SSE server",
+ },
+ },
+ },
+ null,
+ 2,
+ ),
+);
+
+const httpConfigPath = path.join(TEMP_DIR, "http-config.json");
+fs.writeFileSync(
+ httpConfigPath,
+ JSON.stringify(
+ {
+ mcpServers: {
+ "test-http": {
+ type: "streamable-http",
+ url: "http://localhost:3000/mcp",
+ note: "Test HTTP server",
+ },
+ },
+ },
+ null,
+ 2,
+ ),
+);
+
+const stdioConfigPath = path.join(TEMP_DIR, "stdio-config.json");
+fs.writeFileSync(
+ stdioConfigPath,
+ JSON.stringify(
+ {
+ mcpServers: {
+ "test-stdio": {
+ type: "stdio",
+ command: "npx",
+ args: ["@modelcontextprotocol/server-everything"],
+ env: {
+ TEST_ENV: "test-value",
+ },
+ },
+ },
+ },
+ null,
+ 2,
+ ),
+);
+
+// Config without type field (backward compatibility)
+const legacyConfigPath = path.join(TEMP_DIR, "legacy-config.json");
+fs.writeFileSync(
+ legacyConfigPath,
+ JSON.stringify(
+ {
+ mcpServers: {
+ "test-legacy": {
+ command: "npx",
+ args: ["@modelcontextprotocol/server-everything"],
+ env: {
+ LEGACY_ENV: "legacy-value",
+ },
+ },
+ },
+ },
+ null,
+ 2,
+ ),
+);
+
// Function to run a basic test
async function runBasicTest(testName, ...args) {
const outputFile = path.join(
@@ -454,65 +530,6 @@ async function runTests() {
"tools/list",
);
- console.log(
- `\n${colors.YELLOW}=== Running Tool-Related Tests ===${colors.NC}`,
- );
-
- // Test 12: CLI mode with tool call
- await runBasicTest(
- "tool_call",
- TEST_CMD,
- ...TEST_ARGS,
- "--cli",
- "--method",
- "tools/call",
- "--tool-name",
- "echo",
- "--tool-arg",
- "message=Hello",
- );
-
- // Test 13: CLI mode with tool call but missing tool name (should fail)
- await runErrorTest(
- "missing_tool_name",
- TEST_CMD,
- ...TEST_ARGS,
- "--cli",
- "--method",
- "tools/call",
- "--tool-arg",
- "message=Hello",
- );
-
- // Test 14: CLI mode with tool call but invalid tool args format (should fail)
- await runErrorTest(
- "invalid_tool_args",
- TEST_CMD,
- ...TEST_ARGS,
- "--cli",
- "--method",
- "tools/call",
- "--tool-name",
- "echo",
- "--tool-arg",
- "invalid_format",
- );
-
- // Test 15: CLI mode with multiple tool args
- await runBasicTest(
- "multiple_tool_args",
- TEST_CMD,
- ...TEST_ARGS,
- "--cli",
- "--method",
- "tools/call",
- "--tool-name",
- "add",
- "--tool-arg",
- "a=1",
- "b=2",
- );
-
console.log(
`\n${colors.YELLOW}=== Running Resource-Related Tests ===${colors.NC}`,
);
@@ -649,6 +666,160 @@ async function runTests() {
"debug",
);
+ console.log(
+ `\n${colors.YELLOW}=== Running Config Transport Type Tests ===${colors.NC}`,
+ );
+
+ // Test 25: Config with stdio transport type
+ await runBasicTest(
+ "config_stdio_type",
+ "--config",
+ stdioConfigPath,
+ "--server",
+ "test-stdio",
+ "--cli",
+ "--method",
+ "tools/list",
+ );
+
+ // Test 26: Config with SSE transport type (CLI mode) - expects connection error
+ await runErrorTest(
+ "config_sse_type_cli",
+ "--config",
+ sseConfigPath,
+ "--server",
+ "test-sse",
+ "--cli",
+ "--method",
+ "tools/list",
+ );
+
+ // Test 27: Config with streamable-http transport type (CLI mode) - expects connection error
+ await runErrorTest(
+ "config_http_type_cli",
+ "--config",
+ httpConfigPath,
+ "--server",
+ "test-http",
+ "--cli",
+ "--method",
+ "tools/list",
+ );
+
+ // Test 28: Legacy config without type field (backward compatibility)
+ await runBasicTest(
+ "config_legacy_no_type",
+ "--config",
+ legacyConfigPath,
+ "--server",
+ "test-legacy",
+ "--cli",
+ "--method",
+ "tools/list",
+ );
+
+ console.log(
+ `\n${colors.YELLOW}=== Running Default Server Tests ===${colors.NC}`,
+ );
+
+ // Create config with single server for auto-selection
+ const singleServerConfigPath = path.join(
+ TEMP_DIR,
+ "single-server-config.json",
+ );
+ fs.writeFileSync(
+ singleServerConfigPath,
+ JSON.stringify(
+ {
+ mcpServers: {
+ "only-server": {
+ command: "npx",
+ args: ["@modelcontextprotocol/server-everything"],
+ },
+ },
+ },
+ null,
+ 2,
+ ),
+ );
+
+ // Create config with default-server
+ const defaultServerConfigPath = path.join(
+ TEMP_DIR,
+ "default-server-config.json",
+ );
+ fs.writeFileSync(
+ defaultServerConfigPath,
+ JSON.stringify(
+ {
+ mcpServers: {
+ "default-server": {
+ command: "npx",
+ args: ["@modelcontextprotocol/server-everything"],
+ },
+ "other-server": {
+ command: "node",
+ args: ["other.js"],
+ },
+ },
+ },
+ null,
+ 2,
+ ),
+ );
+
+ // Create config with multiple servers (no default)
+ const multiServerConfigPath = path.join(TEMP_DIR, "multi-server-config.json");
+ fs.writeFileSync(
+ multiServerConfigPath,
+ JSON.stringify(
+ {
+ mcpServers: {
+ server1: {
+ command: "npx",
+ args: ["@modelcontextprotocol/server-everything"],
+ },
+ server2: {
+ command: "node",
+ args: ["other.js"],
+ },
+ },
+ },
+ null,
+ 2,
+ ),
+ );
+
+ // Test 29: Config with single server auto-selection
+ await runBasicTest(
+ "single_server_auto_select",
+ "--config",
+ singleServerConfigPath,
+ "--cli",
+ "--method",
+ "tools/list",
+ );
+
+ // Test 30: Config with default-server should now require explicit selection (multiple servers)
+ await runErrorTest(
+ "default_server_requires_explicit_selection",
+ "--config",
+ defaultServerConfigPath,
+ "--cli",
+ "--method",
+ "tools/list",
+ );
+
+ // Test 31: Config with multiple servers and no default (should fail)
+ await runErrorTest(
+ "multi_server_no_default",
+ "--config",
+ multiServerConfigPath,
+ "--cli",
+ "--method",
+ "tools/list",
+ );
+
console.log(
`\n${colors.YELLOW}=== Running HTTP Transport Tests ===${colors.NC}`,
);
@@ -668,7 +839,7 @@ async function runTests() {
await new Promise((resolve) => setTimeout(resolve, 3000));
- // Test 25: HTTP transport inferred from URL ending with /mcp
+ // Test 32: HTTP transport inferred from URL ending with /mcp
await runBasicTest(
"http_transport_inferred",
"http://127.0.0.1:3001/mcp",
@@ -677,7 +848,7 @@ async function runTests() {
"tools/list",
);
- // Test 26: HTTP transport with explicit --transport http flag
+ // Test 33: HTTP transport with explicit --transport http flag
await runBasicTest(
"http_transport_with_explicit_flag",
"http://127.0.0.1:3001/mcp",
@@ -688,7 +859,7 @@ async function runTests() {
"tools/list",
);
- // Test 27: HTTP transport with suffix and --transport http flag
+ // Test 34: HTTP transport with suffix and --transport http flag
await runBasicTest(
"http_transport_with_explicit_flag_and_suffix",
"http://127.0.0.1:3001/mcp",
@@ -699,7 +870,7 @@ async function runTests() {
"tools/list",
);
- // Test 28: SSE transport given to HTTP server (should fail)
+ // Test 35: SSE transport given to HTTP server (should fail)
await runErrorTest(
"sse_transport_given_to_http_server",
"http://127.0.0.1:3001",
@@ -710,7 +881,7 @@ async function runTests() {
"tools/list",
);
- // Test 29: HTTP transport without URL (should fail)
+ // Test 36: HTTP transport without URL (should fail)
await runErrorTest(
"http_transport_without_url",
"--transport",
@@ -720,7 +891,7 @@ async function runTests() {
"tools/list",
);
- // Test 30: SSE transport without URL (should fail)
+ // Test 37: SSE transport without URL (should fail)
await runErrorTest(
"sse_transport_without_url",
"--transport",
diff --git a/cli/scripts/cli-tool-tests.js b/cli/scripts/cli-tool-tests.js
new file mode 100644
index 00000000..b06aea94
--- /dev/null
+++ b/cli/scripts/cli-tool-tests.js
@@ -0,0 +1,614 @@
+#!/usr/bin/env node
+
+// Colors for output
+const colors = {
+ GREEN: "\x1b[32m",
+ YELLOW: "\x1b[33m",
+ RED: "\x1b[31m",
+ BLUE: "\x1b[34m",
+ ORANGE: "\x1b[33m",
+ NC: "\x1b[0m", // No Color
+};
+
+import fs from "fs";
+import path from "path";
+import { spawn } from "child_process";
+import os from "os";
+import { fileURLToPath } from "url";
+
+// Get directory paths with ESM compatibility
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+// Track test results
+let PASSED_TESTS = 0;
+let FAILED_TESTS = 0;
+let SKIPPED_TESTS = 0;
+let TOTAL_TESTS = 0;
+
+console.log(`${colors.YELLOW}=== MCP Inspector CLI Tool Tests ===${colors.NC}`);
+console.log(
+ `${colors.BLUE}This script tests the MCP Inspector CLI's tool-related functionality:${colors.NC}`,
+);
+console.log(`${colors.BLUE}- Tool discovery and listing${colors.NC}`);
+console.log(
+ `${colors.BLUE}- JSON argument parsing (strings, numbers, booleans, objects, arrays)${colors.NC}`,
+);
+console.log(`${colors.BLUE}- Tool schema validation${colors.NC}`);
+console.log(
+ `${colors.BLUE}- Tool execution with various argument types${colors.NC}`,
+);
+console.log(
+ `${colors.BLUE}- Error handling for invalid tools and arguments${colors.NC}`,
+);
+console.log(`\n`);
+
+// Get directory paths
+const SCRIPTS_DIR = __dirname;
+const PROJECT_ROOT = path.join(SCRIPTS_DIR, "../../");
+const BUILD_DIR = path.resolve(SCRIPTS_DIR, "../build");
+
+// Define the test server command using npx
+const TEST_CMD = "npx";
+const TEST_ARGS = ["@modelcontextprotocol/server-everything"];
+
+// Create output directory for test results
+const OUTPUT_DIR = path.join(SCRIPTS_DIR, "tool-test-output");
+if (!fs.existsSync(OUTPUT_DIR)) {
+ fs.mkdirSync(OUTPUT_DIR, { recursive: true });
+}
+
+// Create a temporary directory for test files
+const TEMP_DIR = path.join(os.tmpdir(), "mcp-inspector-tool-tests");
+fs.mkdirSync(TEMP_DIR, { recursive: true });
+
+// Track servers for cleanup
+let runningServers = [];
+
+process.on("exit", () => {
+ try {
+ fs.rmSync(TEMP_DIR, { recursive: true, force: true });
+ } catch (err) {
+ console.error(
+ `${colors.RED}Failed to remove temp directory: ${err.message}${colors.NC}`,
+ );
+ }
+
+ runningServers.forEach((server) => {
+ try {
+ process.kill(-server.pid);
+ } catch (e) {}
+ });
+});
+
+process.on("SIGINT", () => {
+ runningServers.forEach((server) => {
+ try {
+ process.kill(-server.pid);
+ } catch (e) {}
+ });
+ process.exit(1);
+});
+
+// Function to run a basic test
+async function runBasicTest(testName, ...args) {
+ const outputFile = path.join(
+ OUTPUT_DIR,
+ `${testName.replace(/\//g, "_")}.log`,
+ );
+
+ console.log(`\n${colors.YELLOW}Testing: ${testName}${colors.NC}`);
+ TOTAL_TESTS++;
+
+ // Run the command and capture output
+ console.log(
+ `${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`,
+ );
+
+ try {
+ // Create a write stream for the output file
+ const outputStream = fs.createWriteStream(outputFile);
+
+ // Spawn the process
+ return new Promise((resolve) => {
+ const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], {
+ stdio: ["ignore", "pipe", "pipe"],
+ });
+
+ const timeout = setTimeout(() => {
+ console.log(`${colors.YELLOW}Test timed out: ${testName}${colors.NC}`);
+ child.kill();
+ }, 10000);
+
+ // Pipe stdout and stderr to the output file
+ child.stdout.pipe(outputStream);
+ child.stderr.pipe(outputStream);
+
+ // Also capture output for display
+ let output = "";
+ child.stdout.on("data", (data) => {
+ output += data.toString();
+ });
+ child.stderr.on("data", (data) => {
+ output += data.toString();
+ });
+
+ child.on("close", (code) => {
+ clearTimeout(timeout);
+ outputStream.end();
+
+ if (code === 0) {
+ console.log(`${colors.GREEN}✓ Test passed: ${testName}${colors.NC}`);
+ console.log(`${colors.BLUE}First few lines of output:${colors.NC}`);
+ const firstFewLines = output
+ .split("\n")
+ .slice(0, 5)
+ .map((line) => ` ${line}`)
+ .join("\n");
+ console.log(firstFewLines);
+ PASSED_TESTS++;
+ resolve(true);
+ } else {
+ console.log(`${colors.RED}✗ Test failed: ${testName}${colors.NC}`);
+ console.log(`${colors.RED}Error output:${colors.NC}`);
+ console.log(
+ output
+ .split("\n")
+ .map((line) => ` ${line}`)
+ .join("\n"),
+ );
+ FAILED_TESTS++;
+
+ // Stop after any error is encountered
+ console.log(
+ `${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`,
+ );
+ process.exit(1);
+ }
+ });
+ });
+ } catch (error) {
+ console.error(
+ `${colors.RED}Error running test: ${error.message}${colors.NC}`,
+ );
+ FAILED_TESTS++;
+ process.exit(1);
+ }
+}
+
+// Function to run an error test (expected to fail)
+async function runErrorTest(testName, ...args) {
+ const outputFile = path.join(
+ OUTPUT_DIR,
+ `${testName.replace(/\//g, "_")}.log`,
+ );
+
+ console.log(`\n${colors.YELLOW}Testing error case: ${testName}${colors.NC}`);
+ TOTAL_TESTS++;
+
+ // Run the command and capture output
+ console.log(
+ `${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`,
+ );
+
+ try {
+ // Create a write stream for the output file
+ const outputStream = fs.createWriteStream(outputFile);
+
+ // Spawn the process
+ return new Promise((resolve) => {
+ const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], {
+ stdio: ["ignore", "pipe", "pipe"],
+ });
+
+ const timeout = setTimeout(() => {
+ console.log(
+ `${colors.YELLOW}Error test timed out: ${testName}${colors.NC}`,
+ );
+ child.kill();
+ }, 10000);
+
+ // Pipe stdout and stderr to the output file
+ child.stdout.pipe(outputStream);
+ child.stderr.pipe(outputStream);
+
+ // Also capture output for display
+ let output = "";
+ child.stdout.on("data", (data) => {
+ output += data.toString();
+ });
+ child.stderr.on("data", (data) => {
+ output += data.toString();
+ });
+
+ child.on("close", (code) => {
+ clearTimeout(timeout);
+ outputStream.end();
+
+ // For error tests, we expect a non-zero exit code
+ if (code !== 0) {
+ console.log(
+ `${colors.GREEN}✓ Error test passed: ${testName}${colors.NC}`,
+ );
+ console.log(`${colors.BLUE}Error output (expected):${colors.NC}`);
+ const firstFewLines = output
+ .split("\n")
+ .slice(0, 5)
+ .map((line) => ` ${line}`)
+ .join("\n");
+ console.log(firstFewLines);
+ PASSED_TESTS++;
+ resolve(true);
+ } else {
+ console.log(
+ `${colors.RED}✗ Error test failed: ${testName} (expected error but got success)${colors.NC}`,
+ );
+ console.log(`${colors.RED}Output:${colors.NC}`);
+ console.log(
+ output
+ .split("\n")
+ .map((line) => ` ${line}`)
+ .join("\n"),
+ );
+ FAILED_TESTS++;
+
+ // Stop after any error is encountered
+ console.log(
+ `${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`,
+ );
+ process.exit(1);
+ }
+ });
+ });
+ } catch (error) {
+ console.error(
+ `${colors.RED}Error running test: ${error.message}${colors.NC}`,
+ );
+ FAILED_TESTS++;
+ process.exit(1);
+ }
+}
+
+// Run all tests
+async function runTests() {
+ console.log(
+ `\n${colors.YELLOW}=== Running Tool Discovery Tests ===${colors.NC}`,
+ );
+
+ // Test 1: List available tools
+ await runBasicTest(
+ "tool_discovery_list",
+ TEST_CMD,
+ ...TEST_ARGS,
+ "--cli",
+ "--method",
+ "tools/list",
+ );
+
+ console.log(
+ `\n${colors.YELLOW}=== Running JSON Argument Parsing Tests ===${colors.NC}`,
+ );
+
+ // Test 2: String arguments (backward compatibility)
+ await runBasicTest(
+ "json_args_string",
+ TEST_CMD,
+ ...TEST_ARGS,
+ "--cli",
+ "--method",
+ "tools/call",
+ "--tool-name",
+ "echo",
+ "--tool-arg",
+ "message=hello world",
+ );
+
+ // Test 3: Number arguments
+ await runBasicTest(
+ "json_args_number_integer",
+ TEST_CMD,
+ ...TEST_ARGS,
+ "--cli",
+ "--method",
+ "tools/call",
+ "--tool-name",
+ "add",
+ "--tool-arg",
+ "a=42",
+ "b=58",
+ );
+
+ // Test 4: Number arguments with decimals (using add tool with decimal numbers)
+ await runBasicTest(
+ "json_args_number_decimal",
+ TEST_CMD,
+ ...TEST_ARGS,
+ "--cli",
+ "--method",
+ "tools/call",
+ "--tool-name",
+ "add",
+ "--tool-arg",
+ "a=19.99",
+ "b=20.01",
+ );
+
+ // Test 5: Boolean arguments - true
+ await runBasicTest(
+ "json_args_boolean_true",
+ TEST_CMD,
+ ...TEST_ARGS,
+ "--cli",
+ "--method",
+ "tools/call",
+ "--tool-name",
+ "annotatedMessage",
+ "--tool-arg",
+ "messageType=success",
+ "includeImage=true",
+ );
+
+ // Test 6: Boolean arguments - false
+ await runBasicTest(
+ "json_args_boolean_false",
+ TEST_CMD,
+ ...TEST_ARGS,
+ "--cli",
+ "--method",
+ "tools/call",
+ "--tool-name",
+ "annotatedMessage",
+ "--tool-arg",
+ "messageType=error",
+ "includeImage=false",
+ );
+
+ // Test 7: Null arguments (using echo with string "null")
+ await runBasicTest(
+ "json_args_null",
+ TEST_CMD,
+ ...TEST_ARGS,
+ "--cli",
+ "--method",
+ "tools/call",
+ "--tool-name",
+ "echo",
+ "--tool-arg",
+ 'message="null"',
+ );
+
+ // Test 14: Multiple arguments with mixed types (using add tool)
+ await runBasicTest(
+ "json_args_multiple_mixed",
+ TEST_CMD,
+ ...TEST_ARGS,
+ "--cli",
+ "--method",
+ "tools/call",
+ "--tool-name",
+ "add",
+ "--tool-arg",
+ "a=42.5",
+ "b=57.5",
+ );
+
+ console.log(
+ `\n${colors.YELLOW}=== Running JSON Parsing Edge Cases ===${colors.NC}`,
+ );
+
+ // Test 15: Invalid JSON should fall back to string
+ await runBasicTest(
+ "json_args_invalid_fallback",
+ TEST_CMD,
+ ...TEST_ARGS,
+ "--cli",
+ "--method",
+ "tools/call",
+ "--tool-name",
+ "echo",
+ "--tool-arg",
+ "message={invalid json}",
+ );
+
+ // Test 16: Empty string value
+ await runBasicTest(
+ "json_args_empty_value",
+ TEST_CMD,
+ ...TEST_ARGS,
+ "--cli",
+ "--method",
+ "tools/call",
+ "--tool-name",
+ "echo",
+ "--tool-arg",
+ 'message=""',
+ );
+
+ // Test 17: Special characters in strings
+ await runBasicTest(
+ "json_args_special_chars",
+ TEST_CMD,
+ ...TEST_ARGS,
+ "--cli",
+ "--method",
+ "tools/call",
+ "--tool-name",
+ "echo",
+ "--tool-arg",
+ 'message="C:\\\\Users\\\\test"',
+ );
+
+ // Test 18: Unicode characters
+ await runBasicTest(
+ "json_args_unicode",
+ TEST_CMD,
+ ...TEST_ARGS,
+ "--cli",
+ "--method",
+ "tools/call",
+ "--tool-name",
+ "echo",
+ "--tool-arg",
+ 'message="🚀🎉✨"',
+ );
+
+ // Test 19: Arguments with equals signs in values
+ await runBasicTest(
+ "json_args_equals_in_value",
+ TEST_CMD,
+ ...TEST_ARGS,
+ "--cli",
+ "--method",
+ "tools/call",
+ "--tool-name",
+ "echo",
+ "--tool-arg",
+ "message=2+2=4",
+ );
+
+ // Test 20: Base64-like strings
+ await runBasicTest(
+ "json_args_base64_like",
+ TEST_CMD,
+ ...TEST_ARGS,
+ "--cli",
+ "--method",
+ "tools/call",
+ "--tool-name",
+ "echo",
+ "--tool-arg",
+ "message=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=",
+ );
+
+ console.log(
+ `\n${colors.YELLOW}=== Running Tool Error Handling Tests ===${colors.NC}`,
+ );
+
+ // Test 21: Non-existent tool
+ await runErrorTest(
+ "tool_error_nonexistent",
+ TEST_CMD,
+ ...TEST_ARGS,
+ "--cli",
+ "--method",
+ "tools/call",
+ "--tool-name",
+ "nonexistent_tool",
+ "--tool-arg",
+ "message=test",
+ );
+
+ // Test 22: Missing tool name
+ await runErrorTest(
+ "tool_error_missing_name",
+ TEST_CMD,
+ ...TEST_ARGS,
+ "--cli",
+ "--method",
+ "tools/call",
+ "--tool-arg",
+ "message=test",
+ );
+
+ // Test 23: Invalid tool argument format
+ await runErrorTest(
+ "tool_error_invalid_arg_format",
+ TEST_CMD,
+ ...TEST_ARGS,
+ "--cli",
+ "--method",
+ "tools/call",
+ "--tool-name",
+ "echo",
+ "--tool-arg",
+ "invalid_format_no_equals",
+ );
+
+ console.log(
+ `\n${colors.YELLOW}=== Running Prompt JSON Argument Tests ===${colors.NC}`,
+ );
+
+ // Test 24: Prompt with JSON arguments
+ await runBasicTest(
+ "prompt_json_args_mixed",
+ TEST_CMD,
+ ...TEST_ARGS,
+ "--cli",
+ "--method",
+ "prompts/get",
+ "--prompt-name",
+ "complex_prompt",
+ "--prompt-args",
+ "temperature=0.7",
+ 'style="concise"',
+ 'options={"format":"json","max_tokens":100}',
+ );
+
+ // Test 25: Prompt with simple arguments
+ await runBasicTest(
+ "prompt_json_args_simple",
+ TEST_CMD,
+ ...TEST_ARGS,
+ "--cli",
+ "--method",
+ "prompts/get",
+ "--prompt-name",
+ "simple_prompt",
+ "--prompt-args",
+ "name=test",
+ "count=5",
+ );
+
+ console.log(
+ `\n${colors.YELLOW}=== Running Backward Compatibility Tests ===${colors.NC}`,
+ );
+
+ // Test 26: Ensure existing string-only usage still works
+ await runBasicTest(
+ "backward_compatibility_strings",
+ TEST_CMD,
+ ...TEST_ARGS,
+ "--cli",
+ "--method",
+ "tools/call",
+ "--tool-name",
+ "echo",
+ "--tool-arg",
+ "message=hello",
+ );
+
+ // Test 27: Multiple string arguments (existing pattern) - using add tool
+ await runBasicTest(
+ "backward_compatibility_multiple_strings",
+ TEST_CMD,
+ ...TEST_ARGS,
+ "--cli",
+ "--method",
+ "tools/call",
+ "--tool-name",
+ "add",
+ "--tool-arg",
+ "a=10",
+ "b=20",
+ );
+
+ // Print test summary
+ console.log(`\n${colors.YELLOW}=== Test Summary ===${colors.NC}`);
+ console.log(`${colors.GREEN}Passed: ${PASSED_TESTS}${colors.NC}`);
+ console.log(`${colors.RED}Failed: ${FAILED_TESTS}${colors.NC}`);
+ console.log(`${colors.ORANGE}Skipped: ${SKIPPED_TESTS}${colors.NC}`);
+ console.log(`Total: ${TOTAL_TESTS}`);
+ console.log(
+ `${colors.BLUE}Detailed logs saved to: ${OUTPUT_DIR}${colors.NC}`,
+ );
+
+ console.log(`\n${colors.GREEN}All tool tests completed!${colors.NC}`);
+}
+
+// Run all tests
+runTests().catch((error) => {
+ console.error(
+ `${colors.RED}Tests failed with error: ${error.message}${colors.NC}`,
+ );
+ process.exit(1);
+});
diff --git a/cli/src/cli.ts b/cli/src/cli.ts
index 5ff1f111..7188f7c8 100644
--- a/cli/src/cli.ts
+++ b/cli/src/cli.ts
@@ -14,6 +14,9 @@ type Args = {
args: string[];
envArgs: Record;
cli: boolean;
+ transport?: "stdio" | "sse" | "streamable-http";
+ serverUrl?: string;
+ headers?: Record;
};
type CliOptions = {
@@ -21,13 +24,23 @@ type CliOptions = {
config?: string;
server?: string;
cli?: boolean;
+ transport?: string;
+ serverUrl?: string;
+ header?: Record;
};
-type ServerConfig = {
- command: string;
- args?: string[];
- env?: Record;
-};
+type ServerConfig =
+ | {
+ type: "stdio";
+ command: string;
+ args?: string[];
+ env?: Record;
+ }
+ | {
+ type: "sse" | "streamable-http";
+ url: string;
+ note?: string;
+ };
function handleError(error: unknown): never {
let message: string;
@@ -74,6 +87,16 @@ async function runWebClient(args: Args): Promise {
startArgs.push("-e", `${key}=${value}`);
}
+ // Pass transport type if specified
+ if (args.transport) {
+ startArgs.push("--transport", args.transport);
+ }
+
+ // Pass server URL if specified
+ if (args.serverUrl) {
+ startArgs.push("--server-url", args.serverUrl);
+ }
+
// Pass command and args (using -- to separate them)
if (args.command) {
startArgs.push("--", args.command, ...args.args);
@@ -103,7 +126,28 @@ async function runCli(args: Args): Promise {
});
try {
- await spawnPromise("node", [cliPath, args.command, ...args.args], {
+ // Build CLI arguments
+ const cliArgs = [cliPath];
+
+ // Add target URL/command first
+ cliArgs.push(args.command, ...args.args);
+
+ // Add transport flag if specified
+ if (args.transport && args.transport !== "stdio") {
+ // Convert streamable-http back to http for CLI mode
+ const cliTransport =
+ args.transport === "streamable-http" ? "http" : args.transport;
+ cliArgs.push("--transport", cliTransport);
+ }
+
+ // Add headers if specified
+ if (args.headers) {
+ for (const [key, value] of Object.entries(args.headers)) {
+ cliArgs.push("--header", `${key}: ${value}`);
+ }
+ }
+
+ await spawnPromise("node", cliArgs, {
env: { ...process.env, ...args.envArgs },
signal: abort.signal,
echoOutput: true,
@@ -166,6 +210,30 @@ function parseKeyValuePair(
return { ...previous, [key as string]: val };
}
+function parseHeaderPair(
+ value: string,
+ previous: Record = {},
+): Record {
+ const colonIndex = value.indexOf(":");
+
+ if (colonIndex === -1) {
+ throw new Error(
+ `Invalid header format: ${value}. Use "HeaderName: Value" format.`,
+ );
+ }
+
+ const key = value.slice(0, colonIndex).trim();
+ const val = value.slice(colonIndex + 1).trim();
+
+ if (key === "" || val === "") {
+ throw new Error(
+ `Invalid header format: ${value}. Use "HeaderName: Value" format.`,
+ );
+ }
+
+ return { ...previous, [key]: val };
+}
+
function parseArgs(): Args {
const program = new Command();
@@ -190,7 +258,15 @@ function parseArgs(): Args {
)
.option("--config ", "config file path")
.option("--server ", "server name from config file")
- .option("--cli", "enable CLI mode");
+ .option("--cli", "enable CLI mode")
+ .option("--transport ", "transport type (stdio, sse, http)")
+ .option("--server-url ", "server URL for SSE/HTTP transport")
+ .option(
+ "--header ",
+ 'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)',
+ parseHeaderPair,
+ {},
+ );
// Parse only the arguments before --
program.parse(preArgs);
@@ -201,14 +277,33 @@ function parseArgs(): Args {
// Add back any arguments that came after --
const finalArgs = [...remainingArgs, ...postArgs];
- // Validate that config and server are provided together
- if (
- (options.config && !options.server) ||
- (!options.config && options.server)
- ) {
- throw new Error(
- "Both --config and --server must be provided together. If you specify one, you must specify the other.",
+ // Validate config and server options
+ if (!options.config && options.server) {
+ throw new Error("--server requires --config to be specified");
+ }
+
+ // If config is provided without server, try to auto-select
+ if (options.config && !options.server) {
+ const configContent = fs.readFileSync(
+ path.isAbsolute(options.config)
+ ? options.config
+ : path.resolve(process.cwd(), options.config),
+ "utf8",
);
+ const parsedConfig = JSON.parse(configContent);
+ const servers = Object.keys(parsedConfig.mcpServers || {});
+
+ if (servers.length === 1) {
+ // Use the only server if there's just one
+ options.server = servers[0];
+ } else if (servers.length === 0) {
+ throw new Error("No servers found in config file");
+ } else {
+ // Multiple servers, require explicit selection
+ throw new Error(
+ `Multiple servers found in config file. Please specify one with --server.\nAvailable servers: ${servers.join(", ")}`,
+ );
+ }
}
// If config file is specified, load and use the options from the file. We must merge the args
@@ -217,23 +312,56 @@ function parseArgs(): Args {
if (options.config && options.server) {
const config = loadConfigFile(options.config, options.server);
- return {
- command: config.command,
- args: [...(config.args || []), ...finalArgs],
- envArgs: { ...(config.env || {}), ...(options.e || {}) },
- cli: options.cli || false,
- };
+ if (config.type === "stdio") {
+ return {
+ command: config.command,
+ args: [...(config.args || []), ...finalArgs],
+ envArgs: { ...(config.env || {}), ...(options.e || {}) },
+ cli: options.cli || false,
+ transport: "stdio",
+ headers: options.header,
+ };
+ } else if (config.type === "sse" || config.type === "streamable-http") {
+ return {
+ command: config.url,
+ args: finalArgs,
+ envArgs: options.e || {},
+ cli: options.cli || false,
+ transport: config.type,
+ serverUrl: config.url,
+ headers: options.header,
+ };
+ } else {
+ // Backwards compatibility: if no type field, assume stdio
+ return {
+ command: (config as any).command || "",
+ args: [...((config as any).args || []), ...finalArgs],
+ envArgs: { ...((config as any).env || {}), ...(options.e || {}) },
+ cli: options.cli || false,
+ transport: "stdio",
+ headers: options.header,
+ };
+ }
}
// Otherwise use command line arguments
const command = finalArgs[0] || "";
const args = finalArgs.slice(1);
+ // Map "http" shorthand to "streamable-http"
+ let transport = options.transport;
+ if (transport === "http") {
+ transport = "streamable-http";
+ }
+
return {
command,
args,
envArgs: options.e || {},
cli: options.cli || false,
+ transport: transport as "stdio" | "sse" | "streamable-http" | undefined,
+ serverUrl: options.serverUrl,
+ headers: options.header,
};
}
diff --git a/cli/src/client/connection.ts b/cli/src/client/connection.ts
index 931f803d..dcbe8e51 100644
--- a/cli/src/client/connection.ts
+++ b/cli/src/client/connection.ts
@@ -18,6 +18,12 @@ export async function connect(
): Promise {
try {
await client.connect(transport);
+
+ if (client.getServerCapabilities()?.logging) {
+ // default logging level is undefined in the spec, but the user of the
+ // inspector most likely wants debug.
+ await client.setLoggingLevel("debug");
+ }
} catch (error) {
throw new Error(
`Failed to connect to MCP server: ${error instanceof Error ? error.message : String(error)}`,
diff --git a/cli/src/client/prompts.ts b/cli/src/client/prompts.ts
index 0b237496..87014617 100644
--- a/cli/src/client/prompts.ts
+++ b/cli/src/client/prompts.ts
@@ -1,6 +1,16 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { McpResponse } from "./types.js";
+// JSON value type matching the client utils
+type JsonValue =
+ | string
+ | number
+ | boolean
+ | null
+ | undefined
+ | JsonValue[]
+ | { [key: string]: JsonValue };
+
// List available prompts
export async function listPrompts(client: Client): Promise {
try {
@@ -17,12 +27,26 @@ export async function listPrompts(client: Client): Promise {
export async function getPrompt(
client: Client,
name: string,
- args?: Record,
+ args?: Record,
): Promise {
try {
+ // Convert all arguments to strings for prompt arguments
+ const stringArgs: Record = {};
+ if (args) {
+ for (const [key, value] of Object.entries(args)) {
+ if (typeof value === "string") {
+ stringArgs[key] = value;
+ } else if (value === null || value === undefined) {
+ stringArgs[key] = String(value);
+ } else {
+ stringArgs[key] = JSON.stringify(value);
+ }
+ }
+ }
+
const response = await client.getPrompt({
name,
- arguments: args || {},
+ arguments: stringArgs,
});
return response;
diff --git a/cli/src/client/tools.ts b/cli/src/client/tools.ts
index acdb4871..9da0b4d9 100644
--- a/cli/src/client/tools.ts
+++ b/cli/src/client/tools.ts
@@ -2,6 +2,16 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import { McpResponse } from "./types.js";
+// JSON value type matching the client utils
+type JsonValue =
+ | string
+ | number
+ | boolean
+ | null
+ | undefined
+ | JsonValue[]
+ | { [key: string]: JsonValue };
+
type JsonSchemaType = {
type: "string" | "number" | "integer" | "boolean" | "array" | "object";
description?: string;
@@ -20,7 +30,10 @@ export async function listTools(client: Client): Promise {
}
}
-function convertParameterValue(value: string, schema: JsonSchemaType): unknown {
+function convertParameterValue(
+ value: string,
+ schema: JsonSchemaType,
+): JsonValue {
if (!value) {
return value;
}
@@ -35,7 +48,7 @@ function convertParameterValue(value: string, schema: JsonSchemaType): unknown {
if (schema.type === "object" || schema.type === "array") {
try {
- return JSON.parse(value);
+ return JSON.parse(value) as JsonValue;
} catch (error) {
return value;
}
@@ -47,8 +60,8 @@ function convertParameterValue(value: string, schema: JsonSchemaType): unknown {
function convertParameters(
tool: Tool,
params: Record,
-): Record {
- const result: Record = {};
+): Record {
+ const result: Record = {};
const properties = tool.inputSchema.properties || {};
for (const [key, value] of Object.entries(params)) {
@@ -68,18 +81,29 @@ function convertParameters(
export async function callTool(
client: Client,
name: string,
- args: Record,
+ args: Record,
): Promise {
try {
const toolsResponse = await listTools(client);
const tools = toolsResponse.tools as Tool[];
const tool = tools.find((t) => t.name === name);
- let convertedArgs: Record = args;
+ let convertedArgs: Record = args;
if (tool) {
- // Convert parameters based on the tool's schema
- convertedArgs = convertParameters(tool, args);
+ // Convert parameters based on the tool's schema, but only for string values
+ // since we now accept pre-parsed values from the CLI
+ const stringArgs: Record = {};
+ for (const [key, value] of Object.entries(args)) {
+ if (typeof value === "string") {
+ stringArgs[key] = value;
+ }
+ }
+
+ if (Object.keys(stringArgs).length > 0) {
+ const convertedStringArgs = convertParameters(tool, stringArgs);
+ convertedArgs = { ...args, ...convertedStringArgs };
+ }
}
const response = await client.callTool({
diff --git a/cli/src/index.ts b/cli/src/index.ts
index 2b0c4f53..2e8811ac 100644
--- a/cli/src/index.ts
+++ b/cli/src/index.ts
@@ -19,22 +19,35 @@ import {
} from "./client/index.js";
import { handleError } from "./error-handler.js";
import { createTransport, TransportOptions } from "./transport.js";
+import { awaitableLog } from "./utils/awaitable-log.js";
+
+// JSON value type for CLI arguments
+type JsonValue =
+ | string
+ | number
+ | boolean
+ | null
+ | undefined
+ | JsonValue[]
+ | { [key: string]: JsonValue };
type Args = {
target: string[];
method?: string;
promptName?: string;
- promptArgs?: Record;
+ promptArgs?: Record;
uri?: string;
logLevel?: LogLevel;
toolName?: string;
- toolArg?: Record;
+ toolArg?: Record;
transport?: "sse" | "stdio" | "http";
+ headers?: Record;
};
function createTransportOptions(
target: string[],
transport?: "sse" | "stdio" | "http",
+ headers?: Record,
): TransportOptions {
if (target.length === 0) {
throw new Error(
@@ -81,11 +94,16 @@ function createTransportOptions(
command: isUrl ? undefined : command,
args: isUrl ? undefined : commandArgs,
url: isUrl ? command : undefined,
+ headers,
};
}
async function callMethod(args: Args): Promise {
- const transportOptions = createTransportOptions(args.target, args.transport);
+ const transportOptions = createTransportOptions(
+ args.target,
+ args.transport,
+ args.headers,
+ );
const transport = createTransport(transportOptions);
const client = new Client({
name: "inspector-cli",
@@ -150,7 +168,7 @@ async function callMethod(args: Args): Promise {
);
}
- console.log(JSON.stringify(result, null, 2));
+ await awaitableLog(JSON.stringify(result, null, 2));
} finally {
try {
await disconnect(transport);
@@ -162,8 +180,8 @@ async function callMethod(args: Args): Promise {
function parseKeyValuePair(
value: string,
- previous: Record = {},
-): Record {
+ previous: Record = {},
+): Record {
const parts = value.split("=");
const key = parts[0];
const val = parts.slice(1).join("=");
@@ -174,7 +192,40 @@ function parseKeyValuePair(
);
}
- return { ...previous, [key as string]: val };
+ // Try to parse as JSON first
+ let parsedValue: JsonValue;
+ try {
+ parsedValue = JSON.parse(val) as JsonValue;
+ } catch {
+ // If JSON parsing fails, keep as string
+ parsedValue = val;
+ }
+
+ return { ...previous, [key as string]: parsedValue };
+}
+
+function parseHeaderPair(
+ value: string,
+ previous: Record = {},
+): Record {
+ const colonIndex = value.indexOf(":");
+
+ if (colonIndex === -1) {
+ throw new Error(
+ `Invalid header format: ${value}. Use "HeaderName: Value" format.`,
+ );
+ }
+
+ const key = value.slice(0, colonIndex).trim();
+ const val = value.slice(colonIndex + 1).trim();
+
+ if (key === "" || val === "") {
+ throw new Error(
+ `Invalid header format: ${value}. Use "HeaderName: Value" format.`,
+ );
+ }
+
+ return { ...previous, [key]: val };
}
function parseArgs(): Args {
@@ -256,12 +307,24 @@ function parseArgs(): Args {
}
return value as "sse" | "http" | "stdio";
},
+ )
+ //
+ // HTTP headers
+ //
+ .option(
+ "--header ",
+ 'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)',
+ parseHeaderPair,
+ {},
);
// Parse only the arguments before --
program.parse(preArgs);
- const options = program.opts() as Omit;
+ const options = program.opts() as Omit & {
+ header?: Record;
+ };
+
let remainingArgs = program.args;
// Add back any arguments that came after --
@@ -276,6 +339,7 @@ function parseArgs(): Args {
return {
target: finalArgs,
...options,
+ headers: options.header, // commander.js uses 'header' field, map to 'headers'
};
}
@@ -287,8 +351,9 @@ async function main(): Promise {
try {
const args = parseArgs();
await callMethod(args);
- // Explicitly exit to ensure process terminates in CI
- process.exit(0);
+
+ // Let Node.js naturally exit instead of force-exiting
+ // process.exit(0) was causing stdout truncation
} catch (error) {
handleError(error);
}
diff --git a/cli/src/transport.ts b/cli/src/transport.ts
index b6276356..84af393b 100644
--- a/cli/src/transport.ts
+++ b/cli/src/transport.ts
@@ -12,6 +12,7 @@ export type TransportOptions = {
command?: string;
args?: string[];
url?: string;
+ headers?: Record;
};
function createStdioTransport(options: TransportOptions): Transport {
@@ -32,8 +33,8 @@ function createStdioTransport(options: TransportOptions): Transport {
const defaultEnv = getDefaultEnvironment();
const env: Record = {
- ...processEnv,
...defaultEnv,
+ ...processEnv,
};
const { cmd: actualCommand, args: actualArgs } = findActualExecutable(
@@ -64,11 +65,25 @@ export function createTransport(options: TransportOptions): Transport {
const url = new URL(options.url);
if (transportType === "sse") {
- return new SSEClientTransport(url);
+ const transportOptions = options.headers
+ ? {
+ requestInit: {
+ headers: options.headers,
+ },
+ }
+ : undefined;
+ return new SSEClientTransport(url, transportOptions);
}
if (transportType === "http") {
- return new StreamableHTTPClientTransport(url);
+ const transportOptions = options.headers
+ ? {
+ requestInit: {
+ headers: options.headers,
+ },
+ }
+ : undefined;
+ return new StreamableHTTPClientTransport(url, transportOptions);
}
throw new Error(`Unsupported transport type: ${transportType}`);
diff --git a/cli/src/utils/awaitable-log.ts b/cli/src/utils/awaitable-log.ts
new file mode 100644
index 00000000..144f0112
--- /dev/null
+++ b/cli/src/utils/awaitable-log.ts
@@ -0,0 +1,7 @@
+export function awaitableLog(logValue: string): Promise {
+ return new Promise((resolve) => {
+ process.stdout.write(logValue, () => {
+ resolve();
+ });
+ });
+}
diff --git a/client/bin/start.js b/client/bin/start.js
index ae6e9259..f67301de 100755
--- a/client/bin/start.js
+++ b/client/bin/start.js
@@ -28,8 +28,15 @@ function getClientUrl(port, authDisabled, sessionToken, serverPort) {
}
async function startDevServer(serverOptions) {
- const { SERVER_PORT, CLIENT_PORT, sessionToken, envVars, abort } =
- serverOptions;
+ const {
+ SERVER_PORT,
+ CLIENT_PORT,
+ sessionToken,
+ envVars,
+ abort,
+ transport,
+ serverUrl,
+ } = serverOptions;
const serverCommand = "npx";
const serverArgs = ["tsx", "watch", "--clear-screen=false", "src/index.ts"];
const isWindows = process.platform === "win32";
@@ -42,6 +49,8 @@ async function startDevServer(serverOptions) {
CLIENT_PORT,
MCP_PROXY_AUTH_TOKEN: sessionToken,
MCP_ENV_VARS: JSON.stringify(envVars),
+ ...(transport ? { MCP_TRANSPORT: transport } : {}),
+ ...(serverUrl ? { MCP_SERVER_URL: serverUrl } : {}),
},
signal: abort.signal,
echoOutput: true,
@@ -78,6 +87,8 @@ async function startProdServer(serverOptions) {
abort,
command,
mcpServerArgs,
+ transport,
+ serverUrl,
} = serverOptions;
const inspectorServerPath = resolve(
__dirname,
@@ -91,10 +102,12 @@ async function startProdServer(serverOptions) {
"node",
[
inspectorServerPath,
- ...(command ? [`--command`, command] : []),
+ ...(command ? [`--command=${command}`] : []),
...(mcpServerArgs && mcpServerArgs.length > 0
- ? [`--args`, mcpServerArgs.join(" ")]
+ ? [`--args=${mcpServerArgs.join(" ")}`]
: []),
+ ...(transport ? [`--transport=${transport}`] : []),
+ ...(serverUrl ? [`--server-url=${serverUrl}`] : []),
],
{
env: {
@@ -208,6 +221,8 @@ async function main() {
let command = null;
let parsingFlags = true;
let isDev = false;
+ let transport = null;
+ let serverUrl = null;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
@@ -222,6 +237,16 @@ async function main() {
continue;
}
+ if (parsingFlags && arg === "--transport" && i + 1 < args.length) {
+ transport = args[++i];
+ continue;
+ }
+
+ if (parsingFlags && arg === "--server-url" && i + 1 < args.length) {
+ serverUrl = args[++i];
+ continue;
+ }
+
if (parsingFlags && arg === "-e" && i + 1 < args.length) {
const envVar = args[++i];
const equalsIndex = envVar.indexOf("=");
@@ -273,6 +298,8 @@ async function main() {
abort,
command,
mcpServerArgs,
+ transport,
+ serverUrl,
};
const result = isDev
diff --git a/client/e2e/cli-arguments.spec.ts b/client/e2e/cli-arguments.spec.ts
new file mode 100644
index 00000000..a4dcdcce
--- /dev/null
+++ b/client/e2e/cli-arguments.spec.ts
@@ -0,0 +1,65 @@
+import { test, expect } from "@playwright/test";
+
+// These tests verify that CLI arguments correctly set URL parameters
+// The CLI should parse config files and pass transport/serverUrl as URL params
+test.describe("CLI Arguments @cli", () => {
+ test("should pass transport parameter from command line", async ({
+ page,
+ }) => {
+ // Simulate: npx . --transport sse --server-url http://localhost:3000/sse
+ await page.goto(
+ "http://localhost:6274/?transport=sse&serverUrl=http://localhost:3000/sse",
+ );
+
+ // Wait for the Transport Type dropdown to be visible
+ const selectTrigger = page.getByLabel("Transport Type");
+ await expect(selectTrigger).toBeVisible();
+
+ // Verify transport dropdown shows SSE
+ await expect(selectTrigger).toContainText("SSE");
+
+ // Verify URL field is visible and populated
+ const urlInput = page.locator("#sse-url-input");
+ await expect(urlInput).toBeVisible();
+ await expect(urlInput).toHaveValue("http://localhost:3000/sse");
+ });
+
+ test("should pass transport parameter for streamable-http", async ({
+ page,
+ }) => {
+ // Simulate config with streamable-http transport
+ await page.goto(
+ "http://localhost:6274/?transport=streamable-http&serverUrl=http://localhost:3000/mcp",
+ );
+
+ // Wait for the Transport Type dropdown to be visible
+ const selectTrigger = page.getByLabel("Transport Type");
+ await expect(selectTrigger).toBeVisible();
+
+ // Verify transport dropdown shows Streamable HTTP
+ await expect(selectTrigger).toContainText("Streamable HTTP");
+
+ // Verify URL field is visible and populated
+ const urlInput = page.locator("#sse-url-input");
+ await expect(urlInput).toBeVisible();
+ await expect(urlInput).toHaveValue("http://localhost:3000/mcp");
+ });
+
+ test("should not pass transport parameter for stdio config", async ({
+ page,
+ }) => {
+ // Simulate stdio config (no transport param needed)
+ await page.goto("http://localhost:6274/");
+
+ // Wait for the Transport Type dropdown to be visible
+ const selectTrigger = page.getByLabel("Transport Type");
+ await expect(selectTrigger).toBeVisible();
+
+ // Verify transport dropdown defaults to STDIO
+ await expect(selectTrigger).toContainText("STDIO");
+
+ // Verify command/args fields are visible
+ await expect(page.locator("#command-input")).toBeVisible();
+ await expect(page.locator("#arguments-input")).toBeVisible();
+ });
+});
diff --git a/client/package.json b/client/package.json
index 2e725a60..5d634ee1 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/inspector-client",
- "version": "0.16.2",
+ "version": "0.16.5",
"description": "Client-side application for the Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -25,7 +25,7 @@
"cleanup:e2e": "node e2e/global-teardown.js"
},
"dependencies": {
- "@modelcontextprotocol/sdk": "^1.17.0",
+ "@modelcontextprotocol/sdk": "^1.17.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-dialog": "^1.1.3",
"@radix-ui/react-icons": "^1.3.0",
@@ -49,19 +49,19 @@
"serve-handler": "^6.1.6",
"tailwind-merge": "^2.5.3",
"tailwindcss-animate": "^1.0.7",
- "zod": "^3.23.8"
+ "zod": "^3.25.76"
},
"devDependencies": {
"@eslint/js": "^9.11.1",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@types/jest": "^29.5.14",
- "@types/node": "^22.7.5",
+ "@types/node": "^22.17.0",
"@types/prismjs": "^1.26.5",
- "@types/react": "^18.3.10",
+ "@types/react": "^18.3.23",
"@types/react-dom": "^18.3.0",
"@types/serve-handler": "^6.1.4",
- "@vitejs/plugin-react": "^4.3.2",
+ "@vitejs/plugin-react": "^4.7.0",
"autoprefixer": "^10.4.20",
"co": "^4.6.0",
"eslint": "^9.11.1",
@@ -70,11 +70,11 @@
"globals": "^15.9.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
- "postcss": "^8.4.47",
+ "postcss": "^8.5.6",
"tailwindcss": "^3.4.13",
- "ts-jest": "^29.2.6",
+ "ts-jest": "^29.4.0",
"typescript": "^5.5.3",
- "typescript-eslint": "^8.7.0",
- "vite": "^6.3.0"
+ "typescript-eslint": "^8.38.0",
+ "vite": "^6.3.5"
}
}
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 3836aa9b..fecd9839 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -34,7 +34,6 @@ import {
useDraggablePane,
useDraggableSidebar,
} from "./lib/hooks/useDraggablePane";
-import { StdErrNotification } from "./lib/notificationTypes";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
@@ -106,9 +105,6 @@ const App = () => {
>(getInitialTransportType);
const [logLevel, setLogLevel] = useState("debug");
const [notifications, setNotifications] = useState([]);
- const [stdErrNotifications, setStdErrNotifications] = useState<
- StdErrNotification[]
- >([]);
const [roots, setRoots] = useState([]);
const [env, setEnv] = useState>({});
@@ -224,12 +220,6 @@ const App = () => {
onNotification: (notification) => {
setNotifications((prev) => [...prev, notification as ServerNotification]);
},
- onStdErrNotification: (notification) => {
- setStdErrNotifications((prev) => [
- ...prev,
- notification as StdErrNotification,
- ]);
- },
onPendingRequest: (request, resolve, reject) => {
setPendingSampleRequests((prev) => [
...prev,
@@ -260,6 +250,7 @@ const App = () => {
window.location.hash = "elicitations";
},
getRoots: () => rootsRef.current,
+ defaultLoggingLevel: logLevel,
});
useEffect(() => {
@@ -454,6 +445,14 @@ const App = () => {
if (data.defaultArgs) {
setArgs(data.defaultArgs);
}
+ if (data.defaultTransport) {
+ setTransportType(
+ data.defaultTransport as "stdio" | "sse" | "streamable-http",
+ );
+ }
+ if (data.defaultServerUrl) {
+ setSseUrl(data.defaultServerUrl);
+ }
})
.catch((error) =>
console.error("Error fetching default environment:", error),
@@ -748,10 +747,6 @@ const App = () => {
setLogLevel(level);
};
- const clearStdErrNotifications = () => {
- setStdErrNotifications([]);
- };
-
const AuthDebuggerWrapper = () => (
{
setOauthScope={setOauthScope}
onConnect={connectMcpServer}
onDisconnect={disconnectMcpServer}
- stdErrNotifications={stdErrNotifications}
logLevel={logLevel}
sendLogLevelRequest={sendLogLevelRequest}
loggingSupported={!!serverCapabilities?.logging || false}
- clearStdErrNotifications={clearStdErrNotifications}
/>
{
+ if (Array.isArray(type)) {
+ return type.every((t) => supportedTypes.includes(t));
+ }
+ return typeof type === "string" && supportedTypes.includes(type);
+};
+
const isSimpleObject = (schema: JsonSchemaType): boolean => {
const supportedTypes = ["string", "number", "integer", "boolean", "null"];
- if (schema.type && supportedTypes.includes(schema.type)) return true;
+ if (schema.type && isTypeSupported(schema.type, supportedTypes)) return true;
if (schema.type === "object") {
return Object.values(schema.properties ?? {}).every(
- (prop) => prop.type && supportedTypes.includes(prop.type),
+ (prop) => prop.type && isTypeSupported(prop.type, supportedTypes),
);
}
if (schema.type === "array") {
@@ -181,7 +191,13 @@ const DynamicJsonForm = ({
const isRequired =
parentSchema?.required?.includes(propertyName || "") ?? false;
- switch (propSchema.type) {
+ let fieldType = propSchema.type;
+ if (Array.isArray(fieldType)) {
+ // Of the possible types, find the first non-null type to determine the control to render
+ fieldType = fieldType.find((t) => t !== "null") ?? fieldType[0];
+ }
+
+ switch (fieldType) {
case "string": {
if (
propSchema.oneOf &&
@@ -337,6 +353,8 @@ const DynamicJsonForm = ({
required={isRequired}
/>
);
+ case "null":
+ return null;
case "object":
if (!propSchema.properties) {
return (
diff --git a/client/src/components/OAuthDebugCallback.tsx b/client/src/components/OAuthDebugCallback.tsx
index 99774297..95ccc076 100644
--- a/client/src/components/OAuthDebugCallback.tsx
+++ b/client/src/components/OAuthDebugCallback.tsx
@@ -46,6 +46,17 @@ const OAuthDebugCallback = ({ onConnect }: OAuthCallbackProps) => {
if (storedState) {
try {
restoredState = JSON.parse(storedState);
+ if (restoredState && typeof restoredState.resource === "string") {
+ restoredState.resource = new URL(restoredState.resource);
+ }
+ if (
+ restoredState &&
+ typeof restoredState.authorizationUrl === "string"
+ ) {
+ restoredState.authorizationUrl = new URL(
+ restoredState.authorizationUrl,
+ );
+ }
// Clean up the stored state
sessionStorage.removeItem(SESSION_KEYS.AUTH_DEBUGGER_STATE);
} catch (e) {
diff --git a/client/src/components/OAuthFlowProgress.tsx b/client/src/components/OAuthFlowProgress.tsx
index 0e260556..e50b7df4 100644
--- a/client/src/components/OAuthFlowProgress.tsx
+++ b/client/src/components/OAuthFlowProgress.tsx
@@ -4,6 +4,8 @@ import { Button } from "./ui/button";
import { DebugInspectorOAuthClientProvider } from "@/lib/auth";
import { useEffect, useMemo, useState } from "react";
import { OAuthClientInformation } from "@modelcontextprotocol/sdk/shared/auth.js";
+import { validateRedirectUrl } from "@/utils/urlValidation";
+import { useToast } from "@/lib/hooks/useToast";
interface OAuthStepProps {
label: string;
@@ -71,6 +73,7 @@ export const OAuthFlowProgress = ({
updateAuthState,
proceedToNextStep,
}: OAuthFlowProgressProps) => {
+ const { toast } = useToast();
const provider = useMemo(
() => new DebugInspectorOAuthClientProvider(serverUrl),
[serverUrl],
@@ -239,16 +242,32 @@ export const OAuthFlowProgress = ({
{authState.authorizationUrl}
-
{
+ try {
+ validateRedirectUrl(authState.authorizationUrl!);
+ window.open(
+ authState.authorizationUrl!,
+ "_blank",
+ "noopener noreferrer",
+ );
+ } catch (error) {
+ toast({
+ title: "Invalid URL",
+ description:
+ error instanceof Error
+ ? error.message
+ : "The authorization URL is not valid",
+ variant: "destructive",
+ });
+ }
+ }}
className="flex items-center text-blue-500 hover:text-blue-700"
aria-label="Open authorization URL in new tab"
title="Open authorization URL"
>
-
+
Click the link to authorize in your browser. After
@@ -354,7 +373,21 @@ export const OAuthFlowProgress = ({
authState.authorizationUrl && (
diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx
index a41e4bfc..a44c6e29 100644
--- a/client/src/components/Sidebar.tsx
+++ b/client/src/components/Sidebar.tsx
@@ -24,7 +24,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
-import { StdErrNotification } from "@/lib/notificationTypes";
import {
LoggingLevel,
LoggingLevelSchema,
@@ -62,8 +61,6 @@ interface SidebarProps {
setOauthScope: (scope: string) => void;
onConnect: () => void;
onDisconnect: () => void;
- stdErrNotifications: StdErrNotification[];
- clearStdErrNotifications: () => void;
logLevel: LoggingLevel;
sendLogLevelRequest: (level: LoggingLevel) => void;
loggingSupported: boolean;
@@ -93,8 +90,6 @@ const Sidebar = ({
setOauthScope,
onConnect,
onDisconnect,
- stdErrNotifications,
- clearStdErrNotifications,
logLevel,
sendLogLevelRequest,
loggingSupported,
@@ -268,6 +263,7 @@ const Sidebar = ({
placeholder="Command"
value={command}
onChange={(e) => setCommand(e.target.value)}
+ onBlur={(e) => setCommand(e.target.value.trim())}
className="font-mono"
/>
@@ -760,36 +756,6 @@ const Sidebar = ({
)}
-
- {stdErrNotifications.length > 0 && (
- <>
-
-
-
- Error output from MCP server
-
-
-
-
- {stdErrNotifications.map((notification, index) => (
-
- {notification.params.content}
-
- ))}
-
-
- >
- )}
diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx
index 04bd0465..acabd733 100644
--- a/client/src/components/ToolsTab.tsx
+++ b/client/src/components/ToolsTab.tsx
@@ -7,7 +7,11 @@ import { TabsContent } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import DynamicJsonForm from "./DynamicJsonForm";
import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils";
-import { generateDefaultValue, isPropertyRequired } from "@/utils/schemaUtils";
+import {
+ generateDefaultValue,
+ isPropertyRequired,
+ normalizeUnionType,
+} from "@/utils/schemaUtils";
import {
CompatibilityCallToolResult,
ListToolsResult,
@@ -115,7 +119,7 @@ const ToolsTab = ({
{Object.entries(selectedTool.inputSchema.properties ?? []).map(
([key, value]) => {
- const prop = value as JsonSchemaType;
+ const prop = normalizeUnionType(value as JsonSchemaType);
const inputSchema =
selectedTool.inputSchema as JsonSchemaType;
const required = isPropertyRequired(key, inputSchema);
@@ -159,7 +163,10 @@ const ToolsTab = ({
onChange={(e) =>
setParams({
...params,
- [key]: e.target.value,
+ [key]:
+ e.target.value === ""
+ ? undefined
+ : e.target.value,
})
}
className="mt-1"
diff --git a/client/src/components/__tests__/AuthDebugger.test.tsx b/client/src/components/__tests__/AuthDebugger.test.tsx
index fa9f2f7d..b3da23fd 100644
--- a/client/src/components/__tests__/AuthDebugger.test.tsx
+++ b/client/src/components/__tests__/AuthDebugger.test.tsx
@@ -25,6 +25,7 @@ const mockOAuthMetadata = {
token_endpoint: "https://oauth.example.com/token",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code"],
+ scopes_supported: ["read", "write"],
};
const mockOAuthClientInfo = {
@@ -36,7 +37,7 @@ const mockOAuthClientInfo = {
// Mock MCP SDK functions - must be before imports
jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
auth: jest.fn(),
- discoverOAuthMetadata: jest.fn(),
+ discoverAuthorizationServerMetadata: jest.fn(),
registerClient: jest.fn(),
startAuthorization: jest.fn(),
exchangeAuthorization: jest.fn(),
@@ -46,7 +47,7 @@ jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
// Import the functions to get their types
import {
- discoverOAuthMetadata,
+ discoverAuthorizationServerMetadata,
registerClient,
startAuthorization,
exchangeAuthorization,
@@ -56,10 +57,62 @@ import {
import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
import { EMPTY_DEBUGGER_STATE } from "@/lib/auth-types";
+// Mock local auth module
+jest.mock("@/lib/auth", () => ({
+ DebugInspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({
+ tokens: jest.fn().mockImplementation(() => Promise.resolve(undefined)),
+ clear: jest.fn().mockImplementation(() => {
+ // Mock the real clear() behavior which removes items from sessionStorage
+ sessionStorage.removeItem("[https://example.com/mcp] mcp_tokens");
+ sessionStorage.removeItem("[https://example.com/mcp] mcp_client_info");
+ sessionStorage.removeItem(
+ "[https://example.com/mcp] mcp_server_metadata",
+ );
+ }),
+ redirectUrl: "http://localhost:3000/oauth/callback/debug",
+ clientMetadata: {
+ redirect_uris: ["http://localhost:3000/oauth/callback/debug"],
+ token_endpoint_auth_method: "none",
+ grant_types: ["authorization_code", "refresh_token"],
+ response_types: ["code"],
+ client_name: "MCP Inspector",
+ },
+ clientInformation: jest.fn().mockImplementation(async () => {
+ const serverUrl = "https://example.com/mcp";
+ const preregisteredKey = `[${serverUrl}] ${SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION}`;
+ const preregisteredData = sessionStorage.getItem(preregisteredKey);
+ if (preregisteredData) {
+ return JSON.parse(preregisteredData);
+ }
+ const dynamicKey = `[${serverUrl}] ${SESSION_KEYS.CLIENT_INFORMATION}`;
+ const dynamicData = sessionStorage.getItem(dynamicKey);
+ if (dynamicData) {
+ return JSON.parse(dynamicData);
+ }
+ return undefined;
+ }),
+ saveClientInformation: jest.fn().mockImplementation((clientInfo) => {
+ const serverUrl = "https://example.com/mcp";
+ const key = `[${serverUrl}] ${SESSION_KEYS.CLIENT_INFORMATION}`;
+ sessionStorage.setItem(key, JSON.stringify(clientInfo));
+ }),
+ saveTokens: jest.fn(),
+ redirectToAuthorization: jest.fn(),
+ saveCodeVerifier: jest.fn(),
+ codeVerifier: jest.fn(),
+ saveServerMetadata: jest.fn(),
+ getServerMetadata: jest.fn(),
+ })),
+ discoverScopes: jest.fn().mockResolvedValue("read write" as never),
+}));
+
+import { discoverScopes } from "@/lib/auth";
+
// Type the mocked functions properly
-const mockDiscoverOAuthMetadata = discoverOAuthMetadata as jest.MockedFunction<
- typeof discoverOAuthMetadata
->;
+const mockDiscoverAuthorizationServerMetadata =
+ discoverAuthorizationServerMetadata as jest.MockedFunction<
+ typeof discoverAuthorizationServerMetadata
+ >;
const mockRegisterClient = registerClient as jest.MockedFunction<
typeof registerClient
>;
@@ -74,6 +127,9 @@ const mockDiscoverOAuthProtectedResourceMetadata =
discoverOAuthProtectedResourceMetadata as jest.MockedFunction<
typeof discoverOAuthProtectedResourceMetadata
>;
+const mockDiscoverScopes = discoverScopes as jest.MockedFunction<
+ typeof discoverScopes
+>;
const sessionStorageMock = {
getItem: jest.fn(),
@@ -102,7 +158,15 @@ describe("AuthDebugger", () => {
// Suppress console errors in tests to avoid JSDOM navigation noise
jest.spyOn(console, "error").mockImplementation(() => {});
- mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
+ // Set default mock behaviors with complete OAuth metadata
+ mockDiscoverAuthorizationServerMetadata.mockResolvedValue({
+ issuer: "https://oauth.example.com",
+ authorization_endpoint: "https://oauth.example.com/authorize",
+ token_endpoint: "https://oauth.example.com/token",
+ response_types_supported: ["code"],
+ grant_types_supported: ["authorization_code"],
+ scopes_supported: ["read", "write"],
+ });
mockRegisterClient.mockResolvedValue(mockOAuthClientInfo);
mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(
new Error("No protected resource metadata found"),
@@ -203,7 +267,7 @@ describe("AuthDebugger", () => {
});
// Should first discover and save OAuth metadata
- expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
+ expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledWith(
new URL("https://example.com/"),
);
@@ -216,7 +280,7 @@ describe("AuthDebugger", () => {
});
it("should show error when quick OAuth flow fails to discover metadata", async () => {
- mockDiscoverOAuthMetadata.mockRejectedValue(
+ mockDiscoverAuthorizationServerMetadata.mockRejectedValue(
new Error("Metadata discovery failed"),
);
@@ -362,7 +426,7 @@ describe("AuthDebugger", () => {
fireEvent.click(screen.getByText("Continue"));
});
- expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
+ expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledWith(
new URL("https://example.com/"),
);
});
@@ -424,7 +488,24 @@ describe("AuthDebugger", () => {
});
});
- it("should not include scope in authorization URL when scopes_supported is not present", async () => {
+ it("should include scope in authorization URL when scopes_supported is not present", async () => {
+ const updateAuthState =
+ await setupAuthorizationUrlTest(mockOAuthMetadata);
+
+ // Wait for the updateAuthState to be called
+ await waitFor(() => {
+ expect(updateAuthState).toHaveBeenCalledWith(
+ expect.objectContaining({
+ authorizationUrl: expect.stringContaining("scope="),
+ }),
+ );
+ });
+ });
+
+ it("should omit scope from authorization URL when discoverScopes returns undefined", async () => {
+ // Mock discoverScopes to return undefined (no scopes available)
+ mockDiscoverScopes.mockResolvedValueOnce(undefined);
+
const updateAuthState =
await setupAuthorizationUrlTest(mockOAuthMetadata);
@@ -439,6 +520,103 @@ describe("AuthDebugger", () => {
});
});
+ describe("Client Registration behavior", () => {
+ it("uses preregistered (static) client information without calling DCR", async () => {
+ const preregClientInfo = {
+ client_id: "static_client_id",
+ client_secret: "static_client_secret",
+ redirect_uris: ["http://localhost:3000/oauth/callback/debug"],
+ };
+
+ // Return preregistered client info for the server-specific key
+ sessionStorageMock.getItem.mockImplementation((key) => {
+ if (
+ key ===
+ `[${defaultProps.serverUrl}] ${SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION}`
+ ) {
+ return JSON.stringify(preregClientInfo);
+ }
+ return null;
+ });
+
+ const updateAuthState = jest.fn();
+
+ await act(async () => {
+ renderAuthDebugger({
+ updateAuthState,
+ authState: {
+ ...defaultAuthState,
+ isInitiatingAuth: false,
+ oauthStep: "client_registration",
+ oauthMetadata: mockOAuthMetadata as unknown as OAuthMetadata,
+ },
+ });
+ });
+
+ // Proceed from client_registration → authorization_redirect
+ await act(async () => {
+ fireEvent.click(screen.getByText("Continue"));
+ });
+
+ // Should NOT attempt dynamic client registration
+ expect(mockRegisterClient).not.toHaveBeenCalled();
+
+ // Should advance with the preregistered client info
+ expect(updateAuthState).toHaveBeenCalledWith(
+ expect.objectContaining({
+ oauthClientInfo: expect.objectContaining({
+ client_id: "static_client_id",
+ }),
+ oauthStep: "authorization_redirect",
+ }),
+ );
+ });
+
+ it("falls back to DCR when no static client information is available", async () => {
+ // No preregistered or dynamic client info present in session storage
+ sessionStorageMock.getItem.mockImplementation(() => null);
+
+ // DCR returns a new client
+ mockRegisterClient.mockResolvedValueOnce(mockOAuthClientInfo);
+
+ const updateAuthState = jest.fn();
+
+ await act(async () => {
+ renderAuthDebugger({
+ updateAuthState,
+ authState: {
+ ...defaultAuthState,
+ isInitiatingAuth: false,
+ oauthStep: "client_registration",
+ oauthMetadata: mockOAuthMetadata as unknown as OAuthMetadata,
+ },
+ });
+ });
+
+ await act(async () => {
+ fireEvent.click(screen.getByText("Continue"));
+ });
+
+ expect(mockRegisterClient).toHaveBeenCalledTimes(1);
+
+ // Should save and advance with the DCR client info
+ expect(updateAuthState).toHaveBeenCalledWith(
+ expect.objectContaining({
+ oauthClientInfo: expect.objectContaining({
+ client_id: "test_client_id",
+ }),
+ oauthStep: "authorization_redirect",
+ }),
+ );
+
+ // Verify the dynamically registered client info was persisted
+ expect(sessionStorage.setItem).toHaveBeenCalledWith(
+ `[${defaultProps.serverUrl}] ${SESSION_KEYS.CLIENT_INFORMATION}`,
+ expect.any(String),
+ );
+ });
+ });
+
describe("OAuth State Persistence", () => {
it("should store auth state to sessionStorage before redirect in Quick OAuth Flow", async () => {
const updateAuthState = jest.fn();
@@ -509,7 +687,9 @@ describe("AuthDebugger", () => {
mockDiscoverOAuthProtectedResourceMetadata.mockResolvedValue(
mockResourceMetadata,
);
- mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
+ mockDiscoverAuthorizationServerMetadata.mockResolvedValue(
+ mockOAuthMetadata,
+ );
await act(async () => {
renderAuthDebugger({
@@ -563,7 +743,9 @@ describe("AuthDebugger", () => {
// Mock failed metadata discovery
mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(mockError);
// But OAuth metadata should still work with the original URL
- mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
+ mockDiscoverAuthorizationServerMetadata.mockResolvedValue(
+ mockOAuthMetadata,
+ );
await act(async () => {
renderAuthDebugger({
@@ -603,7 +785,7 @@ describe("AuthDebugger", () => {
});
// Verify that regular OAuth metadata discovery was still called
- expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
+ expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledWith(
new URL("https://example.com/"),
);
});
diff --git a/client/src/components/__tests__/DynamicJsonForm.test.tsx b/client/src/components/__tests__/DynamicJsonForm.test.tsx
index 22813c9b..318fba2d 100644
--- a/client/src/components/__tests__/DynamicJsonForm.test.tsx
+++ b/client/src/components/__tests__/DynamicJsonForm.test.tsx
@@ -35,6 +35,18 @@ describe("DynamicJsonForm String Fields", () => {
const input = screen.getByRole("textbox");
expect(input).toHaveProperty("type", "text");
});
+
+ it("should handle a union type of string and null", () => {
+ const schema: JsonSchemaType = {
+ type: ["string", "null"],
+ description: "Test string or null field",
+ };
+ render(
+ ,
+ );
+ const input = screen.getByRole("textbox");
+ expect(input).toHaveProperty("type", "text");
+ });
});
describe("Format Support", () => {
diff --git a/client/src/components/__tests__/Sidebar.test.tsx b/client/src/components/__tests__/Sidebar.test.tsx
index e892a7f8..8f693717 100644
--- a/client/src/components/__tests__/Sidebar.test.tsx
+++ b/client/src/components/__tests__/Sidebar.test.tsx
@@ -31,7 +31,7 @@ Object.defineProperty(navigator, "clipboard", {
// Setup fake timers
jest.useFakeTimers();
-describe("Sidebar Environment Variables", () => {
+describe("Sidebar", () => {
const defaultProps = {
connectionStatus: "disconnected" as const,
transportType: "stdio" as const,
@@ -69,595 +69,329 @@ describe("Sidebar Environment Variables", () => {
);
};
- const openEnvVarsSection = () => {
- const button = screen.getByTestId("env-vars-button");
- fireEvent.click(button);
- };
-
beforeEach(() => {
jest.clearAllMocks();
jest.clearAllTimers();
});
- describe("Basic Operations", () => {
- it("should add a new environment variable", () => {
- const setEnv = jest.fn();
- renderSidebar({ env: {}, setEnv });
+ describe("Command and arguments", () => {
+ it("should trim whitespace from command input on blur", () => {
+ const setCommand = jest.fn();
+ renderSidebar({ command: " node server.js ", setCommand });
- openEnvVarsSection();
+ const commandInput = screen.getByLabelText("Command");
- const addButton = screen.getByText("Add Environment Variable");
- fireEvent.click(addButton);
-
- expect(setEnv).toHaveBeenCalledWith({ "": "" });
+ fireEvent.blur(commandInput);
+ expect(setCommand).toHaveBeenLastCalledWith("node server.js");
});
- it("should remove an environment variable", () => {
- const setEnv = jest.fn();
- const initialEnv = { TEST_KEY: "test_value" };
- renderSidebar({ env: initialEnv, setEnv });
+ it("should handle whitespace-only command input on blur", () => {
+ const setCommand = jest.fn();
+ renderSidebar({ command: " ", setCommand });
- openEnvVarsSection();
+ const commandInput = screen.getByLabelText("Command");
- const removeButton = screen.getByRole("button", { name: "×" });
- fireEvent.click(removeButton);
-
- expect(setEnv).toHaveBeenCalledWith({});
+ fireEvent.blur(commandInput);
+ expect(setCommand).toHaveBeenLastCalledWith("");
});
- it("should update environment variable value", () => {
- const setEnv = jest.fn();
- const initialEnv = { TEST_KEY: "test_value" };
- renderSidebar({ env: initialEnv, setEnv });
+ it("should not affect command without surrounding whitespace", () => {
+ const setCommand = jest.fn();
+ renderSidebar({ command: "node", setCommand });
- openEnvVarsSection();
+ const commandInput = screen.getByLabelText("Command");
- const valueInput = screen.getByDisplayValue("test_value");
- fireEvent.change(valueInput, { target: { value: "new_value" } });
-
- expect(setEnv).toHaveBeenCalledWith({ TEST_KEY: "new_value" });
- });
-
- it("should toggle value visibility", () => {
- const initialEnv = { TEST_KEY: "test_value" };
- renderSidebar({ env: initialEnv });
-
- openEnvVarsSection();
-
- const valueInput = screen.getByDisplayValue("test_value");
- expect(valueInput).toHaveProperty("type", "password");
-
- const toggleButton = screen.getByRole("button", { name: /show value/i });
- fireEvent.click(toggleButton);
-
- expect(valueInput).toHaveProperty("type", "text");
+ fireEvent.blur(commandInput);
+ expect(setCommand).toHaveBeenLastCalledWith("node");
});
});
- describe("Authentication", () => {
- const openAuthSection = () => {
- const button = screen.getByTestId("auth-button");
+ describe("Environment Variables", () => {
+ const openEnvVarsSection = () => {
+ const button = screen.getByTestId("env-vars-button");
fireEvent.click(button);
};
- it("should update bearer token", () => {
- const setBearerToken = jest.fn();
- renderSidebar({
- bearerToken: "",
- setBearerToken,
- transportType: "sse", // Set transport type to SSE
+ describe("Basic Operations", () => {
+ it("should add a new environment variable", () => {
+ const setEnv = jest.fn();
+ renderSidebar({ env: {}, setEnv });
+
+ openEnvVarsSection();
+
+ const addButton = screen.getByText("Add Environment Variable");
+ fireEvent.click(addButton);
+
+ expect(setEnv).toHaveBeenCalledWith({ "": "" });
});
- openAuthSection();
+ it("should remove an environment variable", () => {
+ const setEnv = jest.fn();
+ const initialEnv = { TEST_KEY: "test_value" };
+ renderSidebar({ env: initialEnv, setEnv });
- const tokenInput = screen.getByTestId("bearer-token-input");
- fireEvent.change(tokenInput, { target: { value: "new_token" } });
+ openEnvVarsSection();
- expect(setBearerToken).toHaveBeenCalledWith("new_token");
- });
+ const removeButton = screen.getByRole("button", { name: "×" });
+ fireEvent.click(removeButton);
- it("should update header name", () => {
- const setHeaderName = jest.fn();
- renderSidebar({
- headerName: "Authorization",
- setHeaderName,
- transportType: "sse",
+ expect(setEnv).toHaveBeenCalledWith({});
});
- openAuthSection();
+ it("should update environment variable value", () => {
+ const setEnv = jest.fn();
+ const initialEnv = { TEST_KEY: "test_value" };
+ renderSidebar({ env: initialEnv, setEnv });
- const headerInput = screen.getByTestId("header-input");
- fireEvent.change(headerInput, { target: { value: "X-Custom-Auth" } });
+ openEnvVarsSection();
- expect(setHeaderName).toHaveBeenCalledWith("X-Custom-Auth");
- });
+ const valueInput = screen.getByDisplayValue("test_value");
+ fireEvent.change(valueInput, { target: { value: "new_value" } });
- it("should clear bearer token", () => {
- const setBearerToken = jest.fn();
- renderSidebar({
- bearerToken: "existing_token",
- setBearerToken,
- transportType: "sse", // Set transport type to SSE
+ expect(setEnv).toHaveBeenCalledWith({ TEST_KEY: "new_value" });
});
- openAuthSection();
+ it("should toggle value visibility", () => {
+ const initialEnv = { TEST_KEY: "test_value" };
+ renderSidebar({ env: initialEnv });
- const tokenInput = screen.getByTestId("bearer-token-input");
- fireEvent.change(tokenInput, { target: { value: "" } });
+ openEnvVarsSection();
- expect(setBearerToken).toHaveBeenCalledWith("");
- });
+ const valueInput = screen.getByDisplayValue("test_value");
+ expect(valueInput).toHaveProperty("type", "password");
- it("should properly render bearer token input", () => {
- const { rerender } = renderSidebar({
- bearerToken: "existing_token",
- transportType: "sse", // Set transport type to SSE
- });
+ const toggleButton = screen.getByRole("button", {
+ name: /show value/i,
+ });
+ fireEvent.click(toggleButton);
- openAuthSection();
-
- // Token input should be a password field
- const tokenInput = screen.getByTestId("bearer-token-input");
- expect(tokenInput).toHaveProperty("type", "password");
-
- // Update the token
- fireEvent.change(tokenInput, { target: { value: "new_token" } });
-
- // Rerender with updated token
- rerender(
-
-
- ,
- );
-
- // Token input should still exist after update
- expect(screen.getByTestId("bearer-token-input")).toBeInTheDocument();
- });
-
- it("should maintain token visibility state after update", () => {
- const { rerender } = renderSidebar({
- bearerToken: "existing_token",
- transportType: "sse", // Set transport type to SSE
- });
-
- openAuthSection();
-
- // Token input should be a password field
- const tokenInput = screen.getByTestId("bearer-token-input");
- expect(tokenInput).toHaveProperty("type", "password");
-
- // Update the token
- fireEvent.change(tokenInput, { target: { value: "new_token" } });
-
- // Rerender with updated token
- rerender(
-
-
- ,
- );
-
- // Token input should still exist after update
- expect(screen.getByTestId("bearer-token-input")).toBeInTheDocument();
- });
-
- it("should maintain header name when toggling auth section", () => {
- renderSidebar({
- headerName: "X-API-Key",
- transportType: "sse",
- });
-
- // Open auth section
- openAuthSection();
-
- // Verify header name is displayed
- const headerInput = screen.getByTestId("header-input");
- expect(headerInput).toHaveValue("X-API-Key");
-
- // Close auth section
- const authButton = screen.getByTestId("auth-button");
- fireEvent.click(authButton);
-
- // Reopen auth section
- fireEvent.click(authButton);
-
- // Verify header name is still preserved
- expect(screen.getByTestId("header-input")).toHaveValue("X-API-Key");
- });
-
- it("should display default header name when not specified", () => {
- renderSidebar({
- headerName: undefined,
- transportType: "sse",
- });
-
- openAuthSection();
-
- const headerInput = screen.getByTestId("header-input");
- expect(headerInput).toHaveAttribute("placeholder", "Authorization");
- });
- });
-
- describe("Key Editing", () => {
- it("should maintain order when editing first key", () => {
- const setEnv = jest.fn();
- const initialEnv = {
- FIRST_KEY: "first_value",
- SECOND_KEY: "second_value",
- THIRD_KEY: "third_value",
- };
- renderSidebar({ env: initialEnv, setEnv });
-
- openEnvVarsSection();
-
- const firstKeyInput = screen.getByDisplayValue("FIRST_KEY");
- fireEvent.change(firstKeyInput, { target: { value: "NEW_FIRST_KEY" } });
-
- expect(setEnv).toHaveBeenCalledWith({
- NEW_FIRST_KEY: "first_value",
- SECOND_KEY: "second_value",
- THIRD_KEY: "third_value",
+ expect(valueInput).toHaveProperty("type", "text");
});
});
- it("should maintain order when editing middle key", () => {
- const setEnv = jest.fn();
- const initialEnv = {
- FIRST_KEY: "first_value",
- SECOND_KEY: "second_value",
- THIRD_KEY: "third_value",
- };
- renderSidebar({ env: initialEnv, setEnv });
+ describe("Key Editing", () => {
+ it("should maintain order when editing first key", () => {
+ const setEnv = jest.fn();
+ const initialEnv = {
+ FIRST_KEY: "first_value",
+ SECOND_KEY: "second_value",
+ THIRD_KEY: "third_value",
+ };
+ renderSidebar({ env: initialEnv, setEnv });
- openEnvVarsSection();
+ openEnvVarsSection();
- const middleKeyInput = screen.getByDisplayValue("SECOND_KEY");
- fireEvent.change(middleKeyInput, { target: { value: "NEW_SECOND_KEY" } });
+ const firstKeyInput = screen.getByDisplayValue("FIRST_KEY");
+ fireEvent.change(firstKeyInput, { target: { value: "NEW_FIRST_KEY" } });
- expect(setEnv).toHaveBeenCalledWith({
- FIRST_KEY: "first_value",
- NEW_SECOND_KEY: "second_value",
- THIRD_KEY: "third_value",
- });
- });
-
- it("should maintain order when editing last key", () => {
- const setEnv = jest.fn();
- const initialEnv = {
- FIRST_KEY: "first_value",
- SECOND_KEY: "second_value",
- THIRD_KEY: "third_value",
- };
- renderSidebar({ env: initialEnv, setEnv });
-
- openEnvVarsSection();
-
- const lastKeyInput = screen.getByDisplayValue("THIRD_KEY");
- fireEvent.change(lastKeyInput, { target: { value: "NEW_THIRD_KEY" } });
-
- expect(setEnv).toHaveBeenCalledWith({
- FIRST_KEY: "first_value",
- SECOND_KEY: "second_value",
- NEW_THIRD_KEY: "third_value",
- });
- });
-
- it("should maintain order during key editing", () => {
- const setEnv = jest.fn();
- const initialEnv = {
- KEY1: "value1",
- KEY2: "value2",
- };
- renderSidebar({ env: initialEnv, setEnv });
-
- openEnvVarsSection();
-
- // Type "NEW_" one character at a time
- const key1Input = screen.getByDisplayValue("KEY1");
- "NEW_".split("").forEach((char) => {
- fireEvent.change(key1Input, {
- target: { value: char + "KEY1".slice(1) },
+ expect(setEnv).toHaveBeenCalledWith({
+ NEW_FIRST_KEY: "first_value",
+ SECOND_KEY: "second_value",
+ THIRD_KEY: "third_value",
});
});
- // Verify the last setEnv call maintains the order
- const lastCall = setEnv.mock.calls[
- setEnv.mock.calls.length - 1
- ][0] as Record;
- const entries = Object.entries(lastCall);
+ it("should maintain order when editing middle key", () => {
+ const setEnv = jest.fn();
+ const initialEnv = {
+ FIRST_KEY: "first_value",
+ SECOND_KEY: "second_value",
+ THIRD_KEY: "third_value",
+ };
+ renderSidebar({ env: initialEnv, setEnv });
- // The values should stay with their original keys
- expect(entries[0][1]).toBe("value1"); // First entry should still have value1
- expect(entries[1][1]).toBe("value2"); // Second entry should still have value2
- });
- });
+ openEnvVarsSection();
- describe("Multiple Operations", () => {
- it("should maintain state after multiple key edits", () => {
- const setEnv = jest.fn();
- const initialEnv = {
- FIRST_KEY: "first_value",
- SECOND_KEY: "second_value",
- };
- const { rerender } = renderSidebar({ env: initialEnv, setEnv });
+ const middleKeyInput = screen.getByDisplayValue("SECOND_KEY");
+ fireEvent.change(middleKeyInput, {
+ target: { value: "NEW_SECOND_KEY" },
+ });
- openEnvVarsSection();
+ expect(setEnv).toHaveBeenCalledWith({
+ FIRST_KEY: "first_value",
+ NEW_SECOND_KEY: "second_value",
+ THIRD_KEY: "third_value",
+ });
+ });
- // First key edit
- const firstKeyInput = screen.getByDisplayValue("FIRST_KEY");
- fireEvent.change(firstKeyInput, { target: { value: "NEW_FIRST_KEY" } });
+ it("should maintain order when editing last key", () => {
+ const setEnv = jest.fn();
+ const initialEnv = {
+ FIRST_KEY: "first_value",
+ SECOND_KEY: "second_value",
+ THIRD_KEY: "third_value",
+ };
+ renderSidebar({ env: initialEnv, setEnv });
- // Get the updated env from the first setEnv call
- const updatedEnv = setEnv.mock.calls[0][0] as Record;
+ openEnvVarsSection();
- // Rerender with the updated env
- rerender(
-
-
- ,
- );
+ const lastKeyInput = screen.getByDisplayValue("THIRD_KEY");
+ fireEvent.change(lastKeyInput, { target: { value: "NEW_THIRD_KEY" } });
- // Second key edit
- const secondKeyInput = screen.getByDisplayValue("SECOND_KEY");
- fireEvent.change(secondKeyInput, { target: { value: "NEW_SECOND_KEY" } });
+ expect(setEnv).toHaveBeenCalledWith({
+ FIRST_KEY: "first_value",
+ SECOND_KEY: "second_value",
+ NEW_THIRD_KEY: "third_value",
+ });
+ });
- // Verify the final state matches what we expect
- expect(setEnv).toHaveBeenLastCalledWith({
- NEW_FIRST_KEY: "first_value",
- NEW_SECOND_KEY: "second_value",
+ it("should maintain order during key editing", () => {
+ const setEnv = jest.fn();
+ const initialEnv = {
+ KEY1: "value1",
+ KEY2: "value2",
+ };
+ renderSidebar({ env: initialEnv, setEnv });
+
+ openEnvVarsSection();
+
+ // Type "NEW_" one character at a time
+ const key1Input = screen.getByDisplayValue("KEY1");
+ "NEW_".split("").forEach((char) => {
+ fireEvent.change(key1Input, {
+ target: { value: char + "KEY1".slice(1) },
+ });
+ });
+
+ // Verify the last setEnv call maintains the order
+ const lastCall = setEnv.mock.calls[
+ setEnv.mock.calls.length - 1
+ ][0] as Record;
+ const entries = Object.entries(lastCall);
+
+ // The values should stay with their original keys
+ expect(entries[0][1]).toBe("value1"); // First entry should still have value1
+ expect(entries[1][1]).toBe("value2"); // Second entry should still have value2
});
});
- it("should maintain visibility state after key edit", () => {
- const initialEnv = { TEST_KEY: "test_value" };
- const { rerender } = renderSidebar({ env: initialEnv });
+ describe("Multiple Operations", () => {
+ it("should maintain state after multiple key edits", () => {
+ const setEnv = jest.fn();
+ const initialEnv = {
+ FIRST_KEY: "first_value",
+ SECOND_KEY: "second_value",
+ };
+ const { rerender } = renderSidebar({ env: initialEnv, setEnv });
- openEnvVarsSection();
+ openEnvVarsSection();
- // Show the value
- const toggleButton = screen.getByRole("button", { name: /show value/i });
- fireEvent.click(toggleButton);
+ // First key edit
+ const firstKeyInput = screen.getByDisplayValue("FIRST_KEY");
+ fireEvent.change(firstKeyInput, { target: { value: "NEW_FIRST_KEY" } });
- const valueInput = screen.getByDisplayValue("test_value");
- expect(valueInput).toHaveProperty("type", "text");
+ // Get the updated env from the first setEnv call
+ const updatedEnv = setEnv.mock.calls[0][0] as Record;
- // Edit the key
- const keyInput = screen.getByDisplayValue("TEST_KEY");
- fireEvent.change(keyInput, { target: { value: "NEW_KEY" } });
+ // Rerender with the updated env
+ rerender(
+
+
+ ,
+ );
- // Rerender with updated env
- rerender(
-
-
- ,
- );
+ // Second key edit
+ const secondKeyInput = screen.getByDisplayValue("SECOND_KEY");
+ fireEvent.change(secondKeyInput, {
+ target: { value: "NEW_SECOND_KEY" },
+ });
- // Value should still be visible
- const updatedValueInput = screen.getByDisplayValue("test_value");
- expect(updatedValueInput).toHaveProperty("type", "text");
+ // Verify the final state matches what we expect
+ expect(setEnv).toHaveBeenLastCalledWith({
+ NEW_FIRST_KEY: "first_value",
+ NEW_SECOND_KEY: "second_value",
+ });
+ });
+
+ it("should maintain visibility state after key edit", () => {
+ const initialEnv = { TEST_KEY: "test_value" };
+ const { rerender } = renderSidebar({ env: initialEnv });
+
+ openEnvVarsSection();
+
+ // Show the value
+ const toggleButton = screen.getByRole("button", {
+ name: /show value/i,
+ });
+ fireEvent.click(toggleButton);
+
+ const valueInput = screen.getByDisplayValue("test_value");
+ expect(valueInput).toHaveProperty("type", "text");
+
+ // Edit the key
+ const keyInput = screen.getByDisplayValue("TEST_KEY");
+ fireEvent.change(keyInput, { target: { value: "NEW_KEY" } });
+
+ // Rerender with updated env
+ rerender(
+
+
+ ,
+ );
+
+ // Value should still be visible
+ const updatedValueInput = screen.getByDisplayValue("test_value");
+ expect(updatedValueInput).toHaveProperty("type", "text");
+ });
+ });
+
+ describe("Edge Cases", () => {
+ it("should handle empty key", () => {
+ const setEnv = jest.fn();
+ const initialEnv = { TEST_KEY: "test_value" };
+ renderSidebar({ env: initialEnv, setEnv });
+
+ openEnvVarsSection();
+
+ const keyInput = screen.getByDisplayValue("TEST_KEY");
+ fireEvent.change(keyInput, { target: { value: "" } });
+
+ expect(setEnv).toHaveBeenCalledWith({ "": "test_value" });
+ });
+
+ it("should handle special characters in key", () => {
+ const setEnv = jest.fn();
+ const initialEnv = { TEST_KEY: "test_value" };
+ renderSidebar({ env: initialEnv, setEnv });
+
+ openEnvVarsSection();
+
+ const keyInput = screen.getByDisplayValue("TEST_KEY");
+ fireEvent.change(keyInput, { target: { value: "TEST-KEY@123" } });
+
+ expect(setEnv).toHaveBeenCalledWith({ "TEST-KEY@123": "test_value" });
+ });
+
+ it("should handle unicode characters in key", () => {
+ const setEnv = jest.fn();
+ const initialEnv = { TEST_KEY: "test_value" };
+ renderSidebar({ env: initialEnv, setEnv });
+
+ openEnvVarsSection();
+
+ const keyInput = screen.getByDisplayValue("TEST_KEY");
+ fireEvent.change(keyInput, { target: { value: "TEST_🔑" } });
+
+ expect(setEnv).toHaveBeenCalledWith({ "TEST_🔑": "test_value" });
+ });
+
+ it("should handle a very long key name", () => {
+ const setEnv = jest.fn();
+ const initialEnv = { TEST_KEY: "test_value" };
+ renderSidebar({ env: initialEnv, setEnv });
+
+ openEnvVarsSection();
+
+ const keyInput = screen.getByDisplayValue("TEST_KEY");
+ const longKey = "A".repeat(100);
+ fireEvent.change(keyInput, { target: { value: longKey } });
+
+ expect(setEnv).toHaveBeenCalledWith({ [longKey]: "test_value" });
+ });
});
});
- describe("Edge Cases", () => {
- it("should handle empty key", () => {
- const setEnv = jest.fn();
- const initialEnv = { TEST_KEY: "test_value" };
- renderSidebar({ env: initialEnv, setEnv });
-
- openEnvVarsSection();
-
- const keyInput = screen.getByDisplayValue("TEST_KEY");
- fireEvent.change(keyInput, { target: { value: "" } });
-
- expect(setEnv).toHaveBeenCalledWith({ "": "test_value" });
- });
-
- it("should handle special characters in key", () => {
- const setEnv = jest.fn();
- const initialEnv = { TEST_KEY: "test_value" };
- renderSidebar({ env: initialEnv, setEnv });
-
- openEnvVarsSection();
-
- const keyInput = screen.getByDisplayValue("TEST_KEY");
- fireEvent.change(keyInput, { target: { value: "TEST-KEY@123" } });
-
- expect(setEnv).toHaveBeenCalledWith({ "TEST-KEY@123": "test_value" });
- });
-
- it("should handle unicode characters", () => {
- const setEnv = jest.fn();
- const initialEnv = { TEST_KEY: "test_value" };
- renderSidebar({ env: initialEnv, setEnv });
-
- openEnvVarsSection();
-
- const keyInput = screen.getByDisplayValue("TEST_KEY");
- fireEvent.change(keyInput, { target: { value: "TEST_🔑" } });
-
- expect(setEnv).toHaveBeenCalledWith({ "TEST_🔑": "test_value" });
- });
-
- it("should handle very long key names", () => {
- const setEnv = jest.fn();
- const initialEnv = { TEST_KEY: "test_value" };
- renderSidebar({ env: initialEnv, setEnv });
-
- openEnvVarsSection();
-
- const keyInput = screen.getByDisplayValue("TEST_KEY");
- const longKey = "A".repeat(100);
- fireEvent.change(keyInput, { target: { value: longKey } });
-
- expect(setEnv).toHaveBeenCalledWith({ [longKey]: "test_value" });
- });
- });
-
- describe("Configuration Operations", () => {
- const openConfigSection = () => {
- const button = screen.getByTestId("config-button");
- fireEvent.click(button);
- };
-
- it("should update MCP server request timeout", () => {
- const setConfig = jest.fn();
- renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
-
- openConfigSection();
-
- const timeoutInput = screen.getByTestId(
- "MCP_SERVER_REQUEST_TIMEOUT-input",
- );
- fireEvent.change(timeoutInput, { target: { value: "5000" } });
-
- expect(setConfig).toHaveBeenCalledWith(
- expect.objectContaining({
- MCP_SERVER_REQUEST_TIMEOUT: {
- label: "Request Timeout",
- description: "Timeout for requests to the MCP server (ms)",
- value: 5000,
- is_session_item: false,
- },
- }),
- );
- });
-
- it("should update MCP server proxy address", () => {
- const setConfig = jest.fn();
- renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
-
- openConfigSection();
-
- const proxyAddressInput = screen.getByTestId(
- "MCP_PROXY_FULL_ADDRESS-input",
- );
- fireEvent.change(proxyAddressInput, {
- target: { value: "http://localhost:8080" },
- });
-
- expect(setConfig).toHaveBeenCalledWith(
- expect.objectContaining({
- MCP_PROXY_FULL_ADDRESS: {
- label: "Inspector Proxy Address",
- description:
- "Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
- value: "http://localhost:8080",
- is_session_item: false,
- },
- }),
- );
- });
-
- it("should update max total timeout", () => {
- const setConfig = jest.fn();
- renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
-
- openConfigSection();
-
- const maxTotalTimeoutInput = screen.getByTestId(
- "MCP_REQUEST_MAX_TOTAL_TIMEOUT-input",
- );
- fireEvent.change(maxTotalTimeoutInput, {
- target: { value: "10000" },
- });
-
- expect(setConfig).toHaveBeenCalledWith(
- expect.objectContaining({
- MCP_REQUEST_MAX_TOTAL_TIMEOUT: {
- label: "Maximum Total Timeout",
- description:
- "Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)",
- value: 10000,
- is_session_item: false,
- },
- }),
- );
- });
-
- it("should handle invalid timeout values entered by user", () => {
- const setConfig = jest.fn();
- renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
-
- openConfigSection();
-
- const timeoutInput = screen.getByTestId(
- "MCP_SERVER_REQUEST_TIMEOUT-input",
- );
- fireEvent.change(timeoutInput, { target: { value: "abc1" } });
-
- expect(setConfig).toHaveBeenCalledWith(
- expect.objectContaining({
- MCP_SERVER_REQUEST_TIMEOUT: {
- label: "Request Timeout",
- description: "Timeout for requests to the MCP server (ms)",
- value: 0,
- is_session_item: false,
- },
- }),
- );
- });
-
- it("should maintain configuration state after multiple updates", () => {
- const setConfig = jest.fn();
- const { rerender } = renderSidebar({
- config: DEFAULT_INSPECTOR_CONFIG,
- setConfig,
- });
-
- openConfigSection();
- // First update
- const timeoutInput = screen.getByTestId(
- "MCP_SERVER_REQUEST_TIMEOUT-input",
- );
- fireEvent.change(timeoutInput, { target: { value: "5000" } });
-
- // Get the updated config from the first setConfig call
- const updatedConfig = setConfig.mock.calls[0][0] as InspectorConfig;
-
- // Rerender with the updated config
- rerender(
-
-
- ,
- );
-
- // Second update
- const updatedTimeoutInput = screen.getByTestId(
- "MCP_SERVER_REQUEST_TIMEOUT-input",
- );
- fireEvent.change(updatedTimeoutInput, { target: { value: "3000" } });
-
- // Verify the final state matches what we expect
- expect(setConfig).toHaveBeenLastCalledWith(
- expect.objectContaining({
- MCP_SERVER_REQUEST_TIMEOUT: {
- label: "Request Timeout",
- description: "Timeout for requests to the MCP server (ms)",
- value: 3000,
- is_session_item: false,
- },
- }),
- );
- });
- });
-
- describe("Copy Configuration Features", () => {
- beforeEach(() => {
- jest.clearAllMocks();
- jest.clearAllTimers();
- });
-
+ describe("Copy Server Features", () => {
const getCopyButtons = () => {
return {
serverEntry: screen.getByRole("button", { name: /server entry/i }),
@@ -878,4 +612,307 @@ describe("Sidebar Environment Variables", () => {
expect(mockClipboardWrite).toHaveBeenCalledWith(expectedConfig);
});
});
+
+ describe("Authentication", () => {
+ const openAuthSection = () => {
+ const button = screen.getByTestId("auth-button");
+ fireEvent.click(button);
+ };
+
+ it("should update bearer token", () => {
+ const setBearerToken = jest.fn();
+ renderSidebar({
+ bearerToken: "",
+ setBearerToken,
+ transportType: "sse", // Set transport type to SSE
+ });
+
+ openAuthSection();
+
+ const tokenInput = screen.getByTestId("bearer-token-input");
+ fireEvent.change(tokenInput, { target: { value: "new_token" } });
+
+ expect(setBearerToken).toHaveBeenCalledWith("new_token");
+ });
+
+ it("should update header name", () => {
+ const setHeaderName = jest.fn();
+ renderSidebar({
+ headerName: "Authorization",
+ setHeaderName,
+ transportType: "sse",
+ });
+
+ openAuthSection();
+
+ const headerInput = screen.getByTestId("header-input");
+ fireEvent.change(headerInput, { target: { value: "X-Custom-Auth" } });
+
+ expect(setHeaderName).toHaveBeenCalledWith("X-Custom-Auth");
+ });
+
+ it("should clear bearer token", () => {
+ const setBearerToken = jest.fn();
+ renderSidebar({
+ bearerToken: "existing_token",
+ setBearerToken,
+ transportType: "sse", // Set transport type to SSE
+ });
+
+ openAuthSection();
+
+ const tokenInput = screen.getByTestId("bearer-token-input");
+ fireEvent.change(tokenInput, { target: { value: "" } });
+
+ expect(setBearerToken).toHaveBeenCalledWith("");
+ });
+
+ it("should properly render bearer token input", () => {
+ const { rerender } = renderSidebar({
+ bearerToken: "existing_token",
+ transportType: "sse", // Set transport type to SSE
+ });
+
+ openAuthSection();
+
+ // Token input should be a password field
+ const tokenInput = screen.getByTestId("bearer-token-input");
+ expect(tokenInput).toHaveProperty("type", "password");
+
+ // Update the token
+ fireEvent.change(tokenInput, { target: { value: "new_token" } });
+
+ // Rerender with updated token
+ rerender(
+
+
+ ,
+ );
+
+ // Token input should still exist after update
+ expect(screen.getByTestId("bearer-token-input")).toBeInTheDocument();
+ });
+
+ it("should maintain token visibility state after update", () => {
+ const { rerender } = renderSidebar({
+ bearerToken: "existing_token",
+ transportType: "sse", // Set transport type to SSE
+ });
+
+ openAuthSection();
+
+ // Token input should be a password field
+ const tokenInput = screen.getByTestId("bearer-token-input");
+ expect(tokenInput).toHaveProperty("type", "password");
+
+ // Update the token
+ fireEvent.change(tokenInput, { target: { value: "new_token" } });
+
+ // Rerender with updated token
+ rerender(
+
+
+ ,
+ );
+
+ // Token input should still exist after update
+ expect(screen.getByTestId("bearer-token-input")).toBeInTheDocument();
+ });
+
+ it("should maintain header name when toggling auth section", () => {
+ renderSidebar({
+ headerName: "X-API-Key",
+ transportType: "sse",
+ });
+
+ // Open auth section
+ openAuthSection();
+
+ // Verify header name is displayed
+ const headerInput = screen.getByTestId("header-input");
+ expect(headerInput).toHaveValue("X-API-Key");
+
+ // Close auth section
+ const authButton = screen.getByTestId("auth-button");
+ fireEvent.click(authButton);
+
+ // Reopen auth section
+ fireEvent.click(authButton);
+
+ // Verify header name is still preserved
+ expect(screen.getByTestId("header-input")).toHaveValue("X-API-Key");
+ });
+
+ it("should display default header name when not specified", () => {
+ renderSidebar({
+ headerName: undefined,
+ transportType: "sse",
+ });
+
+ openAuthSection();
+
+ const headerInput = screen.getByTestId("header-input");
+ expect(headerInput).toHaveAttribute("placeholder", "Authorization");
+ });
+ });
+
+ describe("Configuration", () => {
+ const openConfigSection = () => {
+ const button = screen.getByTestId("config-button");
+ fireEvent.click(button);
+ };
+
+ it("should update MCP server request timeout", () => {
+ const setConfig = jest.fn();
+ renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
+
+ openConfigSection();
+
+ const timeoutInput = screen.getByTestId(
+ "MCP_SERVER_REQUEST_TIMEOUT-input",
+ );
+ fireEvent.change(timeoutInput, { target: { value: "5000" } });
+
+ expect(setConfig).toHaveBeenCalledWith(
+ expect.objectContaining({
+ MCP_SERVER_REQUEST_TIMEOUT: {
+ label: "Request Timeout",
+ description: "Timeout for requests to the MCP server (ms)",
+ value: 5000,
+ is_session_item: false,
+ },
+ }),
+ );
+ });
+
+ it("should update MCP server proxy address", () => {
+ const setConfig = jest.fn();
+ renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
+
+ openConfigSection();
+
+ const proxyAddressInput = screen.getByTestId(
+ "MCP_PROXY_FULL_ADDRESS-input",
+ );
+ fireEvent.change(proxyAddressInput, {
+ target: { value: "http://localhost:8080" },
+ });
+
+ expect(setConfig).toHaveBeenCalledWith(
+ expect.objectContaining({
+ MCP_PROXY_FULL_ADDRESS: {
+ label: "Inspector Proxy Address",
+ description:
+ "Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
+ value: "http://localhost:8080",
+ is_session_item: false,
+ },
+ }),
+ );
+ });
+
+ it("should update max total timeout", () => {
+ const setConfig = jest.fn();
+ renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
+
+ openConfigSection();
+
+ const maxTotalTimeoutInput = screen.getByTestId(
+ "MCP_REQUEST_MAX_TOTAL_TIMEOUT-input",
+ );
+ fireEvent.change(maxTotalTimeoutInput, {
+ target: { value: "10000" },
+ });
+
+ expect(setConfig).toHaveBeenCalledWith(
+ expect.objectContaining({
+ MCP_REQUEST_MAX_TOTAL_TIMEOUT: {
+ label: "Maximum Total Timeout",
+ description:
+ "Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)",
+ value: 10000,
+ is_session_item: false,
+ },
+ }),
+ );
+ });
+
+ it("should handle invalid timeout values entered by user", () => {
+ const setConfig = jest.fn();
+ renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
+
+ openConfigSection();
+
+ const timeoutInput = screen.getByTestId(
+ "MCP_SERVER_REQUEST_TIMEOUT-input",
+ );
+ fireEvent.change(timeoutInput, { target: { value: "abc1" } });
+
+ expect(setConfig).toHaveBeenCalledWith(
+ expect.objectContaining({
+ MCP_SERVER_REQUEST_TIMEOUT: {
+ label: "Request Timeout",
+ description: "Timeout for requests to the MCP server (ms)",
+ value: 0,
+ is_session_item: false,
+ },
+ }),
+ );
+ });
+
+ it("should maintain configuration state after multiple updates", () => {
+ const setConfig = jest.fn();
+ const { rerender } = renderSidebar({
+ config: DEFAULT_INSPECTOR_CONFIG,
+ setConfig,
+ });
+
+ openConfigSection();
+ // First update
+ const timeoutInput = screen.getByTestId(
+ "MCP_SERVER_REQUEST_TIMEOUT-input",
+ );
+ fireEvent.change(timeoutInput, { target: { value: "5000" } });
+
+ // Get the updated config from the first setConfig call
+ const updatedConfig = setConfig.mock.calls[0][0] as InspectorConfig;
+
+ // Rerender with the updated config
+ rerender(
+
+
+ ,
+ );
+
+ // Second update
+ const updatedTimeoutInput = screen.getByTestId(
+ "MCP_SERVER_REQUEST_TIMEOUT-input",
+ );
+ fireEvent.change(updatedTimeoutInput, { target: { value: "3000" } });
+
+ // Verify the final state matches what we expect
+ expect(setConfig).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ MCP_SERVER_REQUEST_TIMEOUT: {
+ label: "Request Timeout",
+ description: "Timeout for requests to the MCP server (ms)",
+ value: 3000,
+ is_session_item: false,
+ },
+ }),
+ );
+ });
+ });
});
diff --git a/client/src/lib/__tests__/auth.test.ts b/client/src/lib/__tests__/auth.test.ts
new file mode 100644
index 00000000..329b7f02
--- /dev/null
+++ b/client/src/lib/__tests__/auth.test.ts
@@ -0,0 +1,155 @@
+import { discoverScopes } from "../auth";
+import { discoverAuthorizationServerMetadata } from "@modelcontextprotocol/sdk/client/auth.js";
+
+jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
+ discoverAuthorizationServerMetadata: jest.fn(),
+}));
+
+const mockDiscoverAuth =
+ discoverAuthorizationServerMetadata as jest.MockedFunction<
+ typeof discoverAuthorizationServerMetadata
+ >;
+
+const baseMetadata = {
+ issuer: "https://test.com",
+ authorization_endpoint: "https://test.com/authorize",
+ token_endpoint: "https://test.com/token",
+ response_types_supported: ["code"],
+ grant_types_supported: ["authorization_code"],
+ scopes_supported: ["read", "write"],
+};
+
+describe("discoverScopes", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const testCases = [
+ {
+ name: "returns joined scopes from OAuth metadata",
+ mockResolves: baseMetadata,
+ serverUrl: "https://example.com",
+ expected: "read write",
+ expectedCallUrl: "https://example.com/",
+ },
+ {
+ name: "prefers resource metadata over OAuth metadata",
+ mockResolves: baseMetadata,
+ serverUrl: "https://example.com",
+ resourceMetadata: {
+ resource: "https://example.com",
+ scopes_supported: ["admin", "full"],
+ },
+ expected: "admin full",
+ },
+ {
+ name: "falls back to OAuth when resource has empty scopes",
+ mockResolves: baseMetadata,
+ serverUrl: "https://example.com",
+ resourceMetadata: {
+ resource: "https://example.com",
+ scopes_supported: [],
+ },
+ expected: "read write",
+ },
+ {
+ name: "normalizes URL with port and path",
+ mockResolves: baseMetadata,
+ serverUrl: "https://example.com:8080/some/path",
+ expected: "read write",
+ expectedCallUrl: "https://example.com:8080/",
+ },
+ {
+ name: "normalizes URL with trailing slash",
+ mockResolves: baseMetadata,
+ serverUrl: "https://example.com/",
+ expected: "read write",
+ expectedCallUrl: "https://example.com/",
+ },
+ {
+ name: "handles single scope",
+ mockResolves: { ...baseMetadata, scopes_supported: ["admin"] },
+ serverUrl: "https://example.com",
+ expected: "admin",
+ },
+ {
+ name: "prefers resource metadata even with fewer scopes",
+ mockResolves: {
+ ...baseMetadata,
+ scopes_supported: ["read", "write", "admin", "full"],
+ },
+ serverUrl: "https://example.com",
+ resourceMetadata: {
+ resource: "https://example.com",
+ scopes_supported: ["read"],
+ },
+ expected: "read",
+ },
+ ];
+
+ const undefinedCases = [
+ {
+ name: "returns undefined when OAuth discovery fails",
+ mockRejects: new Error("Discovery failed"),
+ serverUrl: "https://example.com",
+ },
+ {
+ name: "returns undefined when OAuth has no scopes",
+ mockResolves: { ...baseMetadata, scopes_supported: [] },
+ serverUrl: "https://example.com",
+ },
+ {
+ name: "returns undefined when scopes_supported missing",
+ mockResolves: (() => {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { scopes_supported, ...rest } = baseMetadata;
+ return rest;
+ })(),
+ serverUrl: "https://example.com",
+ },
+ {
+ name: "returns undefined with resource metadata but OAuth fails",
+ mockRejects: new Error("No OAuth metadata"),
+ serverUrl: "https://example.com",
+ resourceMetadata: {
+ resource: "https://example.com",
+ scopes_supported: ["read", "write"],
+ },
+ },
+ ];
+
+ test.each(testCases)(
+ "$name",
+ async ({
+ mockResolves,
+ serverUrl,
+ resourceMetadata,
+ expected,
+ expectedCallUrl,
+ }) => {
+ mockDiscoverAuth.mockResolvedValue(mockResolves);
+
+ const result = await discoverScopes(serverUrl, resourceMetadata);
+
+ expect(result).toBe(expected);
+ if (expectedCallUrl) {
+ expect(mockDiscoverAuth).toHaveBeenCalledWith(new URL(expectedCallUrl));
+ }
+ },
+ );
+
+ test.each(undefinedCases)(
+ "$name",
+ async ({ mockResolves, mockRejects, serverUrl, resourceMetadata }) => {
+ if (mockRejects) {
+ mockDiscoverAuth.mockRejectedValue(mockRejects);
+ } else {
+ mockDiscoverAuth.mockResolvedValue(mockResolves);
+ }
+
+ const result = await discoverScopes(serverUrl, resourceMetadata);
+
+ expect(result).toBeUndefined();
+ },
+ );
+});
diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts
index 9989e944..c1505398 100644
--- a/client/src/lib/auth.ts
+++ b/client/src/lib/auth.ts
@@ -6,8 +6,45 @@ import {
OAuthTokensSchema,
OAuthClientMetadata,
OAuthMetadata,
+ OAuthProtectedResourceMetadata,
} from "@modelcontextprotocol/sdk/shared/auth.js";
+import { discoverAuthorizationServerMetadata } from "@modelcontextprotocol/sdk/client/auth.js";
import { SESSION_KEYS, getServerSpecificKey } from "./constants";
+import { generateOAuthState } from "@/utils/oauthUtils";
+import { validateRedirectUrl } from "@/utils/urlValidation";
+
+/**
+ * Discovers OAuth scopes from server metadata, with preference for resource metadata scopes
+ * @param serverUrl - The MCP server URL
+ * @param resourceMetadata - Optional resource metadata containing preferred scopes
+ * @returns Promise resolving to space-separated scope string or undefined
+ */
+export const discoverScopes = async (
+ serverUrl: string,
+ resourceMetadata?: OAuthProtectedResourceMetadata,
+): Promise => {
+ try {
+ const metadata = await discoverAuthorizationServerMetadata(
+ new URL("/", serverUrl),
+ );
+
+ // Prefer resource metadata scopes, but fall back to OAuth metadata if empty
+ const resourceScopes = resourceMetadata?.scopes_supported;
+ const oauthScopes = metadata?.scopes_supported;
+
+ const scopesSupported =
+ resourceScopes && resourceScopes.length > 0
+ ? resourceScopes
+ : oauthScopes;
+
+ return scopesSupported && scopesSupported.length > 0
+ ? scopesSupported.join(" ")
+ : undefined;
+ } catch (error) {
+ console.debug("OAuth scope discovery failed:", error);
+ return undefined;
+ }
+};
export const getClientInformationFromSessionStorage = async ({
serverUrl,
@@ -66,10 +103,15 @@ export const clearClientInformationFromSessionStorage = ({
};
export class InspectorOAuthClientProvider implements OAuthClientProvider {
- constructor(protected serverUrl: string) {
+ constructor(
+ protected serverUrl: string,
+ scope?: string,
+ ) {
+ this.scope = scope;
// Save the server URL to session storage
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, serverUrl);
}
+ scope: string | undefined;
get redirectUrl() {
return window.location.origin + "/oauth/callback";
@@ -83,9 +125,14 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider {
response_types: ["code"],
client_name: "MCP Inspector",
client_uri: "https://github.com/modelcontextprotocol/inspector",
+ scope: this.scope ?? "",
};
}
+ state(): string | Promise {
+ return generateOAuthState();
+ }
+
async clientInformation() {
// Try to get the preregistered client information from session storage first
const preregisteredClientInformation =
@@ -105,10 +152,17 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider {
}
saveClientInformation(clientInformation: OAuthClientInformation) {
+ // Remove client_secret before storing (not needed after initial OAuth flow)
+ const safeInfo = Object.fromEntries(
+ Object.entries(clientInformation).filter(
+ ([key]) => key !== "client_secret",
+ ),
+ ) as OAuthClientInformation;
+
// Save the dynamically registered client information to session storage
saveClientInformationToSessionStorage({
serverUrl: this.serverUrl,
- clientInformation,
+ clientInformation: safeInfo,
isPreregistered: false,
});
}
@@ -129,6 +183,8 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider {
}
redirectToAuthorization(authorizationUrl: URL) {
+ // Validate the URL using the shared utility
+ validateRedirectUrl(authorizationUrl.href);
window.location.href = authorizationUrl.href;
}
diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx
index c340d774..624583dc 100644
--- a/client/src/lib/hooks/__tests__/useConnection.test.tsx
+++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx
@@ -3,11 +3,16 @@ import { useConnection } from "../useConnection";
import { z } from "zod";
import { ClientRequest } from "@modelcontextprotocol/sdk/types.js";
import { DEFAULT_INSPECTOR_CONFIG } from "../../constants";
-import { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
+import {
+ SSEClientTransportOptions,
+ SseError,
+} from "@modelcontextprotocol/sdk/client/sse.js";
import {
ElicitResult,
ElicitRequest,
} from "@modelcontextprotocol/sdk/types.js";
+import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
+import { discoverScopes } from "../../auth";
// Mock fetch
global.fetch = jest.fn().mockResolvedValue({
@@ -53,14 +58,27 @@ jest.mock("@modelcontextprotocol/sdk/client/index.js", () => ({
Client: jest.fn().mockImplementation(() => mockClient),
}));
-jest.mock("@modelcontextprotocol/sdk/client/sse.js", () => ({
- SSEClientTransport: jest.fn((url, options) => {
- mockSSETransport.url = url;
- mockSSETransport.options = options;
- return mockSSETransport;
- }),
- SseError: jest.fn(),
-}));
+jest.mock("@modelcontextprotocol/sdk/client/sse.js", () => {
+ // Minimal mock class that supports instanceof checks
+ class SseError extends Error {
+ code: number;
+ event: ErrorEvent;
+ constructor(code: number, message: string, event: ErrorEvent) {
+ super(message);
+ this.code = code;
+ this.event = event;
+ }
+ }
+
+ return {
+ SSEClientTransport: jest.fn((url, options) => {
+ mockSSETransport.url = url;
+ mockSSETransport.options = options;
+ return mockSSETransport;
+ }),
+ SseError,
+ };
+});
jest.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
StreamableHTTPClientTransport: jest.fn((url, options) => {
@@ -85,11 +103,18 @@ jest.mock("@/lib/hooks/useToast", () => ({
jest.mock("../../auth", () => ({
InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({
tokens: jest.fn().mockResolvedValue({ access_token: "mock-token" }),
+ redirectUrl: "http://localhost:3000/oauth/callback",
})),
clearClientInformationFromSessionStorage: jest.fn(),
saveClientInformationToSessionStorage: jest.fn(),
+ discoverScopes: jest.fn(),
}));
+const mockAuth = auth as jest.MockedFunction;
+const mockDiscoverScopes = discoverScopes as jest.MockedFunction<
+ typeof discoverScopes
+>;
+
describe("useConnection", () => {
const defaultProps = {
transportType: "sse" as const,
@@ -714,4 +739,267 @@ describe("useConnection", () => {
).toHaveProperty("X-MCP-Proxy-Auth", "Bearer test-proxy-token");
});
});
+
+ describe("OAuth Error Handling with Scope Discovery", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockAuth.mockResolvedValue("AUTHORIZED");
+ mockDiscoverScopes.mockResolvedValue(undefined);
+ });
+
+ const setup401Error = () => {
+ const mockErrorEvent = new ErrorEvent("error", {
+ message: "Mock error event",
+ });
+ mockClient.connect.mockRejectedValueOnce(
+ new SseError(401, "Unauthorized", mockErrorEvent),
+ );
+ };
+
+ const attemptConnection = async (props = defaultProps) => {
+ const { result } = renderHook(() => useConnection(props));
+ await act(async () => {
+ try {
+ await result.current.connect();
+ } catch {
+ // Expected error from auth handling
+ }
+ });
+ };
+
+ const testCases = [
+ [
+ "discovers and includes scopes in auth call",
+ {
+ discoveredScope: "read write admin",
+ oauthScope: undefined,
+ expectScopeCall: true,
+ expectedAuthScope: "read write admin",
+ authResult: "AUTHORIZED",
+ },
+ ],
+ [
+ "handles scope discovery failure gracefully",
+ {
+ discoveredScope: undefined,
+ oauthScope: undefined,
+ expectScopeCall: true,
+ expectedAuthScope: undefined,
+ authResult: "AUTHORIZED",
+ },
+ ],
+ [
+ "uses manual oauthScope override instead of discovered scopes",
+ {
+ discoveredScope: "discovered:scope",
+ oauthScope: "manual:scope",
+ expectScopeCall: false,
+ expectedAuthScope: "manual:scope",
+ authResult: "AUTHORIZED",
+ },
+ ],
+ [
+ "triggers scope discovery when oauthScope is whitespace",
+ {
+ discoveredScope: "discovered:scope",
+ oauthScope: " ",
+ expectScopeCall: true,
+ expectedAuthScope: "discovered:scope",
+ authResult: "AUTHORIZED",
+ },
+ ],
+ [
+ "handles auth failure after scope discovery",
+ {
+ discoveredScope: "read write",
+ oauthScope: undefined,
+ expectScopeCall: true,
+ expectedAuthScope: "read write",
+ authResult: "UNAUTHORIZED",
+ },
+ ],
+ ] as const;
+
+ test.each(testCases)(
+ "should %s",
+ async (
+ _,
+ {
+ discoveredScope,
+ oauthScope,
+ expectScopeCall,
+ expectedAuthScope,
+ authResult = "AUTHORIZED",
+ },
+ ) => {
+ mockDiscoverScopes.mockResolvedValue(discoveredScope);
+ mockAuth.mockResolvedValue(authResult as never);
+ setup401Error();
+
+ const props =
+ oauthScope !== undefined
+ ? { ...defaultProps, oauthScope }
+ : defaultProps;
+ await attemptConnection(props);
+
+ if (expectScopeCall) {
+ expect(mockDiscoverScopes).toHaveBeenCalledWith(
+ defaultProps.sseUrl,
+ undefined,
+ );
+ } else {
+ expect(mockDiscoverScopes).not.toHaveBeenCalled();
+ }
+
+ expect(mockAuth).toHaveBeenCalledWith(expect.any(Object), {
+ serverUrl: defaultProps.sseUrl,
+ scope: expectedAuthScope,
+ });
+ },
+ );
+
+ it("should handle slow scope discovery gracefully", async () => {
+ mockDiscoverScopes.mockImplementation(
+ () =>
+ new Promise((resolve) => setTimeout(() => resolve(undefined), 100)),
+ );
+
+ setup401Error();
+ await attemptConnection();
+
+ expect(mockDiscoverScopes).toHaveBeenCalledWith(
+ defaultProps.sseUrl,
+ undefined,
+ );
+ expect(mockAuth).toHaveBeenCalledWith(expect.any(Object), {
+ serverUrl: defaultProps.sseUrl,
+ scope: undefined,
+ });
+ });
+ });
+
+ describe("MCP_PROXY_FULL_ADDRESS Configuration", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ // Reset the mock transport objects
+ mockSSETransport.url = undefined;
+ mockSSETransport.options = undefined;
+ mockStreamableHTTPTransport.url = undefined;
+ mockStreamableHTTPTransport.options = undefined;
+ });
+
+ test("sends proxyFullAddress query parameter for stdio transport when configured", async () => {
+ const propsWithProxyFullAddress = {
+ ...defaultProps,
+ transportType: "stdio" as const,
+ command: "test-command",
+ args: "test-args",
+ env: {},
+ config: {
+ ...DEFAULT_INSPECTOR_CONFIG,
+ MCP_PROXY_FULL_ADDRESS: {
+ ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_FULL_ADDRESS,
+ value: "https://example.com/inspector/mcp_proxy",
+ },
+ },
+ };
+
+ const { result } = renderHook(() =>
+ useConnection(propsWithProxyFullAddress),
+ );
+
+ await act(async () => {
+ await result.current.connect();
+ });
+
+ // Check that the URL contains the proxyFullAddress parameter
+ expect(mockSSETransport.url?.searchParams.get("proxyFullAddress")).toBe(
+ "https://example.com/inspector/mcp_proxy",
+ );
+ });
+
+ test("sends proxyFullAddress query parameter for sse transport when configured", async () => {
+ const propsWithProxyFullAddress = {
+ ...defaultProps,
+ transportType: "sse" as const,
+ sseUrl: "http://localhost:8080",
+ config: {
+ ...DEFAULT_INSPECTOR_CONFIG,
+ MCP_PROXY_FULL_ADDRESS: {
+ ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_FULL_ADDRESS,
+ value: "https://example.com/inspector/mcp_proxy",
+ },
+ },
+ };
+
+ const { result } = renderHook(() =>
+ useConnection(propsWithProxyFullAddress),
+ );
+
+ await act(async () => {
+ await result.current.connect();
+ });
+
+ // Check that the URL contains the proxyFullAddress parameter
+ expect(mockSSETransport.url?.searchParams.get("proxyFullAddress")).toBe(
+ "https://example.com/inspector/mcp_proxy",
+ );
+ });
+
+ test("does not send proxyFullAddress parameter when MCP_PROXY_FULL_ADDRESS is empty", async () => {
+ const propsWithEmptyProxy = {
+ ...defaultProps,
+ transportType: "stdio" as const,
+ command: "test-command",
+ args: "test-args",
+ env: {},
+ config: {
+ ...DEFAULT_INSPECTOR_CONFIG,
+ MCP_PROXY_FULL_ADDRESS: {
+ ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_FULL_ADDRESS,
+ value: "",
+ },
+ },
+ };
+
+ const { result } = renderHook(() => useConnection(propsWithEmptyProxy));
+
+ await act(async () => {
+ await result.current.connect();
+ });
+
+ // Check that the URL does not contain the proxyFullAddress parameter
+ expect(
+ mockSSETransport.url?.searchParams.get("proxyFullAddress"),
+ ).toBeNull();
+ });
+
+ test("does not send proxyFullAddress parameter for streamable-http transport", async () => {
+ const propsWithStreamableHttp = {
+ ...defaultProps,
+ transportType: "streamable-http" as const,
+ sseUrl: "http://localhost:8080",
+ config: {
+ ...DEFAULT_INSPECTOR_CONFIG,
+ MCP_PROXY_FULL_ADDRESS: {
+ ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_FULL_ADDRESS,
+ value: "https://example.com/inspector/mcp_proxy",
+ },
+ },
+ };
+
+ const { result } = renderHook(() =>
+ useConnection(propsWithStreamableHttp),
+ );
+
+ await act(async () => {
+ await result.current.connect();
+ });
+
+ // Check that streamable-http transport doesn't get proxyFullAddress parameter
+ expect(
+ mockStreamableHTTPTransport.url?.searchParams.get("proxyFullAddress"),
+ ).toBeNull();
+ });
+ });
});
diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts
index edc21d92..a07b67dc 100644
--- a/client/src/lib/hooks/useConnection.ts
+++ b/client/src/lib/hooks/useConnection.ts
@@ -28,6 +28,7 @@ import {
ToolListChangedNotificationSchema,
PromptListChangedNotificationSchema,
Progress,
+ LoggingLevel,
ElicitRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js";
@@ -35,12 +36,16 @@ import { useEffect, useState } from "react";
import { useToast } from "@/lib/hooks/useToast";
import { z } from "zod";
import { ConnectionStatus } from "../constants";
-import { Notification, StdErrNotificationSchema } from "../notificationTypes";
-import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
+import { Notification } from "../notificationTypes";
+import {
+ auth,
+ discoverOAuthProtectedResourceMetadata,
+} from "@modelcontextprotocol/sdk/client/auth.js";
import {
clearClientInformationFromSessionStorage,
InspectorOAuthClientProvider,
saveClientInformationToSessionStorage,
+ discoverScopes,
} from "../auth";
import packageJson from "../../../package.json";
import {
@@ -72,6 +77,7 @@ interface UseConnectionOptions {
onElicitationRequest?: (request: any, resolve: any) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getRoots?: () => any[];
+ defaultLoggingLevel?: LoggingLevel;
}
export function useConnection({
@@ -86,10 +92,10 @@ export function useConnection({
oauthScope,
config,
onNotification,
- onStdErrNotification,
onPendingRequest,
onElicitationRequest,
getRoots,
+ defaultLoggingLevel,
}: UseConnectionOptions) {
const [connectionStatus, setConnectionStatus] =
useState("disconnected");
@@ -312,11 +318,27 @@ export function useConnection({
const handleAuthError = async (error: unknown) => {
if (is401Error(error)) {
- const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
+ let scope = oauthScope?.trim();
+ if (!scope) {
+ // Only discover resource metadata when we need to discover scopes
+ let resourceMetadata;
+ try {
+ resourceMetadata = await discoverOAuthProtectedResourceMetadata(
+ new URL("/", sseUrl),
+ );
+ } catch {
+ // Resource metadata is optional, continue without it
+ }
+ scope = await discoverScopes(sseUrl, resourceMetadata);
+ }
+ const serverAuthProvider = new InspectorOAuthClientProvider(
+ sseUrl,
+ scope,
+ );
const result = await auth(serverAuthProvider, {
serverUrl: sseUrl,
- scope: oauthScope,
+ scope,
});
return result === "AUTHORIZED";
}
@@ -348,6 +370,7 @@ export function useConnection({
return;
}
+ let lastRequest = "";
try {
// Inject auth manually instead of using SSEClientTransport, because we're
// proxying through the inspector server first.
@@ -386,11 +409,20 @@ export function useConnection({
let mcpProxyServerUrl;
switch (transportType) {
- case "stdio":
+ case "stdio": {
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/stdio`);
mcpProxyServerUrl.searchParams.append("command", command);
mcpProxyServerUrl.searchParams.append("args", args);
mcpProxyServerUrl.searchParams.append("env", JSON.stringify(env));
+
+ const proxyFullAddress = config.MCP_PROXY_FULL_ADDRESS
+ .value as string;
+ if (proxyFullAddress) {
+ mcpProxyServerUrl.searchParams.append(
+ "proxyFullAddress",
+ proxyFullAddress,
+ );
+ }
transportOptions = {
authProvider: serverAuthProvider,
eventSourceInit: {
@@ -408,10 +440,20 @@ export function useConnection({
},
};
break;
+ }
- case "sse":
+ case "sse": {
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/sse`);
mcpProxyServerUrl.searchParams.append("url", sseUrl);
+
+ const proxyFullAddressSSE = config.MCP_PROXY_FULL_ADDRESS
+ .value as string;
+ if (proxyFullAddressSSE) {
+ mcpProxyServerUrl.searchParams.append(
+ "proxyFullAddress",
+ proxyFullAddressSSE,
+ );
+ }
transportOptions = {
eventSourceInit: {
fetch: (
@@ -428,6 +470,7 @@ export function useConnection({
},
};
break;
+ }
case "streamable-http":
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/mcp`);
@@ -481,13 +524,6 @@ export function useConnection({
};
}
- if (onStdErrNotification) {
- client.setNotificationHandler(
- StdErrNotificationSchema,
- onStdErrNotification,
- );
- }
-
let capabilities;
try {
const transport =
@@ -560,6 +596,12 @@ export function useConnection({
});
}
+ if (capabilities?.logging && defaultLoggingLevel) {
+ lastRequest = "logging/setLevel";
+ await client.setLoggingLevel(defaultLoggingLevel);
+ lastRequest = "";
+ }
+
if (onElicitationRequest) {
client.setRequestHandler(ElicitRequestSchema, async (request) => {
return new Promise((resolve) => {
@@ -571,6 +613,17 @@ export function useConnection({
setMcpClient(client);
setConnectionStatus("connected");
} catch (e) {
+ if (
+ lastRequest === "logging/setLevel" &&
+ e instanceof McpError &&
+ e.code === ErrorCode.MethodNotFound
+ ) {
+ toast({
+ title: "Error",
+ description: `Server declares logging capability but doesn't implement method: "${lastRequest}"`,
+ variant: "destructive",
+ });
+ }
console.error(e);
setConnectionStatus("error");
}
diff --git a/client/src/lib/notificationTypes.ts b/client/src/lib/notificationTypes.ts
index 8627ccc6..a956452a 100644
--- a/client/src/lib/notificationTypes.ts
+++ b/client/src/lib/notificationTypes.ts
@@ -5,18 +5,8 @@ import {
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
-export const StdErrNotificationSchema = BaseNotificationSchema.extend({
- method: z.literal("notifications/stderr"),
- params: z.object({
- content: z.string(),
- }),
-});
-
export const NotificationSchema = ClientNotificationSchema.or(
- StdErrNotificationSchema,
-)
- .or(ServerNotificationSchema)
- .or(BaseNotificationSchema);
+ ServerNotificationSchema,
+).or(BaseNotificationSchema);
-export type StdErrNotification = z.infer;
export type Notification = z.infer;
diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts
index d87b3ecd..91465853 100644
--- a/client/src/lib/oauth-state-machine.ts
+++ b/client/src/lib/oauth-state-machine.ts
@@ -1,7 +1,7 @@
import { OAuthStep, AuthDebuggerState } from "./auth-types";
-import { DebugInspectorOAuthClientProvider } from "./auth";
+import { DebugInspectorOAuthClientProvider, discoverScopes } from "./auth";
import {
- discoverOAuthMetadata,
+ discoverAuthorizationServerMetadata,
registerClient,
startAuthorization,
exchangeAuthorization,
@@ -12,6 +12,7 @@ import {
OAuthMetadataSchema,
OAuthProtectedResourceMetadata,
} from "@modelcontextprotocol/sdk/shared/auth.js";
+import { generateOAuthState } from "@/utils/oauthUtils";
export interface StateMachineContext {
state: AuthDebuggerState;
@@ -56,7 +57,7 @@ export const oauthTransitions: Record = {
resourceMetadata ?? undefined,
);
- const metadata = await discoverOAuthMetadata(authServerUrl);
+ const metadata = await discoverAuthorizationServerMetadata(authServerUrl);
if (!metadata) {
throw new Error("Failed to discover OAuth metadata");
}
@@ -88,12 +89,16 @@ export const oauthTransitions: Record = {
clientMetadata.scope = scopesSupported.join(" ");
}
- const fullInformation = await registerClient(context.serverUrl, {
- metadata,
- clientMetadata,
- });
+ // Try Static client first, with DCR as fallback
+ let fullInformation = await context.provider.clientInformation();
+ if (!fullInformation) {
+ fullInformation = await registerClient(context.serverUrl, {
+ metadata,
+ clientMetadata,
+ });
+ context.provider.saveClientInformation(fullInformation);
+ }
- context.provider.saveClientInformation(fullInformation);
context.updateState({
oauthClientInfo: fullInformation,
oauthStep: "authorization_redirect",
@@ -108,10 +113,10 @@ export const oauthTransitions: Record = {
const metadata = context.state.oauthMetadata!;
const clientInformation = context.state.oauthClientInfo!;
- let scope: string | undefined = undefined;
- if (metadata.scopes_supported) {
- scope = metadata.scopes_supported.join(" ");
- }
+ const scope = await discoverScopes(
+ context.serverUrl,
+ context.state.resourceMetadata ?? undefined,
+ );
const { authorizationUrl, codeVerifier } = await startAuthorization(
context.serverUrl,
@@ -120,6 +125,7 @@ export const oauthTransitions: Record = {
clientInformation,
redirectUrl: context.provider.redirectUrl,
scope,
+ state: generateOAuthState(),
resource: context.state.resource ?? undefined,
},
);
@@ -171,7 +177,11 @@ export const oauthTransitions: Record = {
authorizationCode: context.state.authorizationCode,
codeVerifier,
redirectUri: context.provider.redirectUrl,
- resource: context.state.resource ?? undefined,
+ resource: context.state.resource
+ ? context.state.resource instanceof URL
+ ? context.state.resource
+ : new URL(context.state.resource)
+ : undefined,
});
context.provider.saveTokens(tokens);
diff --git a/client/src/utils/__tests__/oauthUtils.ts b/client/src/utils/__tests__/oauthUtils.test.ts
similarity index 92%
rename from client/src/utils/__tests__/oauthUtils.ts
rename to client/src/utils/__tests__/oauthUtils.test.ts
index cc9674cb..c42f014e 100644
--- a/client/src/utils/__tests__/oauthUtils.ts
+++ b/client/src/utils/__tests__/oauthUtils.test.ts
@@ -1,6 +1,7 @@
import {
generateOAuthErrorDescription,
parseOAuthCallbackParams,
+ generateOAuthState,
} from "@/utils/oauthUtils.ts";
describe("parseOAuthCallbackParams", () => {
@@ -75,4 +76,11 @@ describe("generateOAuthErrorDescription", () => {
"Error: invalid_request.\nDetails: The request could not be completed as dialed.\nMore info: https://example.com/error-docs.",
);
});
+
+ describe("generateOAuthState", () => {
+ it("Returns a string", () => {
+ expect(generateOAuthState()).toBeDefined();
+ expect(generateOAuthState()).toHaveLength(64);
+ });
+ });
});
diff --git a/client/src/utils/__tests__/schemaUtils.test.ts b/client/src/utils/__tests__/schemaUtils.test.ts
index 133e25ec..0ecd8075 100644
--- a/client/src/utils/__tests__/schemaUtils.test.ts
+++ b/client/src/utils/__tests__/schemaUtils.test.ts
@@ -1,6 +1,7 @@
import {
generateDefaultValue,
formatFieldLabel,
+ normalizeUnionType,
cacheToolOutputSchemas,
getToolOutputValidator,
validateToolOutput,
@@ -142,6 +143,189 @@ describe("formatFieldLabel", () => {
});
});
+describe("normalizeUnionType", () => {
+ test("normalizes anyOf with string and null to string type", () => {
+ const schema: JsonSchemaType = {
+ anyOf: [{ type: "string" }, { type: "null" }],
+ description: "Optional string parameter",
+ };
+
+ const normalized = normalizeUnionType(schema);
+
+ expect(normalized.type).toBe("string");
+ expect(normalized.anyOf).toBeUndefined();
+ expect(normalized.description).toBe("Optional string parameter");
+ });
+
+ test("normalizes anyOf with boolean and null to boolean type", () => {
+ const schema: JsonSchemaType = {
+ anyOf: [{ type: "boolean" }, { type: "null" }],
+ description: "Optional boolean parameter",
+ };
+
+ const normalized = normalizeUnionType(schema);
+
+ expect(normalized.type).toBe("boolean");
+ expect(normalized.anyOf).toBeUndefined();
+ expect(normalized.description).toBe("Optional boolean parameter");
+ });
+
+ test("normalizes array type with string and null to string type", () => {
+ const schema: JsonSchemaType = {
+ type: ["string", "null"],
+ description: "Optional string parameter",
+ };
+
+ const normalized = normalizeUnionType(schema);
+
+ expect(normalized.type).toBe("string");
+ expect(normalized.description).toBe("Optional string parameter");
+ });
+
+ test("normalizes array type with boolean and null to boolean type", () => {
+ const schema: JsonSchemaType = {
+ type: ["boolean", "null"],
+ description: "Optional boolean parameter",
+ };
+
+ const normalized = normalizeUnionType(schema);
+
+ expect(normalized.type).toBe("boolean");
+ expect(normalized.description).toBe("Optional boolean parameter");
+ });
+
+ test("normalizes anyOf with number and null to number type", () => {
+ const schema: JsonSchemaType = {
+ anyOf: [{ type: "number" }, { type: "null" }],
+ description: "Optional number parameter",
+ };
+
+ const normalized = normalizeUnionType(schema);
+
+ expect(normalized.type).toBe("number");
+ expect(normalized.anyOf).toBeUndefined();
+ expect(normalized.description).toBe("Optional number parameter");
+ });
+
+ test("normalizes anyOf with integer and null to integer type", () => {
+ const schema: JsonSchemaType = {
+ anyOf: [{ type: "integer" }, { type: "null" }],
+ description: "Optional integer parameter",
+ };
+
+ const normalized = normalizeUnionType(schema);
+
+ expect(normalized.type).toBe("integer");
+ expect(normalized.anyOf).toBeUndefined();
+ expect(normalized.description).toBe("Optional integer parameter");
+ });
+
+ test("normalizes array type with number and null to number type", () => {
+ const schema: JsonSchemaType = {
+ type: ["number", "null"],
+ description: "Optional number parameter",
+ };
+
+ const normalized = normalizeUnionType(schema);
+
+ expect(normalized.type).toBe("number");
+ expect(normalized.description).toBe("Optional number parameter");
+ });
+
+ test("normalizes array type with integer and null to integer type", () => {
+ const schema: JsonSchemaType = {
+ type: ["integer", "null"],
+ description: "Optional integer parameter",
+ };
+
+ const normalized = normalizeUnionType(schema);
+
+ expect(normalized.type).toBe("integer");
+ expect(normalized.description).toBe("Optional integer parameter");
+ });
+
+ test("handles anyOf with reversed order (null first)", () => {
+ const schema: JsonSchemaType = {
+ anyOf: [{ type: "null" }, { type: "string" }],
+ };
+
+ const normalized = normalizeUnionType(schema);
+
+ expect(normalized.type).toBe("string");
+ expect(normalized.anyOf).toBeUndefined();
+ });
+
+ test("leaves non-union schemas unchanged", () => {
+ const schema: JsonSchemaType = {
+ type: "string",
+ description: "Regular string parameter",
+ };
+
+ const normalized = normalizeUnionType(schema);
+
+ expect(normalized).toEqual(schema);
+ });
+
+ test("leaves anyOf with non-matching types unchanged", () => {
+ const schema: JsonSchemaType = {
+ anyOf: [{ type: "string" }, { type: "number" }],
+ };
+
+ const normalized = normalizeUnionType(schema);
+
+ expect(normalized).toEqual(schema);
+ });
+
+ test("leaves anyOf with more than two types unchanged", () => {
+ const schema: JsonSchemaType = {
+ anyOf: [{ type: "string" }, { type: "number" }, { type: "null" }],
+ };
+
+ const normalized = normalizeUnionType(schema);
+
+ expect(normalized).toEqual(schema);
+ });
+
+ test("leaves array type with non-matching types unchanged", () => {
+ const schema: JsonSchemaType = {
+ type: ["string", "number"],
+ };
+
+ const normalized = normalizeUnionType(schema);
+
+ expect(normalized).toEqual(schema);
+ });
+
+ test("handles schemas without type or anyOf", () => {
+ const schema: JsonSchemaType = {
+ description: "Schema without type",
+ };
+
+ const normalized = normalizeUnionType(schema);
+
+ expect(normalized).toEqual(schema);
+ });
+
+ test("preserves other properties when normalizing", () => {
+ const schema: JsonSchemaType = {
+ anyOf: [{ type: "string" }, { type: "null" }],
+ description: "Optional string",
+ minLength: 1,
+ maxLength: 100,
+ pattern: "^[a-z]+$",
+ };
+
+ const normalized = normalizeUnionType(schema);
+
+ expect(normalized.type).toBe("string");
+ expect(normalized.anyOf).toBeUndefined();
+ expect(normalized.description).toBe("Optional string");
+ expect(normalized.minLength).toBe(1);
+ expect(normalized.maxLength).toBe(100);
+ expect(normalized.pattern).toBe("^[a-z]+$");
+ });
+});
+
describe("Output Schema Validation", () => {
const mockTools: Tool[] = [
{
diff --git a/client/src/utils/__tests__/urlValidation.test.ts b/client/src/utils/__tests__/urlValidation.test.ts
new file mode 100644
index 00000000..9ee6a454
--- /dev/null
+++ b/client/src/utils/__tests__/urlValidation.test.ts
@@ -0,0 +1,135 @@
+import { validateRedirectUrl } from "../urlValidation";
+
+describe("validateRedirectUrl", () => {
+ describe("valid URLs", () => {
+ it("should allow HTTP URLs", () => {
+ expect(() => validateRedirectUrl("http://example.com")).not.toThrow();
+ });
+
+ it("should allow HTTPS URLs", () => {
+ expect(() => validateRedirectUrl("https://example.com")).not.toThrow();
+ });
+
+ it("should allow URLs with ports", () => {
+ expect(() =>
+ validateRedirectUrl("https://example.com:8080"),
+ ).not.toThrow();
+ });
+
+ it("should allow URLs with paths", () => {
+ expect(() =>
+ validateRedirectUrl("https://example.com/path/to/auth"),
+ ).not.toThrow();
+ });
+
+ it("should allow URLs with query parameters", () => {
+ expect(() =>
+ validateRedirectUrl("https://example.com?param=value"),
+ ).not.toThrow();
+ });
+ });
+
+ describe("invalid URLs - XSS vectors", () => {
+ it("should block javascript: protocol", () => {
+ expect(() => validateRedirectUrl("javascript:alert('XSS')")).toThrow(
+ "Authorization URL must be HTTP or HTTPS",
+ );
+ });
+
+ it("should block javascript: with encoded characters", () => {
+ expect(() =>
+ validateRedirectUrl("javascript:alert%28%27XSS%27%29"),
+ ).toThrow("Authorization URL must be HTTP or HTTPS");
+ });
+
+ it("should block data: protocol", () => {
+ expect(() =>
+ validateRedirectUrl("data:text/html,"),
+ ).toThrow("Authorization URL must be HTTP or HTTPS");
+ });
+
+ it("should block vbscript: protocol", () => {
+ expect(() => validateRedirectUrl("vbscript:msgbox")).toThrow(
+ "Authorization URL must be HTTP or HTTPS",
+ );
+ });
+
+ it("should block file: protocol", () => {
+ expect(() => validateRedirectUrl("file:///etc/passwd")).toThrow(
+ "Authorization URL must be HTTP or HTTPS",
+ );
+ });
+
+ it("should block about: protocol", () => {
+ expect(() => validateRedirectUrl("about:blank")).toThrow(
+ "Authorization URL must be HTTP or HTTPS",
+ );
+ });
+
+ it("should block custom protocols", () => {
+ expect(() => validateRedirectUrl("custom://example")).toThrow(
+ "Authorization URL must be HTTP or HTTPS",
+ );
+ });
+ });
+
+ describe("edge cases", () => {
+ it("should handle malformed URLs", () => {
+ expect(() => validateRedirectUrl("not a url")).toThrow(
+ "Invalid URL: not a url",
+ );
+ });
+
+ it("should handle empty string", () => {
+ expect(() => validateRedirectUrl("")).toThrow("Invalid URL: ");
+ });
+
+ it("should handle URLs with unicode characters", () => {
+ expect(() => validateRedirectUrl("https://例え.jp")).not.toThrow();
+ });
+
+ it("should handle URLs with case variations", () => {
+ expect(() => validateRedirectUrl("HTTPS://EXAMPLE.COM")).not.toThrow();
+ expect(() => validateRedirectUrl("HtTpS://example.com")).not.toThrow();
+ });
+
+ it("should handle protocol-relative URLs as invalid", () => {
+ expect(() => validateRedirectUrl("//example.com")).toThrow(
+ "Invalid URL: //example.com",
+ );
+ });
+
+ it("should handle URLs with authentication", () => {
+ expect(() =>
+ validateRedirectUrl("https://user:pass@example.com"),
+ ).not.toThrow();
+ });
+ });
+
+ describe("security considerations", () => {
+ it("should not be fooled by whitespace", () => {
+ expect(() => validateRedirectUrl(" javascript:alert('XSS')")).toThrow();
+ expect(() => validateRedirectUrl("javascript:alert('XSS') ")).toThrow();
+ });
+
+ it("should handle null bytes", () => {
+ expect(() =>
+ validateRedirectUrl("java\x00script:alert('XSS')"),
+ ).toThrow();
+ });
+
+ it("should handle tab characters", () => {
+ expect(() => validateRedirectUrl("java\tscript:alert('XSS')")).toThrow();
+ });
+
+ it("should handle newlines", () => {
+ expect(() => validateRedirectUrl("java\nscript:alert('XSS')")).toThrow();
+ });
+
+ it("should handle mixed case protocols", () => {
+ expect(() => validateRedirectUrl("JaVaScRiPt:alert('XSS')")).toThrow(
+ "Authorization URL must be HTTP or HTTPS",
+ );
+ });
+ });
+});
diff --git a/client/src/utils/jsonUtils.ts b/client/src/utils/jsonUtils.ts
index 338b642a..08de50ba 100644
--- a/client/src/utils/jsonUtils.ts
+++ b/client/src/utils/jsonUtils.ts
@@ -21,7 +21,16 @@ export type JsonSchemaType = {
| "boolean"
| "array"
| "object"
- | "null";
+ | "null"
+ | (
+ | "string"
+ | "number"
+ | "integer"
+ | "boolean"
+ | "array"
+ | "object"
+ | "null"
+ )[];
title?: string;
description?: string;
required?: string[];
@@ -54,12 +63,22 @@ export type DataType =
| "array"
| "null";
+/**
+ * Determines the specific data type of a JSON value
+ * @param value The JSON value to analyze
+ * @returns The specific data type including "array" and "null" as distinct types
+ */
export function getDataType(value: JsonValue): DataType {
if (Array.isArray(value)) return "array";
if (value === null) return "null";
return typeof value;
}
+/**
+ * Attempts to parse a string as JSON, only for objects and arrays
+ * @param str The string to parse
+ * @returns Object with success boolean and either parsed data or original string
+ */
export function tryParseJson(str: string): {
success: boolean;
data: JsonValue;
diff --git a/client/src/utils/oauthUtils.ts b/client/src/utils/oauthUtils.ts
index c971271e..9b51179c 100644
--- a/client/src/utils/oauthUtils.ts
+++ b/client/src/utils/oauthUtils.ts
@@ -24,6 +24,11 @@ type CallbackParams =
error_uri: string | null;
};
+/**
+ * Parses OAuth 2.1 callback parameters from a URL search string
+ * @param location The URL search string (e.g., "?code=abc123" or "?error=access_denied")
+ * @returns Parsed callback parameters with success/error information
+ */
export const parseOAuthCallbackParams = (location: string): CallbackParams => {
const params = new URLSearchParams(location);
@@ -48,6 +53,25 @@ export const parseOAuthCallbackParams = (location: string): CallbackParams => {
};
};
+/**
+ * Generate a random state for the OAuth 2.0 flow.
+ *
+ * @returns A random state for the OAuth 2.0 flow.
+ */
+export const generateOAuthState = () => {
+ // Generate a random state
+ const array = new Uint8Array(32);
+ crypto.getRandomValues(array);
+ return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
+ "",
+ );
+};
+
+/**
+ * Generates a human-readable error description from OAuth callback error parameters
+ * @param params OAuth error callback parameters containing error details
+ * @returns Formatted multiline error message with error code, description, and optional URI
+ */
export const generateOAuthErrorDescription = (
params: Extract,
): string => {
diff --git a/client/src/utils/schemaUtils.ts b/client/src/utils/schemaUtils.ts
index 9fc7a724..7a175142 100644
--- a/client/src/utils/schemaUtils.ts
+++ b/client/src/utils/schemaUtils.ts
@@ -145,6 +145,95 @@ export function isPropertyRequired(
return schema.required?.includes(propertyName) ?? false;
}
+/**
+ * Normalizes union types (like string|null from FastMCP) to simple types for form rendering
+ * @param schema The JSON schema to normalize
+ * @returns A normalized schema or the original schema
+ */
+export function normalizeUnionType(schema: JsonSchemaType): JsonSchemaType {
+ // Handle anyOf with exactly string and null (FastMCP pattern)
+ if (
+ schema.anyOf &&
+ schema.anyOf.length === 2 &&
+ schema.anyOf.some((t) => (t as JsonSchemaType).type === "string") &&
+ schema.anyOf.some((t) => (t as JsonSchemaType).type === "null")
+ ) {
+ return { ...schema, type: "string", anyOf: undefined };
+ }
+
+ // Handle anyOf with exactly boolean and null (FastMCP pattern)
+ if (
+ schema.anyOf &&
+ schema.anyOf.length === 2 &&
+ schema.anyOf.some((t) => (t as JsonSchemaType).type === "boolean") &&
+ schema.anyOf.some((t) => (t as JsonSchemaType).type === "null")
+ ) {
+ return { ...schema, type: "boolean", anyOf: undefined };
+ }
+
+ // Handle anyOf with exactly number and null (FastMCP pattern)
+ if (
+ schema.anyOf &&
+ schema.anyOf.length === 2 &&
+ schema.anyOf.some((t) => (t as JsonSchemaType).type === "number") &&
+ schema.anyOf.some((t) => (t as JsonSchemaType).type === "null")
+ ) {
+ return { ...schema, type: "number", anyOf: undefined };
+ }
+
+ // Handle anyOf with exactly integer and null (FastMCP pattern)
+ if (
+ schema.anyOf &&
+ schema.anyOf.length === 2 &&
+ schema.anyOf.some((t) => (t as JsonSchemaType).type === "integer") &&
+ schema.anyOf.some((t) => (t as JsonSchemaType).type === "null")
+ ) {
+ return { ...schema, type: "integer", anyOf: undefined };
+ }
+
+ // Handle array type with exactly string and null
+ if (
+ Array.isArray(schema.type) &&
+ schema.type.length === 2 &&
+ schema.type.includes("string") &&
+ schema.type.includes("null")
+ ) {
+ return { ...schema, type: "string" };
+ }
+
+ // Handle array type with exactly boolean and null
+ if (
+ Array.isArray(schema.type) &&
+ schema.type.length === 2 &&
+ schema.type.includes("boolean") &&
+ schema.type.includes("null")
+ ) {
+ return { ...schema, type: "boolean" };
+ }
+
+ // Handle array type with exactly number and null
+ if (
+ Array.isArray(schema.type) &&
+ schema.type.length === 2 &&
+ schema.type.includes("number") &&
+ schema.type.includes("null")
+ ) {
+ return { ...schema, type: "number" };
+ }
+
+ // Handle array type with exactly integer and null
+ if (
+ Array.isArray(schema.type) &&
+ schema.type.length === 2 &&
+ schema.type.includes("integer") &&
+ schema.type.includes("null")
+ ) {
+ return { ...schema, type: "integer" };
+ }
+
+ return schema;
+}
+
/**
* Formats a field key into a human-readable label
* @param key The field key to format
diff --git a/client/src/utils/urlValidation.ts b/client/src/utils/urlValidation.ts
new file mode 100644
index 00000000..70bc458b
--- /dev/null
+++ b/client/src/utils/urlValidation.ts
@@ -0,0 +1,24 @@
+/**
+ * Validates that a URL is safe for redirection.
+ * Only allows HTTP and HTTPS protocols to prevent XSS attacks.
+ *
+ * @param url - The URL string to validate
+ * @throws Error if the URL has an unsafe protocol
+ */
+export function validateRedirectUrl(url: string): void {
+ try {
+ const parsedUrl = new URL(url);
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
+ throw new Error("Authorization URL must be HTTP or HTTPS");
+ }
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ error.message === "Authorization URL must be HTTP or HTTPS"
+ ) {
+ throw error;
+ }
+ // If URL parsing fails, it's also invalid
+ throw new Error(`Invalid URL: ${url}`);
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index 53313497..7ffb9724 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@modelcontextprotocol/inspector",
- "version": "0.16.2",
+ "version": "0.16.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@modelcontextprotocol/inspector",
- "version": "0.16.2",
+ "version": "0.16.5",
"license": "MIT",
"workspaces": [
"client",
@@ -14,27 +14,29 @@
"cli"
],
"dependencies": {
- "@modelcontextprotocol/inspector-cli": "^0.16.2",
- "@modelcontextprotocol/inspector-client": "^0.16.2",
- "@modelcontextprotocol/inspector-server": "^0.16.2",
- "@modelcontextprotocol/sdk": "^1.17.0",
- "concurrently": "^9.0.1",
- "open": "^10.1.0",
- "shell-quote": "^1.8.2",
+ "@modelcontextprotocol/inspector-cli": "^0.16.5",
+ "@modelcontextprotocol/inspector-client": "^0.16.5",
+ "@modelcontextprotocol/inspector-server": "^0.16.5",
+ "@modelcontextprotocol/sdk": "^1.17.3",
+ "concurrently": "^9.2.0",
+ "open": "^10.2.0",
+ "shell-quote": "^1.8.3",
"spawn-rx": "^5.1.2",
"ts-node": "^10.9.2",
- "zod": "^3.23.8"
+ "zod": "^3.25.76"
},
"bin": {
"mcp-inspector": "cli/build/cli.js"
},
"devDependencies": {
- "@playwright/test": "^1.52.0",
+ "@playwright/test": "^1.54.1",
"@types/jest": "^29.5.14",
- "@types/node": "^22.7.5",
+ "@types/node": "^22.17.0",
"@types/shell-quote": "^1.7.5",
+ "husky": "^9.1.7",
"jest-fixed-jsdom": "^0.0.9",
- "prettier": "3.3.3",
+ "lint-staged": "^16.1.5",
+ "prettier": "^3.6.2",
"rimraf": "^6.0.1",
"typescript": "^5.4.2"
},
@@ -44,10 +46,10 @@
},
"cli": {
"name": "@modelcontextprotocol/inspector-cli",
- "version": "0.16.2",
+ "version": "0.16.5",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/sdk": "^1.17.0",
+ "@modelcontextprotocol/sdk": "^1.17.3",
"commander": "^13.1.0",
"spawn-rx": "^5.1.2"
},
@@ -67,10 +69,10 @@
},
"client": {
"name": "@modelcontextprotocol/inspector-client",
- "version": "0.16.2",
+ "version": "0.16.5",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/sdk": "^1.17.0",
+ "@modelcontextprotocol/sdk": "^1.17.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-dialog": "^1.1.3",
"@radix-ui/react-icons": "^1.3.0",
@@ -94,7 +96,7 @@
"serve-handler": "^6.1.6",
"tailwind-merge": "^2.5.3",
"tailwindcss-animate": "^1.0.7",
- "zod": "^3.23.8"
+ "zod": "^3.25.76"
},
"bin": {
"mcp-inspector-client": "bin/start.js"
@@ -104,12 +106,12 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@types/jest": "^29.5.14",
- "@types/node": "^22.7.5",
+ "@types/node": "^22.17.0",
"@types/prismjs": "^1.26.5",
- "@types/react": "^18.3.10",
+ "@types/react": "^18.3.23",
"@types/react-dom": "^18.3.0",
"@types/serve-handler": "^6.1.4",
- "@vitejs/plugin-react": "^4.3.2",
+ "@vitejs/plugin-react": "^4.7.0",
"autoprefixer": "^10.4.20",
"co": "^4.6.0",
"eslint": "^9.11.1",
@@ -118,12 +120,12 @@
"globals": "^15.9.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
- "postcss": "^8.4.47",
+ "postcss": "^8.5.6",
"tailwindcss": "^3.4.13",
- "ts-jest": "^29.2.6",
+ "ts-jest": "^29.4.0",
"typescript": "^5.5.3",
- "typescript-eslint": "^8.7.0",
- "vite": "^6.3.0"
+ "typescript-eslint": "^8.38.0",
+ "vite": "^6.3.5"
}
},
"client/node_modules/pkce-challenge": {
@@ -184,9 +186,9 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.1.tgz",
- "integrity": "sha512-Q+E+rd/yBzNQhXkG+zQnF58e4zoZfBedaxwzPmicKsiK3nt8iJYrSrDbjwFFDGC4f+rPafqRaPH6TsDoSvMf7A==",
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz",
+ "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -194,22 +196,22 @@
}
},
"node_modules/@babel/core": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz",
- "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
+ "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
- "@babel/generator": "^7.27.1",
- "@babel/helper-compilation-targets": "^7.27.1",
- "@babel/helper-module-transforms": "^7.27.1",
- "@babel/helpers": "^7.27.1",
- "@babel/parser": "^7.27.1",
- "@babel/template": "^7.27.1",
- "@babel/traverse": "^7.27.1",
- "@babel/types": "^7.27.1",
+ "@babel/generator": "^7.28.0",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.27.3",
+ "@babel/helpers": "^7.27.6",
+ "@babel/parser": "^7.28.0",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.0",
+ "@babel/types": "^7.28.0",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -225,16 +227,16 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz",
- "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==",
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz",
+ "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.27.1",
- "@babel/types": "^7.27.1",
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.25",
+ "@babel/parser": "^7.28.0",
+ "@babel/types": "^7.28.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
},
"engines": {
@@ -242,13 +244,13 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.1.tgz",
- "integrity": "sha512-2YaDd/Rd9E598B5+WIc8wJPmWETiiJXFYVE60oX8FDohv7rAUU3CQj+A1MgeEmcsk2+dQuEjIe/GDvig0SqL4g==",
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/compat-data": "^7.27.1",
+ "@babel/compat-data": "^7.27.2",
"@babel/helper-validator-option": "^7.27.1",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
@@ -258,6 +260,16 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/helper-module-imports": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
@@ -273,15 +285,15 @@
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz",
- "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==",
+ "version": "7.27.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
+ "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1",
- "@babel/traverse": "^7.27.1"
+ "@babel/traverse": "^7.27.3"
},
"engines": {
"node": ">=6.9.0"
@@ -331,27 +343,27 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz",
- "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==",
+ "version": "7.28.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz",
+ "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/template": "^7.27.1",
- "@babel/types": "^7.27.1"
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.2"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz",
- "integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==",
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
+ "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.27.1"
+ "@babel/types": "^7.28.0"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -642,14 +654,14 @@
}
},
"node_modules/@babel/template": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz",
- "integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==",
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
- "@babel/parser": "^7.27.1",
+ "@babel/parser": "^7.27.2",
"@babel/types": "^7.27.1"
},
"engines": {
@@ -657,38 +669,28 @@
}
},
"node_modules/@babel/traverse": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz",
- "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==",
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz",
+ "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
- "@babel/generator": "^7.27.1",
- "@babel/parser": "^7.27.1",
- "@babel/template": "^7.27.1",
- "@babel/types": "^7.27.1",
- "debug": "^4.3.1",
- "globals": "^11.1.0"
+ "@babel/generator": "^7.28.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.0",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.0",
+ "debug": "^4.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
- "node_modules/@babel/traverse/node_modules/globals": {
- "version": "11.12.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
- "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/@babel/types": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz",
- "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==",
+ "version": "7.28.2",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
+ "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1952,17 +1954,13 @@
}
},
"node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.8",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
- "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+ "version": "0.3.12",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
+ "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
"license": "MIT",
"dependencies": {
- "@jridgewell/set-array": "^1.2.1",
- "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
@@ -1974,15 +1972,6 @@
"node": ">=6.0.0"
}
},
- "node_modules/@jridgewell/set-array": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
- "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
@@ -1990,9 +1979,9 @@
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.25",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
- "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "version": "0.3.29",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
+ "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -2012,9 +2001,9 @@
"link": true
},
"node_modules/@modelcontextprotocol/sdk": {
- "version": "1.17.0",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.0.tgz",
- "integrity": "sha512-qFfbWFA7r1Sd8D697L7GkTd36yqDuTkvz0KfOGkgXR8EUhQn3/EDNIR/qUdQNMT8IjmasBvHWuXeisxtXTQT2g==",
+ "version": "1.17.3",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.3.tgz",
+ "integrity": "sha512-JPwUKWSsbzx+DLFznf/QZ32Qa+ptfbUlHhRLrBQBAFu9iI1iYvizM4p+zhhRDceSsPutXp4z+R/HPVphlIiclg==",
"license": "MIT",
"dependencies": {
"ajv": "^6.12.6",
@@ -2080,13 +2069,13 @@
}
},
"node_modules/@playwright/test": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz",
- "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==",
+ "version": "1.54.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz",
+ "integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright": "1.52.0"
+ "playwright": "1.54.1"
},
"bin": {
"playwright": "cli.js"
@@ -2879,6 +2868,13 @@
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT"
},
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz",
@@ -3386,9 +3382,9 @@
}
},
"node_modules/@types/cors": {
- "version": "2.8.17",
- "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
- "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
+ "version": "2.8.19",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3403,9 +3399,9 @@
"license": "MIT"
},
"node_modules/@types/express": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
- "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz",
+ "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3545,9 +3541,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "22.15.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz",
- "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==",
+ "version": "22.17.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.0.tgz",
+ "integrity": "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@@ -3582,9 +3578,9 @@
"license": "MIT"
},
"node_modules/@types/react": {
- "version": "18.3.20",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz",
- "integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==",
+ "version": "18.3.23",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
+ "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -3684,21 +3680,21 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.31.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz",
- "integrity": "sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ==",
+ "version": "8.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
+ "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "8.31.1",
- "@typescript-eslint/type-utils": "8.31.1",
- "@typescript-eslint/utils": "8.31.1",
- "@typescript-eslint/visitor-keys": "8.31.1",
+ "@typescript-eslint/scope-manager": "8.38.0",
+ "@typescript-eslint/type-utils": "8.38.0",
+ "@typescript-eslint/utils": "8.38.0",
+ "@typescript-eslint/visitor-keys": "8.38.0",
"graphemer": "^1.4.0",
- "ignore": "^5.3.1",
+ "ignore": "^7.0.0",
"natural-compare": "^1.4.0",
- "ts-api-utils": "^2.0.1"
+ "ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3708,22 +3704,32 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
+ "@typescript-eslint/parser": "^8.38.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
"node_modules/@typescript-eslint/parser": {
- "version": "8.31.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.1.tgz",
- "integrity": "sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==",
+ "version": "8.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz",
+ "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.31.1",
- "@typescript-eslint/types": "8.31.1",
- "@typescript-eslint/typescript-estree": "8.31.1",
- "@typescript-eslint/visitor-keys": "8.31.1",
+ "@typescript-eslint/scope-manager": "8.38.0",
+ "@typescript-eslint/types": "8.38.0",
+ "@typescript-eslint/typescript-estree": "8.38.0",
+ "@typescript-eslint/visitor-keys": "8.38.0",
"debug": "^4.3.4"
},
"engines": {
@@ -3738,15 +3744,37 @@
"typescript": ">=4.8.4 <5.9.0"
}
},
- "node_modules/@typescript-eslint/scope-manager": {
- "version": "8.31.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.1.tgz",
- "integrity": "sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw==",
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz",
+ "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.31.1",
- "@typescript-eslint/visitor-keys": "8.31.1"
+ "@typescript-eslint/tsconfig-utils": "^8.38.0",
+ "@typescript-eslint/types": "^8.38.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz",
+ "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.38.0",
+ "@typescript-eslint/visitor-keys": "8.38.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3756,17 +3784,35 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz",
+ "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.31.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.31.1.tgz",
- "integrity": "sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA==",
+ "version": "8.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz",
+ "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/typescript-estree": "8.31.1",
- "@typescript-eslint/utils": "8.31.1",
+ "@typescript-eslint/types": "8.38.0",
+ "@typescript-eslint/typescript-estree": "8.38.0",
+ "@typescript-eslint/utils": "8.38.0",
"debug": "^4.3.4",
- "ts-api-utils": "^2.0.1"
+ "ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3781,9 +3827,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.31.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.1.tgz",
- "integrity": "sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ==",
+ "version": "8.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz",
+ "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3795,20 +3841,22 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.31.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.1.tgz",
- "integrity": "sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag==",
+ "version": "8.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz",
+ "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.31.1",
- "@typescript-eslint/visitor-keys": "8.31.1",
+ "@typescript-eslint/project-service": "8.38.0",
+ "@typescript-eslint/tsconfig-utils": "8.38.0",
+ "@typescript-eslint/types": "8.38.0",
+ "@typescript-eslint/visitor-keys": "8.38.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
- "ts-api-utils": "^2.0.1"
+ "ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3848,9 +3896,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
- "version": "7.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
- "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -3861,16 +3909,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.31.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.31.1.tgz",
- "integrity": "sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ==",
+ "version": "8.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz",
+ "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint-community/eslint-utils": "^4.4.0",
- "@typescript-eslint/scope-manager": "8.31.1",
- "@typescript-eslint/types": "8.31.1",
- "@typescript-eslint/typescript-estree": "8.31.1"
+ "@eslint-community/eslint-utils": "^4.7.0",
+ "@typescript-eslint/scope-manager": "8.38.0",
+ "@typescript-eslint/types": "8.38.0",
+ "@typescript-eslint/typescript-estree": "8.38.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3885,14 +3933,14 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.31.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.1.tgz",
- "integrity": "sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw==",
+ "version": "8.38.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz",
+ "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.31.1",
- "eslint-visitor-keys": "^4.2.0"
+ "@typescript-eslint/types": "8.38.0",
+ "eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3903,15 +3951,16 @@
}
},
"node_modules/@vitejs/plugin-react": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz",
- "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==",
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/core": "^7.26.10",
- "@babel/plugin-transform-react-jsx-self": "^7.25.9",
- "@babel/plugin-transform-react-jsx-source": "^7.25.9",
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
"@types/babel__core": "^7.20.5",
"react-refresh": "^0.17.0"
},
@@ -3919,7 +3968,7 @@
"node": "^14.18.0 || >=16.0.0"
},
"peerDependencies": {
- "vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
"node_modules/abab": {
@@ -4602,6 +4651,93 @@
"url": "https://polar.sh/cva"
}
},
+ "node_modules/cli-cursor": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
+ "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz",
+ "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "slice-ansi": "^5.0.0",
+ "string-width": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate/node_modules/ansi-regex": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
+ "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/cli-truncate/node_modules/emoji-regex": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
+ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cli-truncate/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -4677,6 +4813,13 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -4706,9 +4849,9 @@
"license": "MIT"
},
"node_modules/concurrently": {
- "version": "9.1.2",
- "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz",
- "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==",
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz",
+ "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==",
"license": "MIT",
"dependencies": {
"chalk": "^4.1.2",
@@ -4915,9 +5058,9 @@
}
},
"node_modules/debug": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
- "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -5198,6 +5341,19 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/environment": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
+ "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@@ -5546,6 +5702,13 @@
"node": ">= 0.6"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/eventsource": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz",
@@ -6002,6 +6165,19 @@
"node": "6.* || 8.* || >= 10.*"
}
},
+ "node_modules/get-east-asian-width": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
+ "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -6281,6 +6457,22 @@
"node": ">=10.17.0"
}
},
+ "node_modules/husky": {
+ "version": "9.1.7",
+ "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
+ "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "husky": "bin.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/typicode"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -7810,6 +8002,160 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"license": "MIT"
},
+ "node_modules/lint-staged": {
+ "version": "16.1.5",
+ "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.1.5.tgz",
+ "integrity": "sha512-uAeQQwByI6dfV7wpt/gVqg+jAPaSp8WwOA8kKC/dv1qw14oGpnpAisY65ibGHUGDUv0rYaZ8CAJZ/1U8hUvC2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^5.5.0",
+ "commander": "^14.0.0",
+ "debug": "^4.4.1",
+ "lilconfig": "^3.1.3",
+ "listr2": "^9.0.1",
+ "micromatch": "^4.0.8",
+ "nano-spawn": "^1.0.2",
+ "pidtree": "^0.6.0",
+ "string-argv": "^0.3.2",
+ "yaml": "^2.8.1"
+ },
+ "bin": {
+ "lint-staged": "bin/lint-staged.js"
+ },
+ "engines": {
+ "node": ">=20.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/lint-staged"
+ }
+ },
+ "node_modules/lint-staged/node_modules/chalk": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz",
+ "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/lint-staged/node_modules/commander": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz",
+ "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/listr2": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.1.tgz",
+ "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cli-truncate": "^4.0.0",
+ "colorette": "^2.0.20",
+ "eventemitter3": "^5.0.1",
+ "log-update": "^6.1.0",
+ "rfdc": "^1.4.1",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/listr2/node_modules/ansi-regex": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
+ "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/listr2/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/listr2/node_modules/emoji-regex": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
+ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/listr2/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/listr2/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/listr2/node_modules/wrap-ansi": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
+ "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -7846,6 +8192,160 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/log-update": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
+ "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-escapes": "^7.0.0",
+ "cli-cursor": "^5.0.0",
+ "slice-ansi": "^7.1.0",
+ "strip-ansi": "^7.1.0",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/ansi-escapes": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz",
+ "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "environment": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/ansi-regex": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
+ "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/emoji-regex": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
+ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/log-update/node_modules/is-fullwidth-code-point": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz",
+ "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-east-asian-width": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/slice-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz",
+ "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "is-fullwidth-code-point": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/wrap-ansi": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
+ "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -8023,6 +8523,19 @@
"node": ">=6"
}
},
+ "node_modules/mimic-function": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
+ "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@@ -8071,6 +8584,19 @@
"thenify-all": "^1.0.0"
}
},
+ "node_modules/nano-spawn": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz",
+ "integrity": "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.17"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -8226,15 +8752,15 @@
}
},
"node_modules/open": {
- "version": "10.1.0",
- "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz",
- "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==",
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
+ "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
"license": "MIT",
"dependencies": {
"default-browser": "^5.2.1",
"define-lazy-prop": "^3.0.0",
"is-inside-container": "^1.0.0",
- "is-wsl": "^3.1.0"
+ "wsl-utils": "^0.1.0"
},
"engines": {
"node": ">=18"
@@ -8449,6 +8975,19 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pidtree": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
+ "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "pidtree": "bin/pidtree.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
@@ -8546,13 +9085,13 @@
}
},
"node_modules/playwright": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz",
- "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==",
+ "version": "1.54.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz",
+ "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.52.0"
+ "playwright-core": "1.54.1"
},
"bin": {
"playwright": "cli.js"
@@ -8565,9 +9104,9 @@
}
},
"node_modules/playwright-core": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz",
- "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==",
+ "version": "1.54.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz",
+ "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -8593,9 +9132,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.3",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
- "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"funding": [
{
"type": "opencollective",
@@ -8612,7 +9151,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.8",
+ "nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -8746,9 +9285,9 @@
}
},
"node_modules/prettier": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
- "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
+ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"bin": {
@@ -9178,6 +9717,52 @@
"node": ">=10"
}
},
+ "node_modules/restore-cursor": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
+ "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^7.0.0",
+ "signal-exit": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/onetime": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
+ "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-function": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
@@ -9188,6 +9773,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/rfdc": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/rimraf": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz",
@@ -9557,9 +10149,9 @@
}
},
"node_modules/shell-quote": {
- "version": "1.8.2",
- "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz",
- "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==",
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
+ "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -9664,6 +10256,49 @@
"node": ">=8"
}
},
+ "node_modules/slice-ansi": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
+ "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.0.0",
+ "is-fullwidth-code-point": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/slice-ansi/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
+ "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -9743,6 +10378,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/string-argv": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
+ "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6.19"
+ }
+ },
"node_modules/string-length": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
@@ -10214,21 +10859,20 @@
"license": "Apache-2.0"
},
"node_modules/ts-jest": {
- "version": "29.3.2",
- "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.2.tgz",
- "integrity": "sha512-bJJkrWc6PjFVz5g2DGCNUo8z7oFEYaz1xP1NpeDU7KNLMWPpEyV8Chbpkn8xjzgRDpQhnGMyvyldoL7h8JXyug==",
+ "version": "29.4.0",
+ "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.0.tgz",
+ "integrity": "sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"bs-logger": "^0.2.6",
"ejs": "^3.1.10",
"fast-json-stable-stringify": "^2.1.0",
- "jest-util": "^29.0.0",
"json5": "^2.2.3",
"lodash.memoize": "^4.1.2",
"make-error": "^1.3.6",
- "semver": "^7.7.1",
- "type-fest": "^4.39.1",
+ "semver": "^7.7.2",
+ "type-fest": "^4.41.0",
"yargs-parser": "^21.1.1"
},
"bin": {
@@ -10239,10 +10883,11 @@
},
"peerDependencies": {
"@babel/core": ">=7.0.0-beta.0 <8",
- "@jest/transform": "^29.0.0",
- "@jest/types": "^29.0.0",
- "babel-jest": "^29.0.0",
- "jest": "^29.0.0",
+ "@jest/transform": "^29.0.0 || ^30.0.0",
+ "@jest/types": "^29.0.0 || ^30.0.0",
+ "babel-jest": "^29.0.0 || ^30.0.0",
+ "jest": "^29.0.0 || ^30.0.0",
+ "jest-util": "^29.0.0 || ^30.0.0",
"typescript": ">=4.3 <6"
},
"peerDependenciesMeta": {
@@ -10260,13 +10905,16 @@
},
"esbuild": {
"optional": true
+ },
+ "jest-util": {
+ "optional": true
}
}
},
"node_modules/ts-jest/node_modules/semver": {
- "version": "7.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
- "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -10277,9 +10925,9 @@
}
},
"node_modules/ts-jest/node_modules/type-fest": {
- "version": "4.40.1",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.40.1.tgz",
- "integrity": "sha512-9YvLNnORDpI+vghLU/Nf+zSv0kL47KbVJ1o3sKgoTefl6i+zebxbiDQWoe/oWWqPhIgQdRZRT1KA9sCPL810SA==",
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
@@ -10428,15 +11076,16 @@
}
},
"node_modules/typescript-eslint": {
- "version": "8.31.1",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.31.1.tgz",
- "integrity": "sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA==",
+ "version": "8.38.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.38.0.tgz",
+ "integrity": "sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/eslint-plugin": "8.31.1",
- "@typescript-eslint/parser": "8.31.1",
- "@typescript-eslint/utils": "8.31.1"
+ "@typescript-eslint/eslint-plugin": "8.38.0",
+ "@typescript-eslint/parser": "8.38.0",
+ "@typescript-eslint/typescript-estree": "8.38.0",
+ "@typescript-eslint/utils": "8.38.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -10606,9 +11255,9 @@
}
},
"node_modules/vite": {
- "version": "6.3.4",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz",
- "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
+ "version": "6.3.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
+ "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10879,6 +11528,21 @@
}
}
},
+ "node_modules/wsl-utils": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
+ "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-wsl": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/xml-name-validator": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
@@ -10913,15 +11577,15 @@
"license": "ISC"
},
"node_modules/yaml": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz",
- "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==",
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
+ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
- "node": ">= 14"
+ "node": ">= 14.6"
}
},
"node_modules/yargs": {
@@ -10974,9 +11638,9 @@
}
},
"node_modules/zod": {
- "version": "3.24.3",
- "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz",
- "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==",
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
@@ -10993,21 +11657,21 @@
},
"server": {
"name": "@modelcontextprotocol/inspector-server",
- "version": "0.16.2",
+ "version": "0.16.5",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/sdk": "^1.17.0",
+ "@modelcontextprotocol/sdk": "^1.17.3",
"cors": "^2.8.5",
"express": "^5.1.0",
"ws": "^8.18.0",
- "zod": "^3.23.8"
+ "zod": "^3.25.76"
},
"bin": {
"mcp-inspector-server": "build/index.js"
},
"devDependencies": {
- "@types/cors": "^2.8.17",
- "@types/express": "^4.17.21",
+ "@types/cors": "^2.8.19",
+ "@types/express": "^4.17.23",
"@types/ws": "^8.5.12",
"tsx": "^4.19.0",
"typescript": "^5.6.2"
diff --git a/package.json b/package.json
index df2861c8..879c7b2c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/inspector",
- "version": "0.16.2",
+ "version": "0.16.5",
"description": "Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -41,34 +41,41 @@
"prettier-fix": "prettier --write .",
"prettier-check": "prettier --check .",
"lint": "prettier --check . && cd client && npm run lint",
- "prepare": "npm run build",
+ "prepare": "husky && npm run build",
"publish-all": "npm publish --workspaces --access public && npm publish --access public",
"update-version": "node scripts/update-version.js",
"check-version": "node scripts/check-version-consistency.js"
},
"dependencies": {
- "@modelcontextprotocol/inspector-cli": "^0.16.2",
- "@modelcontextprotocol/inspector-client": "^0.16.2",
- "@modelcontextprotocol/inspector-server": "^0.16.2",
- "@modelcontextprotocol/sdk": "^1.17.0",
- "concurrently": "^9.0.1",
- "open": "^10.1.0",
- "shell-quote": "^1.8.2",
+ "@modelcontextprotocol/inspector-cli": "^0.16.5",
+ "@modelcontextprotocol/inspector-client": "^0.16.5",
+ "@modelcontextprotocol/inspector-server": "^0.16.5",
+ "@modelcontextprotocol/sdk": "^1.17.3",
+ "concurrently": "^9.2.0",
+ "open": "^10.2.0",
+ "shell-quote": "^1.8.3",
"spawn-rx": "^5.1.2",
"ts-node": "^10.9.2",
- "zod": "^3.23.8"
+ "zod": "^3.25.76"
},
"devDependencies": {
- "@playwright/test": "^1.52.0",
+ "@playwright/test": "^1.54.1",
"@types/jest": "^29.5.14",
- "@types/node": "^22.7.5",
+ "@types/node": "^22.17.0",
"@types/shell-quote": "^1.7.5",
+ "husky": "^9.1.7",
"jest-fixed-jsdom": "^0.0.9",
- "prettier": "3.3.3",
+ "lint-staged": "^16.1.5",
+ "prettier": "^3.6.2",
"rimraf": "^6.0.1",
"typescript": "^5.4.2"
},
"engines": {
"node": ">=22.7.5"
+ },
+ "lint-staged": {
+ "**/*.{js,ts,jsx,tsx,json,md}": [
+ "prettier --write"
+ ]
}
}
diff --git a/server/package.json b/server/package.json
index 040fe71f..fb835f63 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/inspector-server",
- "version": "0.16.2",
+ "version": "0.16.5",
"description": "Server-side application for the Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -20,17 +20,17 @@
"dev:windows": "tsx watch --clear-screen=false src/index.ts < NUL"
},
"devDependencies": {
- "@types/cors": "^2.8.17",
- "@types/express": "^4.17.21",
+ "@types/cors": "^2.8.19",
+ "@types/express": "^4.17.23",
"@types/ws": "^8.5.12",
"tsx": "^4.19.0",
"typescript": "^5.6.2"
},
"dependencies": {
- "@modelcontextprotocol/sdk": "^1.17.0",
+ "@modelcontextprotocol/sdk": "^1.17.3",
"cors": "^2.8.5",
"express": "^5.1.0",
"ws": "^8.18.0",
- "zod": "^3.23.8"
+ "zod": "^3.25.76"
}
}
diff --git a/server/src/index.ts b/server/src/index.ts
index 92badc2c..c0fb3797 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -40,6 +40,8 @@ const { values } = parseArgs({
env: { type: "string", default: "" },
args: { type: "string", default: "" },
command: { type: "string", default: "" },
+ transport: { type: "string", default: "" },
+ "server-url": { type: "string", default: "" },
},
});
@@ -179,10 +181,10 @@ const createTransport = async (req: express.Request): Promise => {
const transportType = query.transportType as string;
if (transportType === "stdio") {
- const command = query.command as string;
+ const command = (query.command as string).trim();
const origArgs = shellParseArgs(query.args as string) as string[];
const queryEnv = query.env ? JSON.parse(query.env as string) : {};
- const env = { ...process.env, ...defaultEnvironment, ...queryEnv };
+ const env = { ...defaultEnvironment, ...process.env, ...queryEnv };
const { cmd, args } = findActualExecutable(command, origArgs);
@@ -376,7 +378,6 @@ app.get(
let serverTransport: Transport | undefined;
try {
serverTransport = await createTransport(req);
- console.log("Created server transport");
} catch (error) {
if (error instanceof SseError && error.code === 401) {
console.error(
@@ -389,34 +390,81 @@ app.get(
throw error;
}
- const webAppTransport = new SSEServerTransport("/message", res);
+ const proxyFullAddress = (req.query.proxyFullAddress as string) || "";
+ const prefix = proxyFullAddress || "";
+ const endpoint = `${prefix}/message`;
+
+ const webAppTransport = new SSEServerTransport(endpoint, res);
+ webAppTransports.set(webAppTransport.sessionId, webAppTransport);
console.log("Created client transport");
- webAppTransports.set(webAppTransport.sessionId, webAppTransport);
serverTransports.set(webAppTransport.sessionId, serverTransport);
+ console.log("Created server transport");
await webAppTransport.start();
(serverTransport as StdioClientTransport).stderr!.on("data", (chunk) => {
if (chunk.toString().includes("MODULE_NOT_FOUND")) {
+ // Server command not found, remove transports
+ const message = "Command not found, transports removed";
webAppTransport.send({
jsonrpc: "2.0",
- method: "notifications/stderr",
+ method: "notifications/message",
params: {
- content: "Command not found, transports removed",
+ level: "emergency",
+ logger: "proxy",
+ data: {
+ message,
+ },
},
});
webAppTransport.close();
serverTransport.close();
webAppTransports.delete(webAppTransport.sessionId);
serverTransports.delete(webAppTransport.sessionId);
- console.error("Command not found, transports removed");
+ console.error(message);
} else {
+ // Inspect message and attempt to assign a RFC 5424 Syslog Protocol level
+ let level;
+ let message = chunk.toString().trim();
+ let ucMsg = chunk.toString().toUpperCase();
+ if (ucMsg.includes("DEBUG")) {
+ level = "debug";
+ } else if (ucMsg.includes("INFO")) {
+ level = "info";
+ } else if (ucMsg.includes("NOTICE")) {
+ level = "notice";
+ } else if (ucMsg.includes("WARN")) {
+ level = "warning";
+ } else if (ucMsg.includes("ERROR")) {
+ level = "error";
+ } else if (ucMsg.includes("CRITICAL")) {
+ level = "critical";
+ } else if (ucMsg.includes("ALERT")) {
+ level = "alert";
+ } else if (ucMsg.includes("EMERGENCY")) {
+ level = "emergency";
+ } else if (ucMsg.includes("SIGINT")) {
+ message = "SIGINT received. Server shutdown.";
+ level = "emergency";
+ } else if (ucMsg.includes("SIGHUP")) {
+ message = "SIGHUP received. Server shutdown.";
+ level = "emergency";
+ } else if (ucMsg.includes("SIGTERM")) {
+ message = "SIGTERM received. Server shutdown.";
+ level = "emergency";
+ } else {
+ level = "info";
+ }
webAppTransport.send({
jsonrpc: "2.0",
- method: "notifications/stderr",
+ method: "notifications/message",
params: {
- content: chunk.toString(),
+ level,
+ logger: "stdio",
+ data: {
+ message,
+ },
},
});
}
@@ -440,7 +488,7 @@ app.get(
async (req, res) => {
try {
console.log(
- "New SSE connection request. NOTE: The sse transport is deprecated and has been replaced by StreamableHttp",
+ "New SSE connection request. NOTE: The SSE transport is deprecated and has been replaced by StreamableHttp",
);
let serverTransport: Transport | undefined;
try {
@@ -467,9 +515,14 @@ app.get(
}
if (serverTransport) {
- const webAppTransport = new SSEServerTransport("/message", res);
+ const proxyFullAddress = (req.query.proxyFullAddress as string) || "";
+ const prefix = proxyFullAddress || "";
+ const endpoint = `${prefix}/message`;
+
+ const webAppTransport = new SSEServerTransport(endpoint, res);
webAppTransports.set(webAppTransport.sessionId, webAppTransport);
console.log("Created client transport");
+
serverTransports.set(webAppTransport.sessionId, serverTransport!);
console.log("Created server transport");
@@ -523,6 +576,8 @@ app.get("/config", originValidationMiddleware, authMiddleware, (req, res) => {
defaultEnvironment,
defaultCommand: values.command,
defaultArgs: values.args,
+ defaultTransport: values.transport,
+ defaultServerUrl: values["server-url"],
});
} catch (error) {
console.error("Error in /config route:", error);