Merge branch 'main' into accurate-cli-version
This commit is contained in:
commit
32ea112051
47 changed files with 3542 additions and 561 deletions
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ""
|
||||
labels: ""
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Inspector Version**
|
||||
|
||||
- [e.g. 0.16.5)
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
33
.github/workflows/claude.yml
vendored
33
.github/workflows/claude.yml
vendored
|
|
@ -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 <details> block.
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -9,9 +9,12 @@ 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
|
||||
.claude/settings.local.json
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ server/build
|
|||
CODE_OF_CONDUCT.md
|
||||
SECURITY.md
|
||||
mcp.json
|
||||
.claude/settings.local.json
|
||||
|
|
@ -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"]
|
||||
ENTRYPOINT ["npm", "start"]
|
||||
|
|
|
|||
40
README.md
40
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`:
|
||||
|
|
@ -90,6 +98,16 @@ The MCP Inspector provides convenient buttons to export server launch configurat
|
|||
}
|
||||
```
|
||||
|
||||
**Streamable HTTP transport example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "streamable-http",
|
||||
"url": "http://localhost:3000/mcp",
|
||||
"note": "For Streamable HTTP connections, add this URL directly in your MCP Client"
|
||||
}
|
||||
```
|
||||
|
||||
- **Servers File** - Copies a complete MCP configuration file structure to your clipboard, with your current server configuration added as `default-server`. This can be saved directly as `mcp.json`.
|
||||
|
||||
**STDIO transport example:**
|
||||
|
|
@ -123,9 +141,23 @@ The MCP Inspector provides convenient buttons to export server launch configurat
|
|||
}
|
||||
```
|
||||
|
||||
**Streamable HTTP transport example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"default-server": {
|
||||
"type": "streamable-http",
|
||||
"url": "http://localhost:3000/mcp",
|
||||
"note": "For Streamable HTTP connections, add this URL directly in your MCP Client"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
These buttons appear in the Inspector UI after you've configured your server settings, making it easy to save and reuse your configurations.
|
||||
|
||||
For SSE transport connections, the Inspector provides similar functionality for both buttons. The "Server Entry" button copies the SSE URL configuration that can be added to your existing configuration file, while the "Servers File" button creates a complete configuration file containing the SSE URL for direct use in clients.
|
||||
For SSE and Streamable HTTP transport connections, the Inspector provides similar functionality for both buttons. The "Server Entry" button copies the configuration that can be added to your existing configuration file, while the "Servers File" button creates a complete configuration file containing the URL for direct use in clients.
|
||||
|
||||
You can paste the Server Entry into your existing `mcp.json` file under your chosen server name, or use the complete Servers File payload to create a new configuration file.
|
||||
|
||||
|
|
@ -387,6 +419,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
|
||||
|
||||
|
|
@ -399,6 +434,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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector-cli",
|
||||
"version": "0.16.5",
|
||||
"version": "0.16.8",
|
||||
"description": "CLI for the Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
|
|
@ -17,11 +17,14 @@
|
|||
"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 && node scripts/cli-header-tests.js",
|
||||
"test:cli": "node scripts/cli-tests.js",
|
||||
"test:cli-tools": "node scripts/cli-tool-tests.js",
|
||||
"test:cli-headers": "node scripts/cli-header-tests.js"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.17.3",
|
||||
"@modelcontextprotocol/sdk": "^1.18.0",
|
||||
"commander": "^13.1.0",
|
||||
"spawn-rx": "^5.1.2"
|
||||
}
|
||||
|
|
|
|||
252
cli/scripts/cli-header-tests.js
Normal file
252
cli/scripts/cli-header-tests.js
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Integration tests for header functionality
|
||||
* Tests the CLI header parsing end-to-end
|
||||
*/
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const CLI_PATH = resolve(__dirname, "..", "build", "index.js");
|
||||
|
||||
// ANSI colors for output
|
||||
const colors = {
|
||||
GREEN: "\x1b[32m",
|
||||
RED: "\x1b[31m",
|
||||
YELLOW: "\x1b[33m",
|
||||
BLUE: "\x1b[34m",
|
||||
NC: "\x1b[0m", // No Color
|
||||
};
|
||||
|
||||
let testsPassed = 0;
|
||||
let testsFailed = 0;
|
||||
|
||||
/**
|
||||
* Run a CLI test with given arguments and check for expected behavior
|
||||
*/
|
||||
function runHeaderTest(
|
||||
testName,
|
||||
args,
|
||||
expectSuccess = false,
|
||||
expectedInOutput = null,
|
||||
) {
|
||||
return new Promise((resolve) => {
|
||||
console.log(`\n${colors.BLUE}Testing: ${testName}${colors.NC}`);
|
||||
console.log(
|
||||
`${colors.BLUE}Command: node ${CLI_PATH} ${args.join(" ")}${colors.NC}`,
|
||||
);
|
||||
|
||||
const child = spawn("node", [CLI_PATH, ...args], {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
const output = stdout + stderr;
|
||||
let passed = true;
|
||||
let reason = "";
|
||||
|
||||
// Check exit code expectation
|
||||
if (expectSuccess && code !== 0) {
|
||||
passed = false;
|
||||
reason = `Expected success (exit code 0) but got ${code}`;
|
||||
} else if (!expectSuccess && code === 0) {
|
||||
passed = false;
|
||||
reason = `Expected failure (non-zero exit code) but got success`;
|
||||
}
|
||||
|
||||
// Check expected output
|
||||
if (passed && expectedInOutput && !output.includes(expectedInOutput)) {
|
||||
passed = false;
|
||||
reason = `Expected output to contain "${expectedInOutput}"`;
|
||||
}
|
||||
|
||||
if (passed) {
|
||||
console.log(`${colors.GREEN}PASS: ${testName}${colors.NC}`);
|
||||
testsPassed++;
|
||||
} else {
|
||||
console.log(`${colors.RED}FAIL: ${testName}${colors.NC}`);
|
||||
console.log(`${colors.RED}Reason: ${reason}${colors.NC}`);
|
||||
console.log(`${colors.RED}Exit code: ${code}${colors.NC}`);
|
||||
console.log(`${colors.RED}Output: ${output}${colors.NC}`);
|
||||
testsFailed++;
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
console.log(
|
||||
`${colors.RED}ERROR: ${testName} - ${error.message}${colors.NC}`,
|
||||
);
|
||||
testsFailed++;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function runHeaderIntegrationTests() {
|
||||
console.log(
|
||||
`${colors.YELLOW}=== MCP Inspector CLI Header Integration Tests ===${colors.NC}`,
|
||||
);
|
||||
console.log(
|
||||
`${colors.BLUE}Testing header parsing and validation${colors.NC}`,
|
||||
);
|
||||
|
||||
// Test 1: Valid header format should parse successfully (connection will fail)
|
||||
await runHeaderTest(
|
||||
"Valid single header",
|
||||
[
|
||||
"https://example.com",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--header",
|
||||
"Authorization: Bearer token123",
|
||||
],
|
||||
false,
|
||||
);
|
||||
|
||||
// Test 2: Multiple headers should parse successfully
|
||||
await runHeaderTest(
|
||||
"Multiple headers",
|
||||
[
|
||||
"https://example.com",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--header",
|
||||
"Authorization: Bearer token123",
|
||||
"--header",
|
||||
"X-API-Key: secret123",
|
||||
],
|
||||
false,
|
||||
);
|
||||
|
||||
// Test 3: Invalid header format - no colon
|
||||
await runHeaderTest(
|
||||
"Invalid header format - no colon",
|
||||
[
|
||||
"https://example.com",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--header",
|
||||
"InvalidHeader",
|
||||
],
|
||||
false,
|
||||
"Invalid header format",
|
||||
);
|
||||
|
||||
// Test 4: Invalid header format - empty name
|
||||
await runHeaderTest(
|
||||
"Invalid header format - empty name",
|
||||
[
|
||||
"https://example.com",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--header",
|
||||
": value",
|
||||
],
|
||||
false,
|
||||
"Invalid header format",
|
||||
);
|
||||
|
||||
// Test 5: Invalid header format - empty value
|
||||
await runHeaderTest(
|
||||
"Invalid header format - empty value",
|
||||
[
|
||||
"https://example.com",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--header",
|
||||
"Header:",
|
||||
],
|
||||
false,
|
||||
"Invalid header format",
|
||||
);
|
||||
|
||||
// Test 6: Header with colons in value
|
||||
await runHeaderTest(
|
||||
"Header with colons in value",
|
||||
[
|
||||
"https://example.com",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--header",
|
||||
"X-Time: 2023:12:25:10:30:45",
|
||||
],
|
||||
false,
|
||||
);
|
||||
|
||||
// Test 7: Whitespace handling
|
||||
await runHeaderTest(
|
||||
"Whitespace handling in headers",
|
||||
[
|
||||
"https://example.com",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--header",
|
||||
" X-Header : value with spaces ",
|
||||
],
|
||||
false,
|
||||
);
|
||||
|
||||
console.log(`\n${colors.YELLOW}=== Test Results ===${colors.NC}`);
|
||||
console.log(`${colors.GREEN}Tests passed: ${testsPassed}${colors.NC}`);
|
||||
console.log(`${colors.RED}Tests failed: ${testsFailed}${colors.NC}`);
|
||||
|
||||
if (testsFailed === 0) {
|
||||
console.log(
|
||||
`${colors.GREEN}All header integration tests passed!${colors.NC}`,
|
||||
);
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log(
|
||||
`${colors.RED}Some header integration tests failed.${colors.NC}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on("SIGINT", () => {
|
||||
console.log(`\n${colors.YELLOW}Test interrupted by user${colors.NC}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
console.log(`\n${colors.YELLOW}Test terminated${colors.NC}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Run the tests
|
||||
runHeaderIntegrationTests().catch((error) => {
|
||||
console.error(`${colors.RED}Test runner error: ${error.message}${colors.NC}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -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}`,
|
||||
|
|
@ -533,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}`,
|
||||
);
|
||||
|
|
|
|||
614
cli/scripts/cli-tool-tests.js
Normal file
614
cli/scripts/cli-tool-tests.js
Normal file
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -16,6 +16,7 @@ type Args = {
|
|||
cli: boolean;
|
||||
transport?: "stdio" | "sse" | "streamable-http";
|
||||
serverUrl?: string;
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
type CliOptions = {
|
||||
|
|
@ -25,6 +26,7 @@ type CliOptions = {
|
|||
cli?: boolean;
|
||||
transport?: string;
|
||||
serverUrl?: string;
|
||||
header?: Record<string, string>;
|
||||
};
|
||||
|
||||
type ServerConfig =
|
||||
|
|
@ -104,6 +106,10 @@ async function runWebClient(args: Args): Promise<void> {
|
|||
await spawnPromise("node", [inspectorClientPath, ...startArgs], {
|
||||
signal: abort.signal,
|
||||
echoOutput: true,
|
||||
// pipe the stdout through here, prevents issues with buffering and
|
||||
// dropping the end of console.out after 8192 chars due to node
|
||||
// closing the stdout pipe before the output has finished flushing
|
||||
stdio: "inherit",
|
||||
});
|
||||
} catch (e) {
|
||||
if (!cancelled || process.env.DEBUG) throw e;
|
||||
|
|
@ -127,6 +133,9 @@ async function runCli(args: Args): Promise<void> {
|
|||
// 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
|
||||
|
|
@ -135,13 +144,21 @@ async function runCli(args: Args): Promise<void> {
|
|||
cliArgs.push("--transport", cliTransport);
|
||||
}
|
||||
|
||||
// Add command and remaining args
|
||||
cliArgs.push(args.command, ...args.args);
|
||||
// Add headers if specified
|
||||
if (args.headers) {
|
||||
for (const [key, value] of Object.entries(args.headers)) {
|
||||
cliArgs.push("--header", `${key}: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
await spawnPromise("node", cliArgs, {
|
||||
env: { ...process.env, ...args.envArgs },
|
||||
signal: abort.signal,
|
||||
echoOutput: true,
|
||||
// pipe the stdout through here, prevents issues with buffering and
|
||||
// dropping the end of console.out after 8192 chars due to node
|
||||
// closing the stdout pipe before the output has finished flushing
|
||||
stdio: "inherit",
|
||||
});
|
||||
} catch (e) {
|
||||
if (!cancelled || process.env.DEBUG) {
|
||||
|
|
@ -201,6 +218,30 @@ function parseKeyValuePair(
|
|||
return { ...previous, [key as string]: val };
|
||||
}
|
||||
|
||||
function parseHeaderPair(
|
||||
value: string,
|
||||
previous: Record<string, string> = {},
|
||||
): Record<string, string> {
|
||||
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();
|
||||
|
||||
|
|
@ -227,7 +268,13 @@ function parseArgs(): Args {
|
|||
.option("--server <n>", "server name from config file")
|
||||
.option("--cli", "enable CLI mode")
|
||||
.option("--transport <type>", "transport type (stdio, sse, http)")
|
||||
.option("--server-url <url>", "server URL for SSE/HTTP transport");
|
||||
.option("--server-url <url>", "server URL for SSE/HTTP transport")
|
||||
.option(
|
||||
"--header <headers...>",
|
||||
'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)',
|
||||
parseHeaderPair,
|
||||
{},
|
||||
);
|
||||
|
||||
// Parse only the arguments before --
|
||||
program.parse(preArgs);
|
||||
|
|
@ -280,6 +327,7 @@ function parseArgs(): Args {
|
|||
envArgs: { ...(config.env || {}), ...(options.e || {}) },
|
||||
cli: options.cli || false,
|
||||
transport: "stdio",
|
||||
headers: options.header,
|
||||
};
|
||||
} else if (config.type === "sse" || config.type === "streamable-http") {
|
||||
return {
|
||||
|
|
@ -289,6 +337,7 @@ function parseArgs(): Args {
|
|||
cli: options.cli || false,
|
||||
transport: config.type,
|
||||
serverUrl: config.url,
|
||||
headers: options.header,
|
||||
};
|
||||
} else {
|
||||
// Backwards compatibility: if no type field, assume stdio
|
||||
|
|
@ -298,6 +347,7 @@ function parseArgs(): Args {
|
|||
envArgs: { ...((config as any).env || {}), ...(options.e || {}) },
|
||||
cli: options.cli || false,
|
||||
transport: "stdio",
|
||||
headers: options.header,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -319,6 +369,7 @@ function parseArgs(): Args {
|
|||
cli: options.cli || false,
|
||||
transport: transport as "stdio" | "sse" | "streamable-http" | undefined,
|
||||
serverUrl: options.serverUrl,
|
||||
headers: options.header,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<McpResponse> {
|
||||
try {
|
||||
|
|
@ -17,12 +27,26 @@ export async function listPrompts(client: Client): Promise<McpResponse> {
|
|||
export async function getPrompt(
|
||||
client: Client,
|
||||
name: string,
|
||||
args?: Record<string, string>,
|
||||
args?: Record<string, JsonValue>,
|
||||
): Promise<McpResponse> {
|
||||
try {
|
||||
// Convert all arguments to strings for prompt arguments
|
||||
const stringArgs: Record<string, string> = {};
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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<McpResponse> {
|
|||
}
|
||||
}
|
||||
|
||||
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<string, string>,
|
||||
): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
): Record<string, JsonValue> {
|
||||
const result: Record<string, JsonValue> = {};
|
||||
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<string, string>,
|
||||
args: Record<string, JsonValue>,
|
||||
): Promise<McpResponse> {
|
||||
try {
|
||||
const toolsResponse = await listTools(client);
|
||||
const tools = toolsResponse.tools as Tool[];
|
||||
const tool = tools.find((t) => t.name === name);
|
||||
|
||||
let convertedArgs: Record<string, unknown> = args;
|
||||
let convertedArgs: Record<string, JsonValue> = 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<string, string> = {};
|
||||
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({
|
||||
|
|
|
|||
|
|
@ -19,6 +19,17 @@ 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 };
|
||||
|
||||
import packageJson from "../package.json" with { type: "json" };
|
||||
|
||||
|
|
@ -26,17 +37,19 @@ type Args = {
|
|||
target: string[];
|
||||
method?: string;
|
||||
promptName?: string;
|
||||
promptArgs?: Record<string, string>;
|
||||
promptArgs?: Record<string, JsonValue>;
|
||||
uri?: string;
|
||||
logLevel?: LogLevel;
|
||||
toolName?: string;
|
||||
toolArg?: Record<string, string>;
|
||||
toolArg?: Record<string, JsonValue>;
|
||||
transport?: "sse" | "stdio" | "http";
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
function createTransportOptions(
|
||||
target: string[],
|
||||
transport?: "sse" | "stdio" | "http",
|
||||
headers?: Record<string, string>,
|
||||
): TransportOptions {
|
||||
if (target.length === 0) {
|
||||
throw new Error(
|
||||
|
|
@ -83,11 +96,16 @@ function createTransportOptions(
|
|||
command: isUrl ? undefined : command,
|
||||
args: isUrl ? undefined : commandArgs,
|
||||
url: isUrl ? command : undefined,
|
||||
headers,
|
||||
};
|
||||
}
|
||||
|
||||
async function callMethod(args: Args): Promise<void> {
|
||||
const transportOptions = createTransportOptions(args.target, args.transport);
|
||||
const transportOptions = createTransportOptions(
|
||||
args.target,
|
||||
args.transport,
|
||||
args.headers,
|
||||
);
|
||||
const transport = createTransport(transportOptions);
|
||||
|
||||
const [, name = packageJson.name] = packageJson.name.split("/");
|
||||
|
|
@ -154,7 +172,7 @@ async function callMethod(args: Args): Promise<void> {
|
|||
);
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
await awaitableLog(JSON.stringify(result, null, 2));
|
||||
} finally {
|
||||
try {
|
||||
await disconnect(transport);
|
||||
|
|
@ -166,8 +184,8 @@ async function callMethod(args: Args): Promise<void> {
|
|||
|
||||
function parseKeyValuePair(
|
||||
value: string,
|
||||
previous: Record<string, string> = {},
|
||||
): Record<string, string> {
|
||||
previous: Record<string, JsonValue> = {},
|
||||
): Record<string, JsonValue> {
|
||||
const parts = value.split("=");
|
||||
const key = parts[0];
|
||||
const val = parts.slice(1).join("=");
|
||||
|
|
@ -178,7 +196,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<string, string> = {},
|
||||
): Record<string, string> {
|
||||
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 {
|
||||
|
|
@ -260,12 +311,24 @@ function parseArgs(): Args {
|
|||
}
|
||||
return value as "sse" | "http" | "stdio";
|
||||
},
|
||||
)
|
||||
//
|
||||
// HTTP headers
|
||||
//
|
||||
.option(
|
||||
"--header <headers...>",
|
||||
'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<Args, "target">;
|
||||
const options = program.opts() as Omit<Args, "target"> & {
|
||||
header?: Record<string, string>;
|
||||
};
|
||||
|
||||
let remainingArgs = program.args;
|
||||
|
||||
// Add back any arguments that came after --
|
||||
|
|
@ -280,6 +343,7 @@ function parseArgs(): Args {
|
|||
return {
|
||||
target: finalArgs,
|
||||
...options,
|
||||
headers: options.header, // commander.js uses 'header' field, map to 'headers'
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -291,6 +355,7 @@ async function main(): Promise<void> {
|
|||
try {
|
||||
const args = parseArgs();
|
||||
await callMethod(args);
|
||||
|
||||
// Explicitly exit to ensure process terminates in CI
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export type TransportOptions = {
|
|||
command?: string;
|
||||
args?: string[];
|
||||
url?: string;
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
function createStdioTransport(options: TransportOptions): Transport {
|
||||
|
|
@ -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}`);
|
||||
|
|
|
|||
7
cli/src/utils/awaitable-log.ts
Normal file
7
cli/src/utils/awaitable-log.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export function awaitableLog(logValue: string): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
process.stdout.write(logValue, () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector-client",
|
||||
"version": "0.16.5",
|
||||
"version": "0.16.8",
|
||||
"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.3",
|
||||
"@modelcontextprotocol/sdk": "^1.18.0",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-dialog": "^1.1.3",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
|
|
@ -33,6 +33,7 @@
|
|||
"@radix-ui/react-popover": "^1.1.3",
|
||||
"@radix-ui/react-select": "^2.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.1",
|
||||
"@radix-ui/react-toast": "^1.2.6",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
|
|
@ -48,7 +49,6 @@
|
|||
"react-simple-code-editor": "^0.14.1",
|
||||
"serve-handler": "^6.1.6",
|
||||
"tailwind-merge": "^2.5.3",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -70,8 +70,10 @@
|
|||
"globals": "^15.9.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-fixed-jsdom": "^0.0.9",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"ts-jest": "^29.4.0",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.38.0",
|
||||
|
|
|
|||
|
|
@ -74,6 +74,10 @@ import ElicitationTab, {
|
|||
PendingElicitationRequest,
|
||||
ElicitationResponse,
|
||||
} from "./components/ElicitationTab";
|
||||
import {
|
||||
CustomHeaders,
|
||||
migrateFromLegacyAuth,
|
||||
} from "./lib/types/customHeaders";
|
||||
|
||||
const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1";
|
||||
|
||||
|
|
@ -103,6 +107,14 @@ const App = () => {
|
|||
const [transportType, setTransportType] = useState<
|
||||
"stdio" | "sse" | "streamable-http"
|
||||
>(getInitialTransportType);
|
||||
const [connectionType, setConnectionType] = useState<"direct" | "proxy">(
|
||||
() => {
|
||||
return (
|
||||
(localStorage.getItem("lastConnectionType") as "direct" | "proxy") ||
|
||||
"proxy"
|
||||
);
|
||||
},
|
||||
);
|
||||
const [logLevel, setLogLevel] = useState<LoggingLevel>("debug");
|
||||
const [notifications, setNotifications] = useState<ServerNotification[]>([]);
|
||||
const [roots, setRoots] = useState<Root[]>([]);
|
||||
|
|
@ -127,6 +139,39 @@ const App = () => {
|
|||
return localStorage.getItem("lastOauthScope") || "";
|
||||
});
|
||||
|
||||
// Custom headers state with migration from legacy auth
|
||||
const [customHeaders, setCustomHeaders] = useState<CustomHeaders>(() => {
|
||||
const savedHeaders = localStorage.getItem("lastCustomHeaders");
|
||||
if (savedHeaders) {
|
||||
try {
|
||||
return JSON.parse(savedHeaders);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Failed to parse custom headers: "${savedHeaders}", will try legacy migration`,
|
||||
error,
|
||||
);
|
||||
// Fall back to migration if JSON parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate from legacy auth if available
|
||||
const legacyToken = localStorage.getItem("lastBearerToken") || "";
|
||||
const legacyHeaderName = localStorage.getItem("lastHeaderName") || "";
|
||||
|
||||
if (legacyToken) {
|
||||
return migrateFromLegacyAuth(legacyToken, legacyHeaderName);
|
||||
}
|
||||
|
||||
// Default to Authorization: Bearer as the most common case
|
||||
return [
|
||||
{
|
||||
name: "Authorization",
|
||||
value: "Bearer ",
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const [pendingSampleRequests, setPendingSampleRequests] = useState<
|
||||
Array<
|
||||
PendingRequest & {
|
||||
|
|
@ -200,6 +245,7 @@ const App = () => {
|
|||
serverCapabilities,
|
||||
mcpClient,
|
||||
requestHistory,
|
||||
clearRequestHistory,
|
||||
makeRequest,
|
||||
sendNotification,
|
||||
handleCompletion,
|
||||
|
|
@ -212,11 +258,11 @@ const App = () => {
|
|||
args,
|
||||
sseUrl,
|
||||
env,
|
||||
bearerToken,
|
||||
headerName,
|
||||
customHeaders,
|
||||
oauthClientId,
|
||||
oauthScope,
|
||||
config,
|
||||
connectionType,
|
||||
onNotification: (notification) => {
|
||||
setNotifications((prev) => [...prev, notification as ServerNotification]);
|
||||
},
|
||||
|
|
@ -302,13 +348,42 @@ const App = () => {
|
|||
}, [transportType]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("lastBearerToken", bearerToken);
|
||||
localStorage.setItem("lastConnectionType", connectionType);
|
||||
}, [connectionType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (bearerToken) {
|
||||
localStorage.setItem("lastBearerToken", bearerToken);
|
||||
} else {
|
||||
localStorage.removeItem("lastBearerToken");
|
||||
}
|
||||
}, [bearerToken]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("lastHeaderName", headerName);
|
||||
if (headerName) {
|
||||
localStorage.setItem("lastHeaderName", headerName);
|
||||
} else {
|
||||
localStorage.removeItem("lastHeaderName");
|
||||
}
|
||||
}, [headerName]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("lastCustomHeaders", JSON.stringify(customHeaders));
|
||||
}, [customHeaders]);
|
||||
|
||||
// Auto-migrate from legacy auth when custom headers are empty but legacy auth exists
|
||||
useEffect(() => {
|
||||
if (customHeaders.length === 0 && (bearerToken || headerName)) {
|
||||
const migratedHeaders = migrateFromLegacyAuth(bearerToken, headerName);
|
||||
if (migratedHeaders.length > 0) {
|
||||
setCustomHeaders(migratedHeaders);
|
||||
// Clear legacy auth after migration
|
||||
setBearerToken("");
|
||||
setHeaderName("");
|
||||
}
|
||||
}
|
||||
}, [bearerToken, headerName, customHeaders, setCustomHeaders]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("lastOauthClientId", oauthClientId);
|
||||
}, [oauthClientId]);
|
||||
|
|
@ -736,6 +811,10 @@ const App = () => {
|
|||
await sendNotification({ method: "notifications/roots/list_changed" });
|
||||
};
|
||||
|
||||
const handleClearNotifications = () => {
|
||||
setNotifications([]);
|
||||
};
|
||||
|
||||
const sendLogLevelRequest = async (level: LoggingLevel) => {
|
||||
await sendMCPRequest(
|
||||
{
|
||||
|
|
@ -805,10 +884,8 @@ const App = () => {
|
|||
setEnv={setEnv}
|
||||
config={config}
|
||||
setConfig={setConfig}
|
||||
bearerToken={bearerToken}
|
||||
setBearerToken={setBearerToken}
|
||||
headerName={headerName}
|
||||
setHeaderName={setHeaderName}
|
||||
customHeaders={customHeaders}
|
||||
setCustomHeaders={setCustomHeaders}
|
||||
oauthClientId={oauthClientId}
|
||||
setOauthClientId={setOauthClientId}
|
||||
oauthScope={oauthScope}
|
||||
|
|
@ -818,6 +895,8 @@ const App = () => {
|
|||
logLevel={logLevel}
|
||||
sendLogLevelRequest={sendLogLevelRequest}
|
||||
loggingSupported={!!serverCapabilities?.logging || false}
|
||||
connectionType={connectionType}
|
||||
setConnectionType={setConnectionType}
|
||||
/>
|
||||
<div
|
||||
onMouseDown={handleSidebarDragStart}
|
||||
|
|
@ -1102,6 +1181,8 @@ const App = () => {
|
|||
<HistoryAndNotifications
|
||||
requestHistory={requestHistory}
|
||||
serverNotifications={notifications}
|
||||
onClearHistory={clearRequestHistory}
|
||||
onClearNotifications={handleClearNotifications}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ jest.mock("../lib/hooks/useConnection", () => ({
|
|||
serverCapabilities: null,
|
||||
mcpClient: null,
|
||||
requestHistory: [],
|
||||
clearRequestHistory: jest.fn(),
|
||||
makeRequest: jest.fn(),
|
||||
sendNotification: jest.fn(),
|
||||
handleCompletion: jest.fn(),
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ const disconnectedConnectionState = {
|
|||
serverCapabilities: null,
|
||||
mcpClient: null,
|
||||
requestHistory: [],
|
||||
clearRequestHistory: jest.fn(),
|
||||
makeRequest: jest.fn(),
|
||||
sendNotification: jest.fn(),
|
||||
handleCompletion: jest.fn(),
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue