Merge branch 'main' into bump-version

This commit is contained in:
Ola Hungerford 2025-08-08 18:35:10 -07:00 • committed by GitHub
commit 62926b61ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 524 additions and 28 deletions

View file

@ -120,6 +120,85 @@ try {
const invalidConfigPath = path.join(TEMP_DIR, "invalid-config.json");
fs.writeFileSync(invalidConfigPath, '{\n "mcpServers": {\n "invalid": {');
// Create config files with different transport types for testing
const sseConfigPath = path.join(TEMP_DIR, "sse-config.json");
fs.writeFileSync(
sseConfigPath,
JSON.stringify(
{
mcpServers: {
"test-sse": {
type: "sse",
url: "http://localhost:3000/sse",
note: "Test SSE server",
},
},
},
null,
2,
),
);
const httpConfigPath = path.join(TEMP_DIR, "http-config.json");
fs.writeFileSync(
httpConfigPath,
JSON.stringify(
{
mcpServers: {
"test-http": {
type: "streamable-http",
url: "http://localhost:3000/mcp",
note: "Test HTTP server",
},
},
},
null,
2,
),
);
const stdioConfigPath = path.join(TEMP_DIR, "stdio-config.json");
fs.writeFileSync(
stdioConfigPath,
JSON.stringify(
{
mcpServers: {
"test-stdio": {
type: "stdio",
command: "npx",
args: ["@modelcontextprotocol/server-everything"],
env: {
TEST_ENV: "test-value",
},
},
},
},
null,
2,
),
);
// Config without type field (backward compatibility)
const legacyConfigPath = path.join(TEMP_DIR, "legacy-config.json");
fs.writeFileSync(
legacyConfigPath,
JSON.stringify(
{
mcpServers: {
"test-legacy": {
command: "npx",
args: ["@modelcontextprotocol/server-everything"],
env: {
LEGACY_ENV: "legacy-value",
},
},
},
},
null,
2,
),
);
// Function to run a basic test
async function runBasicTest(testName, ...args) {
const outputFile = path.join(
@ -649,6 +728,160 @@ async function runTests() {
"debug",
);
console.log(
`\n${colors.YELLOW}=== Running Config Transport Type Tests ===${colors.NC}`,
);
// Test 25: Config with stdio transport type
await runBasicTest(
"config_stdio_type",
"--config",
stdioConfigPath,
"--server",
"test-stdio",
"--cli",
"--method",
"tools/list",
);
// Test 26: Config with SSE transport type (CLI mode) - expects connection error
await runErrorTest(
"config_sse_type_cli",
"--config",
sseConfigPath,
"--server",
"test-sse",
"--cli",
"--method",
"tools/list",
);
// Test 27: Config with streamable-http transport type (CLI mode) - expects connection error
await runErrorTest(
"config_http_type_cli",
"--config",
httpConfigPath,
"--server",
"test-http",
"--cli",
"--method",
"tools/list",
);
// Test 28: Legacy config without type field (backward compatibility)
await runBasicTest(
"config_legacy_no_type",
"--config",
legacyConfigPath,
"--server",
"test-legacy",
"--cli",
"--method",
"tools/list",
);
console.log(
`\n${colors.YELLOW}=== Running Default Server Tests ===${colors.NC}`,
);
// Create config with single server for auto-selection
const singleServerConfigPath = path.join(
TEMP_DIR,
"single-server-config.json",
);
fs.writeFileSync(
singleServerConfigPath,
JSON.stringify(
{
mcpServers: {
"only-server": {
command: "npx",
args: ["@modelcontextprotocol/server-everything"],
},
},
},
null,
2,
),
);
// Create config with default-server
const defaultServerConfigPath = path.join(
TEMP_DIR,
"default-server-config.json",
);
fs.writeFileSync(
defaultServerConfigPath,
JSON.stringify(
{
mcpServers: {
"default-server": {
command: "npx",
args: ["@modelcontextprotocol/server-everything"],
},
"other-server": {
command: "node",
args: ["other.js"],
},
},
},
null,
2,
),
);
// Create config with multiple servers (no default)
const multiServerConfigPath = path.join(TEMP_DIR, "multi-server-config.json");
fs.writeFileSync(
multiServerConfigPath,
JSON.stringify(
{
mcpServers: {
server1: {
command: "npx",
args: ["@modelcontextprotocol/server-everything"],
},
server2: {
command: "node",
args: ["other.js"],
},
},
},
null,
2,
),
);
// Test 29: Config with single server auto-selection
await runBasicTest(
"single_server_auto_select",
"--config",
singleServerConfigPath,
"--cli",
"--method",
"tools/list",
);
// Test 30: Config with default-server should now require explicit selection (multiple servers)
await runErrorTest(
"default_server_requires_explicit_selection",
"--config",
defaultServerConfigPath,
"--cli",
"--method",
"tools/list",
);
// Test 31: Config with multiple servers and no default (should fail)
await runErrorTest(
"multi_server_no_default",
"--config",
multiServerConfigPath,
"--cli",
"--method",
"tools/list",
);
console.log(
`\n${colors.YELLOW}=== Running HTTP Transport Tests ===${colors.NC}`,
);
@ -668,7 +901,7 @@ async function runTests() {
await new Promise((resolve) => setTimeout(resolve, 3000));
// Test 25: HTTP transport inferred from URL ending with /mcp
// Test 32: HTTP transport inferred from URL ending with /mcp
await runBasicTest(
"http_transport_inferred",
"http://127.0.0.1:3001/mcp",
@ -677,7 +910,7 @@ async function runTests() {
"tools/list",
);
// Test 26: HTTP transport with explicit --transport http flag
// Test 33: HTTP transport with explicit --transport http flag
await runBasicTest(
"http_transport_with_explicit_flag",
"http://127.0.0.1:3001/mcp",
@ -688,7 +921,7 @@ async function runTests() {
"tools/list",
);
// Test 27: HTTP transport with suffix and --transport http flag
// Test 34: HTTP transport with suffix and --transport http flag
await runBasicTest(
"http_transport_with_explicit_flag_and_suffix",
"http://127.0.0.1:3001/mcp",
@ -699,7 +932,7 @@ async function runTests() {
"tools/list",
);
// Test 28: SSE transport given to HTTP server (should fail)
// Test 35: SSE transport given to HTTP server (should fail)
await runErrorTest(
"sse_transport_given_to_http_server",
"http://127.0.0.1:3001",
@ -710,7 +943,7 @@ async function runTests() {
"tools/list",
);
// Test 29: HTTP transport without URL (should fail)
// Test 36: HTTP transport without URL (should fail)
await runErrorTest(
"http_transport_without_url",
"--transport",
@ -720,7 +953,7 @@ async function runTests() {
"tools/list",
);
// Test 30: SSE transport without URL (should fail)
// Test 37: SSE transport without URL (should fail)
await runErrorTest(
"sse_transport_without_url",
"--transport",

View file

@ -14,6 +14,8 @@ type Args = {
args: string[];
envArgs: Record<string, string>;
cli: boolean;
transport?: "stdio" | "sse" | "streamable-http";
serverUrl?: string;
};
type CliOptions = {
@ -21,13 +23,22 @@ type CliOptions = {
config?: string;
server?: string;
cli?: boolean;
transport?: string;
serverUrl?: string;
};
type ServerConfig = {
command: string;
args?: string[];
env?: Record<string, string>;
};
type ServerConfig =
| {
type: "stdio";
command: string;
args?: string[];
env?: Record<string, string>;
}
| {
type: "sse" | "streamable-http";
url: string;
note?: string;
};
function handleError(error: unknown): never {
let message: string;
@ -74,6 +85,16 @@ async function runWebClient(args: Args): Promise<void> {
startArgs.push("-e", `${key}=${value}`);
}
// Pass transport type if specified
if (args.transport) {
startArgs.push("--transport", args.transport);
}
// Pass server URL if specified
if (args.serverUrl) {
startArgs.push("--server-url", args.serverUrl);
}
// Pass command and args (using -- to separate them)
if (args.command) {
startArgs.push("--", args.command, ...args.args);
@ -103,7 +124,21 @@ async function runCli(args: Args): Promise<void> {
});
try {
await spawnPromise("node", [cliPath, args.command, ...args.args], {
// Build CLI arguments
const cliArgs = [cliPath];
// Add transport flag if specified
if (args.transport && args.transport !== "stdio") {
// Convert streamable-http back to http for CLI mode
const cliTransport =
args.transport === "streamable-http" ? "http" : args.transport;
cliArgs.push("--transport", cliTransport);
}
// Add command and remaining args
cliArgs.push(args.command, ...args.args);
await spawnPromise("node", cliArgs, {
env: { ...process.env, ...args.envArgs },
signal: abort.signal,
echoOutput: true,
@ -190,7 +225,9 @@ function parseArgs(): Args {
)
.option("--config <path>", "config file path")
.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("--server-url <url>", "server URL for SSE/HTTP transport");
// Parse only the arguments before --
program.parse(preArgs);
@ -201,14 +238,33 @@ function parseArgs(): Args {
// Add back any arguments that came after --
const finalArgs = [...remainingArgs, ...postArgs];
// Validate that config and server are provided together
if (
(options.config && !options.server) ||
(!options.config && options.server)
) {
throw new Error(
"Both --config and --server must be provided together. If you specify one, you must specify the other.",
// Validate config and server options
if (!options.config && options.server) {
throw new Error("--server requires --config to be specified");
}
// If config is provided without server, try to auto-select
if (options.config && !options.server) {
const configContent = fs.readFileSync(
path.isAbsolute(options.config)
? options.config
: path.resolve(process.cwd(), options.config),
"utf8",
);
const parsedConfig = JSON.parse(configContent);
const servers = Object.keys(parsedConfig.mcpServers || {});
if (servers.length === 1) {
// Use the only server if there's just one
options.server = servers[0];
} else if (servers.length === 0) {
throw new Error("No servers found in config file");
} else {
// Multiple servers, require explicit selection
throw new Error(
`Multiple servers found in config file. Please specify one with --server.\nAvailable servers: ${servers.join(", ")}`,
);
}
}
// If config file is specified, load and use the options from the file. We must merge the args
@ -217,23 +273,52 @@ function parseArgs(): Args {
if (options.config && options.server) {
const config = loadConfigFile(options.config, options.server);
return {
command: config.command,
args: [...(config.args || []), ...finalArgs],
envArgs: { ...(config.env || {}), ...(options.e || {}) },
cli: options.cli || false,
};
if (config.type === "stdio") {
return {
command: config.command,
args: [...(config.args || []), ...finalArgs],
envArgs: { ...(config.env || {}), ...(options.e || {}) },
cli: options.cli || false,
transport: "stdio",
};
} else if (config.type === "sse" || config.type === "streamable-http") {
return {
command: config.url,
args: finalArgs,
envArgs: options.e || {},
cli: options.cli || false,
transport: config.type,
serverUrl: config.url,
};
} else {
// Backwards compatibility: if no type field, assume stdio
return {
command: (config as any).command || "",
args: [...((config as any).args || []), ...finalArgs],
envArgs: { ...((config as any).env || {}), ...(options.e || {}) },
cli: options.cli || false,
transport: "stdio",
};
}
}
// Otherwise use command line arguments
const command = finalArgs[0] || "";
const args = finalArgs.slice(1);
// Map "http" shorthand to "streamable-http"
let transport = options.transport;
if (transport === "http") {
transport = "streamable-http";
}
return {
command,
args,
envArgs: options.e || {},
cli: options.cli || false,
transport: transport as "stdio" | "sse" | "streamable-http" | undefined,
serverUrl: options.serverUrl,
};
}