diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..0db5467a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -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. diff --git a/.gitignore b/.gitignore index 176b0d4c..a7eee74a 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ client/results.json client/test-results/ client/e2e/test-results/ mcp.json +.claude/settings.local.json diff --git a/README.md b/README.md index fc563f91..29037550 100644 --- a/README.md +++ b/README.md @@ -98,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:** @@ -131,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. diff --git a/cli/package.json b/cli/package.json index e274bbdf..caf21a3e 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-cli", - "version": "0.16.5", + "version": "0.16.7", "description": "CLI for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -17,13 +17,14 @@ "scripts": { "build": "tsc", "postbuild": "node scripts/make-executable.js", - "test": "node scripts/cli-tests.js && node scripts/cli-tool-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-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" } diff --git a/cli/scripts/cli-header-tests.js b/cli/scripts/cli-header-tests.js new file mode 100644 index 00000000..0f1d22a9 --- /dev/null +++ b/cli/scripts/cli-header-tests.js @@ -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); +}); diff --git a/cli/src/cli.ts b/cli/src/cli.ts index 7188f7c8..f4187e02 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -106,6 +106,10 @@ async function runWebClient(args: Args): Promise { 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; @@ -151,6 +155,10 @@ async function runCli(args: Args): Promise { 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) { diff --git a/cli/src/index.ts b/cli/src/index.ts index 2e8811ac..8e219e45 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -352,8 +352,8 @@ async function main(): Promise { const args = parseArgs(); await callMethod(args); - // Let Node.js naturally exit instead of force-exiting - // process.exit(0) was causing stdout truncation + // Explicitly exit to ensure process terminates in CI + process.exit(0); } catch (error) { handleError(error); } diff --git a/client/package.json b/client/package.json index 5d634ee1..fd6fc698 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-client", - "version": "0.16.5", + "version": "0.16.7", "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", @@ -48,7 +48,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": { @@ -72,6 +71,7 @@ "jest-environment-jsdom": "^29.7.0", "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", diff --git a/client/src/App.tsx b/client/src/App.tsx index fecd9839..60ce90c5 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -200,6 +200,7 @@ const App = () => { serverCapabilities, mcpClient, requestHistory, + clearRequestHistory, makeRequest, sendNotification, handleCompletion, @@ -736,6 +737,10 @@ const App = () => { await sendNotification({ method: "notifications/roots/list_changed" }); }; + const handleClearNotifications = () => { + setNotifications([]); + }; + const sendLogLevelRequest = async (level: LoggingLevel) => { await sendMCPRequest( { @@ -1102,6 +1107,8 @@ const App = () => { diff --git a/client/src/__tests__/App.config.test.tsx b/client/src/__tests__/App.config.test.tsx index b2e03d36..7458c205 100644 --- a/client/src/__tests__/App.config.test.tsx +++ b/client/src/__tests__/App.config.test.tsx @@ -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(), diff --git a/client/src/__tests__/App.routing.test.tsx b/client/src/__tests__/App.routing.test.tsx index dbd463f9..cc80f77d 100644 --- a/client/src/__tests__/App.routing.test.tsx +++ b/client/src/__tests__/App.routing.test.tsx @@ -42,6 +42,7 @@ const disconnectedConnectionState = { serverCapabilities: null, mcpClient: null, requestHistory: [], + clearRequestHistory: jest.fn(), makeRequest: jest.fn(), sendNotification: jest.fn(), handleCompletion: jest.fn(), diff --git a/client/src/components/AuthDebugger.tsx b/client/src/components/AuthDebugger.tsx index e7acbd80..6252c116 100644 --- a/client/src/components/AuthDebugger.tsx +++ b/client/src/components/AuthDebugger.tsx @@ -187,7 +187,7 @@ const AuthDebugger = ({ JSON.stringify(currentState), ); // Open the authorization URL automatically - window.location.href = currentState.authorizationUrl; + window.location.href = currentState.authorizationUrl.toString(); break; } } diff --git a/client/src/components/HistoryAndNotifications.tsx b/client/src/components/HistoryAndNotifications.tsx index 34c035f5..1c606071 100644 --- a/client/src/components/HistoryAndNotifications.tsx +++ b/client/src/components/HistoryAndNotifications.tsx @@ -1,13 +1,18 @@ import { ServerNotification } from "@modelcontextprotocol/sdk/types.js"; import { useState } from "react"; import JsonView from "./JsonView"; +import { Button } from "@/components/ui/button"; const HistoryAndNotifications = ({ requestHistory, serverNotifications, + onClearHistory, + onClearNotifications, }: { requestHistory: Array<{ request: string; response?: string }>; serverNotifications: ServerNotification[]; + onClearHistory?: () => void; + onClearNotifications?: () => void; }) => { const [expandedRequests, setExpandedRequests] = useState<{ [key: number]: boolean; @@ -27,7 +32,17 @@ const HistoryAndNotifications = ({ return (
-

History

+
+

History

+ +
{requestHistory.length === 0 ? (

No history yet @@ -93,7 +108,17 @@ const HistoryAndNotifications = ({ )}

-

Server Notifications

+
+

Server Notifications

+ +
{serverNotifications.length === 0 ? (

No notifications yet diff --git a/client/src/components/OAuthFlowProgress.tsx b/client/src/components/OAuthFlowProgress.tsx index e50b7df4..5f44a4f5 100644 --- a/client/src/components/OAuthFlowProgress.tsx +++ b/client/src/components/OAuthFlowProgress.tsx @@ -240,7 +240,7 @@ export const OAuthFlowProgress = ({

Authorization URL:

- {authState.authorizationUrl} + {String(authState.authorizationUrl)}