Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,12 @@ npx @modelcontextprotocol/inspector --cli node build/index.js --method resources
# List available prompts
npx @modelcontextprotocol/inspector --cli node build/index.js --method prompts/list

# Connect to a remote MCP server
# Connect to a remote MCP server (default is SSE transport)
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com

# Connect to a remote MCP server (with Streamable HTTP transport)
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --transport http

# Call a tool on a remote server
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --method tools/call --tool-name remotetool --tool-arg param=value

Expand Down
119 changes: 115 additions & 4 deletions cli/scripts/cli-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@ console.log(`${colors.BLUE}- Resource-related options (--uri)${colors.NC}`);
console.log(
`${colors.BLUE}- Prompt-related options (--prompt-name, --prompt-args)${colors.NC}`,
);
console.log(`${colors.BLUE}- Logging options (--log-level)${colors.NC}\n`);
console.log(`${colors.BLUE}- Logging options (--log-level)${colors.NC}`);
console.log(
`${colors.BLUE}- Transport types (--transport http/sse/stdio)${colors.NC}`,
);
console.log(
`${colors.BLUE}- Transport inference from URL suffixes (/mcp, /sse)${colors.NC}`,
);
console.log(`\n`);

// Get directory paths
const SCRIPTS_DIR = __dirname;
Expand All @@ -62,9 +69,11 @@ if (!fs.existsSync(OUTPUT_DIR)) {
}

// Create a temporary directory for test files
const TEMP_DIR = fs.mkdirSync(path.join(os.tmpdir(), "mcp-inspector-tests"), {
recursive: true,
});
const TEMP_DIR = path.join(os.tmpdir(), "mcp-inspector-tests");
fs.mkdirSync(TEMP_DIR, { recursive: true });

// Track servers for cleanup
let runningServers = [];

process.on("exit", () => {
try {
Expand All @@ -74,6 +83,21 @@ process.on("exit", () => {
`${colors.RED}Failed to remove temp directory: ${err.message}${colors.NC}`,
);
}

runningServers.forEach((server) => {
try {
process.kill(-server.pid);
} catch (e) {}
});
});

process.on("SIGINT", () => {
runningServers.forEach((server) => {
try {
process.kill(-server.pid);
} catch (e) {}
});
process.exit(1);
});

// Use the existing sample config file
Expand Down Expand Up @@ -121,6 +145,11 @@ async function runBasicTest(testName, ...args) {
stdio: ["ignore", "pipe", "pipe"],
});

const timeout = setTimeout(() => {
console.log(`${colors.YELLOW}Test timed out: ${testName}${colors.NC}`);
child.kill();
}, 10000);

// Pipe stdout and stderr to the output file
child.stdout.pipe(outputStream);
child.stderr.pipe(outputStream);
Expand All @@ -135,6 +164,7 @@ async function runBasicTest(testName, ...args) {
});

