Skip to content

feat: Auto-detect transport type from config files #661

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
141 changes: 135 additions & 6 deletions cli/scripts/cli-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -649,6 +728,56 @@ 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 (should pass transport to client)
await runBasicTest(
"config_sse_type",
"--config",
sseConfigPath,
"--server",
"test-sse",
"echo",
"test",
);

// Test 27: Config with streamable-http transport type
await runBasicTest(
"config_http_type",
"--config",
httpConfigPath,
"--server",
"test-http",
"echo",
"test",
);

// 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 HTTP Transport Tests ===${colors.NC}`,
);
Expand All @@ -668,7 +797,7 @@ async function runTests() {

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

// Test 25: HTTP transport inferred from URL ending with /mcp
// Test 29: HTTP transport inferred from URL ending with /mcp
await runBasicTest(
"http_transport_inferred",
"http://127.0.0.1:3001/mcp",
Expand All @@ -677,7 +806,7 @@ async function runTests() {
"tools/list",
);

// Test 26: HTTP transport with explicit --transport http flag
// Test 30: HTTP transport with explicit --transport http flag
await runBasicTest(
"http_transport_with_explicit_flag",
"http://127.0.0.1:3001/mcp",
Expand All @@ -688,7 +817,7 @@ async function runTests() {
"tools/list",
);

// Test 27: HTTP transport with suffix and --transport http flag
// Test 31: HTTP transport with suffix and --transport http flag
await runBasicTest(
"http_transport_with_explicit_flag_and_suffix",
"http://127.0.0.1:3001/mcp",
Expand All @@ -699,7 +828,7 @@ async function runTests() {
"tools/list",
);

// Test 28: SSE transport given to HTTP server (should fail)
// Test 32: SSE transport given to HTTP server (should fail)
await runErrorTest(
"sse_transport_given_to_http_server",
"http://127.0.0.1:3001",
Expand All @@ -710,7 +839,7 @@ async function runTests() {
"tools/list",
);

// Test 29: HTTP transport without URL (should fail)
// Test 33: HTTP transport without URL (should fail)
await runErrorTest(
"http_transport_without_url",
"--transport",
Expand All @@ -720,7 +849,7 @@ async function runTests() {
"tools/list",
);

// Test 30: SSE transport without URL (should fail)
// Test 34: SSE transport without URL (should fail)
await runErrorTest(
"sse_transport_without_url",
"--transport",
Expand Down
62 changes: 51 additions & 11 deletions cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ type Args = {
args: string[];
envArgs: Record<string, string>;
cli: boolean;
transport?: "stdio" | "sse" | "streamable-http";
serverUrl?: string;
};

type CliOptions = {
Expand All @@ -23,11 +25,18 @@ type CliOptions = {
cli?: boolean;
};

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;
Expand Down Expand Up @@ -74,6 +83,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);
Expand Down Expand Up @@ -217,12 +236,33 @@ 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: "",
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
Expand Down
31 changes: 30 additions & 1 deletion client/bin/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@ function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms, true));
}

function getClientUrl(port, authDisabled, sessionToken, serverPort) {
function getClientUrl(
port,
authDisabled,
sessionToken,
serverPort,
transport,
serverUrl,
) {
const host = process.env.HOST || "localhost";
const baseUrl = `http://${host}:${port}`;

Expand All @@ -24,6 +31,12 @@ function getClientUrl(port, authDisabled, sessionToken, serverPort) {
if (!authDisabled) {
params.set("MCP_PROXY_AUTH_TOKEN", sessionToken);
}
if (transport) {
params.set("transport", transport);
}
if (serverUrl) {
params.set("serverUrl", serverUrl);
}
return params.size > 0 ? `${baseUrl}/?${params.toString()}` : baseUrl;
}

Expand Down Expand Up @@ -123,6 +136,8 @@ async function startDevClient(clientOptions) {
sessionToken,
abort,
cancelled,
transport,
serverUrl,
} = clientOptions;
const clientCommand = "npx";
const host = process.env.HOST || "localhost";
Expand All @@ -140,6 +155,8 @@ async function startDevClient(clientOptions) {
authDisabled,
sessionToken,
SERVER_PORT,
transport,
serverUrl,
);

// Give vite time to start before opening or logging the URL
Expand Down Expand Up @@ -173,6 +190,8 @@ async function startProdClient(clientOptions) {
sessionToken,
abort,
cancelled,
transport,
serverUrl,
} = clientOptions;
const inspectorClientPath = resolve(
__dirname,
Expand All @@ -187,6 +206,8 @@ async function startProdClient(clientOptions) {
authDisabled,
sessionToken,
SERVER_PORT,
transport,
serverUrl,
);

await spawnPromise("node", [inspectorClientPath], {
Expand All @@ -208,6 +229,8 @@ async function main() {
let command = null;
let parsingFlags = true;
let isDev = false;
let transport = null;
let serverUrl = null;

for (let i = 0; i < args.length; i++) {
const arg = args[i];
Expand All @@ -233,6 +256,10 @@ async function main() {
} else {
envVars[envVar] = "";
}
} else if (parsingFlags && arg === "--transport" && i + 1 < args.length) {
transport = args[++i];
} else if (parsingFlags && arg === "--server-url" && i + 1 < args.length) {
serverUrl = args[++i];
} else if (!command && !isDev) {
command = arg;
} else if (!isDev) {
Expand Down Expand Up @@ -292,6 +319,8 @@ async function main() {
sessionToken,
abort,
cancelled,
transport,
serverUrl,
};

await (isDev
Expand Down