#!/usr/bin/env node
import * as fs from "fs";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { Command } from "commander";
import { callTool, connect, disconnect, getPrompt, listPrompts, listResources, listResourceTemplates, listTools, readResource, setLoggingLevel, validLogLevels, } from "./client/index.js";
import { handleError } from "./error-handler.js";
import { createTransport } from "./transport.js";
import { awaitableLog } from "./utils/awaitable-log.js";
function createTransportOptions(target, transport, headers) {
if (target.length === 0) {
throw new Error("Target is required. Specify a URL or a command to execute.");
}
const [command, ...commandArgs] = target;
if (!command) {
throw new Error("Command is required.");
}
const isUrl = command.startsWith("http://") || command.startsWith("https://");
if (isUrl && commandArgs.length > 0) {
throw new Error("Arguments cannot be passed to a URL-based MCP server.");
}
let transportType;
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,
command: isUrl ? undefined : command,
args: isUrl ? undefined : commandArgs,
url: isUrl ? command : undefined,
headers,
};
}
async function callMethod(args) {
// Read package.json to get name and version for client identity
const pathA = "../package.json"; // We're in package @modelcontextprotocol/inspector-cli
const pathB = "../../package.json"; // We're in package @modelcontextprotocol/inspector
let packageJson;
let packageJsonData = await import(fs.existsSync(pathA) ? pathA : pathB, {
with: { type: "json" },
});
packageJson = packageJsonData.default;
const transportOptions = createTransportOptions(args.target, args.transport, args.headers);
const transport = createTransport(transportOptions);
const [, name = packageJson.name] = packageJson.name.split("/");
const version = packageJson.version;
const clientIdentity = { name, version };
const client = new Client(clientIdentity);
try {
await connect(client, transport);
let result;
// Tools methods
if (args.method === "tools/list") {
result = await listTools(client, args.metadata);
}
else if (args.method === "tools/call") {
if (!args.toolName) {
throw new Error("Tool name is required for tools/call method. Use --tool-name to specify the tool name.");
}
result = await callTool(client, args.toolName, args.toolArg || {}, args.metadata, args.toolMeta);
}
// Resources methods
else if (args.method === "resources/list") {
result = await listResources(client, args.metadata);
}
else if (args.method === "resources/read") {
if (!args.uri) {
throw new Error("URI is required for resources/read method. Use --uri to specify the resource URI.");
}
result = await readResource(client, args.uri, args.metadata);
}
else if (args.method === "resources/templates/list") {
result = await listResourceTemplates(client, args.metadata);
}
// Prompts methods
else if (args.method === "prompts/list") {
result = await listPrompts(client, args.metadata);
}
else if (args.method === "prompts/get") {
if (!args.promptName) {
throw new Error("Prompt name is required for prompts/get method. Use --prompt-name to specify the prompt name.");
}
result = await getPrompt(client, args.promptName, args.promptArgs || {}, args.metadata);
}
// Logging methods
else if (args.method === "logging/setLevel") {
if (!args.logLevel) {
throw new Error("Log level is required for logging/setLevel method. Use --log-level to specify the log level.");
}
result = await setLoggingLevel(client, args.logLevel);
}
else {
throw new Error(`Unsupported method: ${args.method}. Supported methods include: tools/list, tools/call, resources/list, resources/read, resources/templates/list, prompts/list, prompts/get, logging/setLevel`);
}
await awaitableLog(JSON.stringify(result, null, 2));
}
finally {
try {
await disconnect(transport);
}
catch (disconnectError) {
throw disconnectError;
}
}
}
function parseKeyValuePair(value, previous = {}) {
const parts = value.split("=");
const key = parts[0];
const val = parts.slice(1).join("=");
if (val === undefined || val === "") {
throw new Error(`Invalid parameter format: ${value}. Use key=value format.`);
}
// Try to parse as JSON first
let parsedValue;
try {
parsedValue = JSON.parse(val);
}
catch {
// If JSON parsing fails, keep as string
parsedValue = val;
}
return { ...previous, [key]: parsedValue };
}
function parseHeaderPair(value, previous = {}) {
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() {
const program = new Command();
// Find if there's a -- in the arguments and split them
const argSeparatorIndex = process.argv.indexOf("--");
let preArgs = process.argv;
let postArgs = [];
if (argSeparatorIndex !== -1) {
preArgs = process.argv.slice(0, argSeparatorIndex);
postArgs = process.argv.slice(argSeparatorIndex + 1);
}
program
.name("inspector-cli")
.allowUnknownOption()
.argument("<target...>", "Command and arguments or URL of the MCP server")
//
// Method selection
//
.option("--method <method>", "Method to invoke")
//
// Tool-related options
//
.option("--tool-name <toolName>", "Tool name (for tools/call method)")
.option("--tool-arg <pairs...>", "Tool argument as key=value pair", parseKeyValuePair, {})
//
// Resource-related options
//
.option("--uri <uri>", "URI of the resource (for resources/read method)")
//
// Prompt-related options
//
.option("--prompt-name <promptName>", "Name of the prompt (for prompts/get method)")
.option("--prompt-args <pairs...>", "Prompt arguments as key=value pairs", parseKeyValuePair, {})
//
// Logging options
//
.option("--log-level <level>", "Logging level (for logging/setLevel method)", (value) => {
if (!validLogLevels.includes(value)) {
throw new Error(`Invalid log level: ${value}. Valid levels are: ${validLogLevels.join(", ")}`);
}
return value;
})
//
// Transport options
//
.option("--transport <type>", "Transport type (sse, http, or stdio). Auto-detected from URL: /mcp → http, /sse → sse, commands → stdio", (value) => {
const validTransports = ["sse", "http", "stdio"];
if (!validTransports.includes(value)) {
throw new Error(`Invalid transport type: ${value}. Valid types are: ${validTransports.join(", ")}`);
}
return value;
})
//
// HTTP headers
//
.option("--header <headers...>", 'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)', parseHeaderPair, {})
//
// Metadata options
//
.option("--metadata <pairs...>", "General metadata as key=value pairs (applied to all methods)", parseKeyValuePair, {})
.option("--tool-metadata <pairs...>", "Tool-specific metadata as key=value pairs (for tools/call method only)", parseKeyValuePair, {});
// Parse only the arguments before --
program.parse(preArgs);
const options = program.opts();
let remainingArgs = program.args;
// Add back any arguments that came after --
const finalArgs = [...remainingArgs, ...postArgs];
if (!options.method) {
throw new Error("Method is required. Use --method to specify the method to invoke.");
}
return {
target: finalArgs,
...options,
headers: options.header, // commander.js uses 'header' field, map to 'headers'
metadata: options.metadata
? Object.fromEntries(Object.entries(options.metadata).map(([key, value]) => [
key,
String(value),
]))
: undefined,
toolMeta: options.toolMetadata
? Object.fromEntries(Object.entries(options.toolMetadata).map(([key, value]) => [
key,
String(value),
]))
: undefined,
};
}
async function main() {
process.on("uncaughtException", (error) => {
handleError(error);
});
try {
const args = parseArgs();
await callMethod(args);
// Explicitly exit to ensure process terminates in CI
process.exit(0);
}
catch (error) {
handleError(error);
}
}
main();