child.on("close", (code) => {
clearTimeout(timeout);
outputStream.end();

if (code === 0) {
Expand Down Expand Up @@ -201,6 +231,13 @@ async function runErrorTest(testName, ...args) {
stdio: ["ignore", "pipe", "pipe"],
});

const timeout = setTimeout(() => {
console.log(
`${colors.YELLOW}Error test timed out: ${testName}${colors.NC}`,
);
child.kill();
}, 10000);

// Pipe stdout and stderr to the output file
child.stdout.pipe(outputStream);
child.stderr.pipe(outputStream);
Expand All @@ -215,6 +252,7 @@ async function runErrorTest(testName, ...args) {
});

child.on("close", (code) => {
clearTimeout(timeout);
outputStream.end();

// For error tests, we expect a non-zero exit code
Expand Down Expand Up @@ -611,6 +649,79 @@ async function runTests() {
"debug",
);

console.log(
`\n${colors.YELLOW}=== Running HTTP Transport Tests ===${colors.NC}`,
);

console.log(
`${colors.BLUE}Starting server-everything in streamableHttp mode.${colors.NC}`,
);
const httpServer = spawn(
"npx",
["@modelcontextprotocol/server-everything", "streamableHttp"],
{
detached: true,
stdio: "ignore",
},
);
runningServers.push(httpServer);

await new Promise((resolve) => setTimeout(resolve, 3000));

// Test 25: HTTP transport inferred from URL ending with /mcp
await runBasicTest(
"http_transport_inferred",
"http://127.0.0.1:3001/mcp",
"--cli",
"--method",
"tools/list",
);

// Test 26: HTTP transport with explicit --transport http flag
await runBasicTest(
"http_transport_with_explicit_flag",
"http://127.0.0.1:3001",
"--transport",
"http",
"--cli",
"--method",
"tools/list",
);

// Test 27: HTTP transport with suffix and --transport http flag
await runBasicTest(
"http_transport_with_explicit_flag_and_suffix",
"http://127.0.0.1:3001/mcp",
"--transport",
"http",
"--cli",
"--method",
"tools/list",
);

// Test 28: SSE transport given to HTTP server (should fail)
await runErrorTest(
"sse_transport_given_to_http_server",
"http://127.0.0.1:3001",
"--transport",
"sse",
"--cli",
"--method",
"tools/list",
);

// Kill HTTP server
try {
process.kill(-httpServer.pid);
console.log(
`${colors.BLUE}HTTP server killed, waiting for port to be released...${colors.NC}`,
);
} catch (e) {
console.log(
`${colors.RED}Error killing HTTP server: ${e.message}${colors.NC}`,
);
}

// Print test summary
console.log(`\n${colors.YELLOW}=== Test Summary ===${colors.NC}`);
console.log(`${colors.GREEN}Passed: ${PASSED_TESTS}${colors.NC}`);
Expand Down
48 changes: 45 additions & 3 deletions cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ type Args = {
logLevel?: LogLevel;
toolName?: string;
toolArg?: Record<string, string>;
transport?: "sse" | "stdio" | "http";
};

function createTransportOptions(target: string[]): TransportOptions {
function createTransportOptions(
target: string[],
transport?: "sse" | "stdio" | "http",
): TransportOptions {
if (target.length === 0) {
throw new Error(
"Target is required. Specify a URL or a command to execute.",
Expand All @@ -50,16 +54,38 @@ function createTransportOptions(target: string[]): TransportOptions {
throw new Error("Arguments cannot be passed to a URL-based MCP server.");
}

let transportType: "sse" | "stdio" | "http";
if (transport) {
if (!isUrl && transport !== "stdio") {
throw new Error("Only stdio transport can be used with local commands.");
}
if (isUrl && transport === "stdio") {
throw new Error("stdio transport cannot be used with URLs.");
}
transportType = transport;
} else if (isUrl) {
const url = new URL(command);
if (url.pathname.endsWith("/mcp")) {
transportType = "http";
} else if (url.pathname.endsWith("/sse")) {
transportType = "sse";
} else {
transportType = "sse";
}
} else {
transportType = "stdio";
}

return {
transportType: isUrl ? "sse" : "stdio",
transportType,
command: isUrl ? undefined : command,
args: isUrl ? undefined : commandArgs,
url: isUrl ? command : undefined,
};
}

async function callMethod(args: Args): Promise<void> {
const transportOptions = createTransportOptions(args.target);
const transportOptions = createTransportOptions(args.target, args.transport);
const transport = createTransport(transportOptions);
const client = new Client({
name: "inspector-cli",
Expand Down Expand Up @@ -214,6 +240,22 @@ function parseArgs(): Args {

return value as LogLevel;
},
)
//
// Transport options
//
.option(
"--transport <type>",
"Transport type (sse, http, or stdio). Auto-detected from URL: /mcp → http, /sse → sse, commands → stdio",
(value: string) => {
const validTransports = ["sse", "http", "stdio"];
if (!validTransports.includes(value)) {
throw new Error(
`Invalid transport type: ${value}. Valid types are: ${validTransports.join(", ")}`,
);
}
return value as "sse" | "http" | "stdio";
},
);

// Parse only the arguments before --
Expand Down
20 changes: 18 additions & 2 deletions cli/src/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,35 @@ import {
getDefaultEnvironment,
StdioClientTransport,
} from "@modelcontextprotocol/sdk/client/stdio.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import { findActualExecutable } from "spawn-rx";

export type TransportOptions = {
transportType: "sse" | "stdio";
transportType: "sse" | "stdio" | "http";
command?: string;
args?: string[];
url?: string;
};

function createSSETransport(options: TransportOptions): Transport {
const baseUrl = new URL(options.url ?? "");
const sseUrl = new URL("/sse", baseUrl);
const sseUrl = baseUrl.pathname.endsWith("/sse")
? baseUrl
: new URL("/sse", baseUrl);

return new SSEClientTransport(sseUrl);
}

function createHTTPTransport(options: TransportOptions): Transport {
const baseUrl = new URL(options.url ?? "");
const mcpUrl = baseUrl.pathname.endsWith("/mcp")
? baseUrl
: new URL("/mcp", baseUrl);

return new StreamableHTTPClientTransport(mcpUrl);
}

function createStdioTransport(options: TransportOptions): Transport {
let args: string[] = [];

Expand Down Expand Up @@ -67,6 +79,10 @@ export function createTransport(options: TransportOptions): Transport {
return createSSETransport(options);
}

if (transportType === "http") {
return createHTTPTransport(options);
}

throw new Error(`Unsupported transport type: ${transportType}`);
} catch (error) {
throw new Error(
Expand Down