Merge branch 'main' into tool-search
This commit is contained in:
commit
b59f9ce4bc
6 changed files with 117 additions and 9 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -15,4 +15,5 @@ sdk
|
||||||
client/playwright-report/
|
client/playwright-report/
|
||||||
client/results.json
|
client/results.json
|
||||||
client/test-results/
|
client/test-results/
|
||||||
|
client/e2e/test-results/
|
||||||
mcp.json
|
mcp.json
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,4 @@ server/build
|
||||||
CODE_OF_CONDUCT.md
|
CODE_OF_CONDUCT.md
|
||||||
SECURITY.md
|
SECURITY.md
|
||||||
mcp.json
|
mcp.json
|
||||||
|
.claude/settings.local.json
|
||||||
|
|
@ -410,6 +410,9 @@ npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com
|
||||||
# Connect to a remote MCP server (with Streamable HTTP transport)
|
# 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
|
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
|
# 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
|
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --method tools/call --tool-name remotetool --tool-arg param=value
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ type Args = {
|
||||||
cli: boolean;
|
cli: boolean;
|
||||||
transport?: "stdio" | "sse" | "streamable-http";
|
transport?: "stdio" | "sse" | "streamable-http";
|
||||||
serverUrl?: string;
|
serverUrl?: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CliOptions = {
|
type CliOptions = {
|
||||||
|
|
@ -25,6 +26,7 @@ type CliOptions = {
|
||||||
cli?: boolean;
|
cli?: boolean;
|
||||||
transport?: string;
|
transport?: string;
|
||||||
serverUrl?: string;
|
serverUrl?: string;
|
||||||
|
header?: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ServerConfig =
|
type ServerConfig =
|
||||||
|
|
@ -127,6 +129,9 @@ async function runCli(args: Args): Promise<void> {
|
||||||
// Build CLI arguments
|
// Build CLI arguments
|
||||||
const cliArgs = [cliPath];
|
const cliArgs = [cliPath];
|
||||||
|
|
||||||
|
// Add target URL/command first
|
||||||
|
cliArgs.push(args.command, ...args.args);
|
||||||
|
|
||||||
// Add transport flag if specified
|
// Add transport flag if specified
|
||||||
if (args.transport && args.transport !== "stdio") {
|
if (args.transport && args.transport !== "stdio") {
|
||||||
// Convert streamable-http back to http for CLI mode
|
// Convert streamable-http back to http for CLI mode
|
||||||
|
|
@ -135,8 +140,12 @@ async function runCli(args: Args): Promise<void> {
|
||||||
cliArgs.push("--transport", cliTransport);
|
cliArgs.push("--transport", cliTransport);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add command and remaining args
|
// Add headers if specified
|
||||||
cliArgs.push(args.command, ...args.args);
|
if (args.headers) {
|
||||||
|
for (const [key, value] of Object.entries(args.headers)) {
|
||||||
|
cliArgs.push("--header", `${key}: ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await spawnPromise("node", cliArgs, {
|
await spawnPromise("node", cliArgs, {
|
||||||
env: { ...process.env, ...args.envArgs },
|
env: { ...process.env, ...args.envArgs },
|
||||||
|
|
@ -201,6 +210,30 @@ function parseKeyValuePair(
|
||||||
return { ...previous, [key as string]: val };
|
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 {
|
function parseArgs(): Args {
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
||||||
|
|
@ -227,7 +260,13 @@ function parseArgs(): Args {
|
||||||
.option("--server <n>", "server name from config file")
|
.option("--server <n>", "server name from config file")
|
||||||
.option("--cli", "enable CLI mode")
|
.option("--cli", "enable CLI mode")
|
||||||
.option("--transport <type>", "transport type (stdio, sse, http)")
|
.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 --
|
// Parse only the arguments before --
|
||||||
program.parse(preArgs);
|
program.parse(preArgs);
|
||||||
|
|
@ -280,6 +319,7 @@ function parseArgs(): Args {
|
||||||
envArgs: { ...(config.env || {}), ...(options.e || {}) },
|
envArgs: { ...(config.env || {}), ...(options.e || {}) },
|
||||||
cli: options.cli || false,
|
cli: options.cli || false,
|
||||||
transport: "stdio",
|
transport: "stdio",
|
||||||
|
headers: options.header,
|
||||||
};
|
};
|
||||||
} else if (config.type === "sse" || config.type === "streamable-http") {
|
} else if (config.type === "sse" || config.type === "streamable-http") {
|
||||||
return {
|
return {
|
||||||
|
|
@ -289,6 +329,7 @@ function parseArgs(): Args {
|
||||||
cli: options.cli || false,
|
cli: options.cli || false,
|
||||||
transport: config.type,
|
transport: config.type,
|
||||||
serverUrl: config.url,
|
serverUrl: config.url,
|
||||||
|
headers: options.header,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Backwards compatibility: if no type field, assume stdio
|
// Backwards compatibility: if no type field, assume stdio
|
||||||
|
|
@ -298,6 +339,7 @@ function parseArgs(): Args {
|
||||||
envArgs: { ...((config as any).env || {}), ...(options.e || {}) },
|
envArgs: { ...((config as any).env || {}), ...(options.e || {}) },
|
||||||
cli: options.cli || false,
|
cli: options.cli || false,
|
||||||
transport: "stdio",
|
transport: "stdio",
|
||||||
|
headers: options.header,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -319,6 +361,7 @@ function parseArgs(): Args {
|
||||||
cli: options.cli || false,
|
cli: options.cli || false,
|
||||||
transport: transport as "stdio" | "sse" | "streamable-http" | undefined,
|
transport: transport as "stdio" | "sse" | "streamable-http" | undefined,
|
||||||
serverUrl: options.serverUrl,
|
serverUrl: options.serverUrl,
|
||||||
|
headers: options.header,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,13 @@ type Args = {
|
||||||
toolName?: string;
|
toolName?: string;
|
||||||
toolArg?: Record<string, JsonValue>;
|
toolArg?: Record<string, JsonValue>;
|
||||||
transport?: "sse" | "stdio" | "http";
|
transport?: "sse" | "stdio" | "http";
|
||||||
|
headers?: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function createTransportOptions(
|
function createTransportOptions(
|
||||||
target: string[],
|
target: string[],
|
||||||
transport?: "sse" | "stdio" | "http",
|
transport?: "sse" | "stdio" | "http",
|
||||||
|
headers?: Record<string, string>,
|
||||||
): TransportOptions {
|
): TransportOptions {
|
||||||
if (target.length === 0) {
|
if (target.length === 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -91,11 +93,16 @@ function createTransportOptions(
|
||||||
command: isUrl ? undefined : command,
|
command: isUrl ? undefined : command,
|
||||||
args: isUrl ? undefined : commandArgs,
|
args: isUrl ? undefined : commandArgs,
|
||||||
url: isUrl ? command : undefined,
|
url: isUrl ? command : undefined,
|
||||||
|
headers,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function callMethod(args: Args): Promise<void> {
|
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 transport = createTransport(transportOptions);
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
name: "inspector-cli",
|
name: "inspector-cli",
|
||||||
|
|
@ -196,6 +203,30 @@ function parseKeyValuePair(
|
||||||
return { ...previous, [key as string]: parsedValue };
|
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 {
|
function parseArgs(): Args {
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
||||||
|
|
@ -275,12 +306,24 @@ function parseArgs(): Args {
|
||||||
}
|
}
|
||||||
return value as "sse" | "http" | "stdio";
|
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 --
|
// Parse only the arguments before --
|
||||||
program.parse(preArgs);
|
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;
|
let remainingArgs = program.args;
|
||||||
|
|
||||||
// Add back any arguments that came after --
|
// Add back any arguments that came after --
|
||||||
|
|
@ -295,6 +338,7 @@ function parseArgs(): Args {
|
||||||
return {
|
return {
|
||||||
target: finalArgs,
|
target: finalArgs,
|
||||||
...options,
|
...options,
|
||||||
|
headers: options.header, // commander.js uses 'header' field, map to 'headers'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -306,8 +350,9 @@ async function main(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const args = parseArgs();
|
const args = parseArgs();
|
||||||
await callMethod(args);
|
await callMethod(args);
|
||||||
// Explicitly exit to ensure process terminates in CI
|
|
||||||
process.exit(0);
|
// Let Node.js naturally exit instead of force-exiting
|
||||||
|
// process.exit(0) was causing stdout truncation
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error);
|
handleError(error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export type TransportOptions = {
|
||||||
command?: string;
|
command?: string;
|
||||||
args?: string[];
|
args?: string[];
|
||||||
url?: string;
|
url?: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function createStdioTransport(options: TransportOptions): Transport {
|
function createStdioTransport(options: TransportOptions): Transport {
|
||||||
|
|
@ -64,11 +65,25 @@ export function createTransport(options: TransportOptions): Transport {
|
||||||
const url = new URL(options.url);
|
const url = new URL(options.url);
|
||||||
|
|
||||||
if (transportType === "sse") {
|
if (transportType === "sse") {
|
||||||
return new SSEClientTransport(url);
|
const transportOptions = options.headers
|
||||||
|
? {
|
||||||
|
requestInit: {
|
||||||
|
headers: options.headers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
return new SSEClientTransport(url, transportOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transportType === "http") {
|
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}`);
|
throw new Error(`Unsupported transport type: ${transportType}`);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue