Merge branch 'main' into main
This commit is contained in:
commit
80ab4dcc28
6 changed files with 1022 additions and 118 deletions
|
|
@ -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 =
|
||||
|
|
@ -131,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
|
||||
|
|
@ -139,8 +144,12 @@ 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 },
|
||||
|
|
@ -209,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();
|
||||
|
||||
|
|
@ -235,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);
|
||||
|
|
@ -288,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 {
|
||||
|
|
@ -297,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
|
||||
|
|
@ -306,6 +347,7 @@ function parseArgs(): Args {
|
|||
envArgs: { ...((config as any).env || {}), ...(options.e || {}) },
|
||||
cli: options.cli || false,
|
||||
transport: "stdio",
|
||||
headers: options.header,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -327,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,22 +19,35 @@ 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 };
|
||||
|
||||
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(
|
||||
|
|
@ -81,11 +94,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 client = new Client({
|
||||
name: "inspector-cli",
|
||||
|
|
@ -150,7 +168,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);
|
||||
|
|
@ -162,8 +180,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("=");
|
||||
|
|
@ -174,7 +192,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 {
|
||||
|
|
@ -256,12 +307,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 --
|
||||
|
|
@ -276,6 +339,7 @@ function parseArgs(): Args {
|
|||
return {
|
||||
target: finalArgs,
|
||||
...options,
|
||||
headers: options.header, // commander.js uses 'header' field, map to 'headers'
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -287,8 +351,9 @@ async function main(): Promise<void> {
|
|||
try {
|
||||
const args = parseArgs();
|
||||
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) {
|
||||
handleError(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();
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue