Skip to content

Commit e611526

Browse files
feat: Auto-detect transport type from config files
When launching the inspector with --config, automatically set the transport dropdown and server URL based on the config file contents. This eliminates the need to manually switch between stdio/sse/streamable-http in the UI. - Use discriminated union for ServerConfig to properly type different transports - Detect transport type and URL from config, pass via query params - Maintain backwards compatibility for configs without explicit 'type' field - Add comprehensive tests for the new functionality Improves UX by making the config file the single source of truth for server settings. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent fd0b962 commit e611526

File tree

4 files changed

+465
-13
lines changed

4 files changed

+465
-13
lines changed

cli/scripts/cli-tests.js

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,83 @@ try {
120120
const invalidConfigPath = path.join(TEMP_DIR, "invalid-config.json");
121121
fs.writeFileSync(invalidConfigPath, '{\n "mcpServers": {\n "invalid": {');
122122

123+
// Function to run a UI mode test (without --cli flag)
124+
async function runUITest(testName, ...args) {
125+
const outputFile = path.join(
126+
OUTPUT_DIR,
127+
`${testName.replace(/\//g, "_")}.log`,
128+
);
129+
130+
console.log(`\n${colors.YELLOW}Testing UI mode: ${testName}${colors.NC}`);
131+
TOTAL_TESTS++;
132+
133+
// Run the command and capture output
134+
console.log(
135+
`${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`,
136+
);
137+
138+
try {
139+
// Create a write stream for the output file
140+
const outputStream = fs.createWriteStream(outputFile);
141+
142+
// Spawn the process
143+
return new Promise((resolve) => {
144+
const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], {
145+
stdio: ["ignore", "pipe", "pipe"],
146+
env: { ...process.env, DANGEROUSLY_OMIT_AUTH: "true", MCP_AUTO_OPEN_ENABLED: "false" },
147+
});
148+
149+
const timeout = setTimeout(() => {
150+
console.log(`${colors.GREEN}✓ UI test started successfully: ${testName}${colors.NC}`);
151+
child.kill();
152+
PASSED_TESTS++;
153+
resolve(true);
154+
}, 3000); // Give it 3 seconds to start
155+
156+
// Pipe stdout and stderr to the output file
157+
child.stdout.pipe(outputStream);
158+
child.stderr.pipe(outputStream);
159+
160+
// Also capture output for display
161+
let output = "";
162+
child.stdout.on("data", (data) => {
163+
output += data.toString();
164+
// Check if the expected URL parameters are present
165+
if (testName.includes("ui_config")) {
166+
console.log(`${colors.BLUE}Output includes: ${data.toString().slice(0, 200)}${colors.NC}`);
167+
}
168+
});
169+
child.stderr.on("data", (data) => {
170+
output += data.toString();
171+
});
172+
173+
child.on("close", (code) => {
174+
clearTimeout(timeout);
175+
outputStream.end();
176+
177+
if (code !== 0) {
178+
console.log(`${colors.RED}✗ UI test failed: ${testName}${colors.NC}`);
179+
console.log(`${colors.RED}Error output:${colors.NC}`);
180+
console.log(
181+
output
182+
.split("\n")
183+
.map((line) => ` ${line}`)
184+
.join("\n"),
185+
);
186+
FAILED_TESTS++;
187+
resolve(false);
188+
}
189+
});
190+
});
191+
} catch (error) {
192+
console.error(
193+
`${colors.RED}Error running UI test: ${error.message}${colors.NC}`,
194+
);
195+
FAILED_TESTS++;
196+
return false;
197+
}
198+
}
199+
123200
// Function to run a basic test
124201
async function runBasicTest(testName, ...args) {
125202
const outputFile = path.join(
@@ -742,6 +819,151 @@ async function runTests() {
742819
);
743820
}
744821

822+
console.log(
823+
`\n${colors.YELLOW}=== Running Config Transport Type Tests ===${colors.NC}`,
824+
);
825+
826+
// Create test config files with different transport types
827+
const stdioConfigPath = path.join(TEMP_DIR, "stdio-config.json");
828+
const sseConfigPath = path.join(TEMP_DIR, "sse-config.json");
829+
const httpConfigPath = path.join(TEMP_DIR, "http-config.json");
830+
const legacyConfigPath = path.join(TEMP_DIR, "legacy-config.json");
831+
832+
// STDIO config with explicit type
833+
fs.writeFileSync(
834+
stdioConfigPath,
835+
JSON.stringify({
836+
mcpServers: {
837+
"stdio-server": {
838+
type: "stdio",
839+
command: "npx",
840+
args: ["@modelcontextprotocol/server-everything"],
841+
env: { TEST_MODE: "stdio" },
842+
},
843+
},
844+
}),
845+
);
846+
847+
// SSE config
848+
fs.writeFileSync(
849+
sseConfigPath,
850+
JSON.stringify({
851+
mcpServers: {
852+
"sse-server": {
853+
type: "sse",
854+
url: "http://localhost:3000/events",
855+
note: "Test SSE server",
856+
},
857+
},
858+
}),
859+
);
860+
861+
// Streamable HTTP config
862+
fs.writeFileSync(
863+
httpConfigPath,
864+
JSON.stringify({
865+
mcpServers: {
866+
"http-server": {
867+
type: "streamable-http",
868+
url: "http://localhost:3001/mcp",
869+
},
870+
},
871+
}),
872+
);
873+
874+
// Legacy config without type field (should default to stdio)
875+
fs.writeFileSync(
876+
legacyConfigPath,
877+
JSON.stringify({
878+
mcpServers: {
879+
"legacy-server": {
880+
command: "npx",
881+
args: ["@modelcontextprotocol/server-everything"],
882+
env: { TEST_MODE: "legacy" },
883+
},
884+
},
885+
}),
886+
);
887+
888+
// Test 31: Config with explicit stdio type
889+
await runBasicTest(
890+
"config_stdio_type",
891+
"--config",
892+
stdioConfigPath,
893+
"--server",
894+
"stdio-server",
895+
"--cli",
896+
"--method",
897+
"tools/list",
898+
);
899+
900+
// Test 32: Config with SSE type (should fail in CLI mode but config should parse)
901+
await runErrorTest(
902+
"config_sse_type",
903+
"--config",
904+
sseConfigPath,
905+
"--server",
906+
"sse-server",
907+
"--cli",
908+
"--method",
909+
"tools/list",
910+
);
911+
912+
// Test 33: Config with streamable-http type (should fail in CLI mode but config should parse)
913+
await runErrorTest(
914+
"config_http_type",
915+
"--config",
916+
httpConfigPath,
917+
"--server",
918+
"http-server",
919+
"--cli",
920+
"--method",
921+
"tools/list",
922+
);
923+
924+
// Test 34: Legacy config without type field
925+
await runBasicTest(
926+
"config_legacy_no_type",
927+
"--config",
928+
legacyConfigPath,
929+
"--server",
930+
"legacy-server",
931+
"--cli",
932+
"--method",
933+
"tools/list",
934+
);
935+
936+
console.log(
937+
`\n${colors.YELLOW}=== Running UI Mode Transport Tests ===${colors.NC}`,
938+
);
939+
940+
// Test 35: UI mode with SSE config should pass transport parameters
941+
await runUITest(
942+
"ui_config_sse_transport",
943+
"--config",
944+
sseConfigPath,
945+
"--server",
946+
"sse-server",
947+
);
948+
949+
// Test 36: UI mode with HTTP config should pass transport parameters
950+
await runUITest(
951+
"ui_config_http_transport",
952+
"--config",
953+
httpConfigPath,
954+
"--server",
955+
"http-server",
956+
);
957+
958+
// Test 37: UI mode with STDIO config should pass transport parameters
959+
await runUITest(
960+
"ui_config_stdio_transport",
961+
"--config",
962+
stdioConfigPath,
963+
"--server",
964+
"stdio-server",
965+
);
966+
745967
// Print test summary
746968
console.log(`\n${colors.YELLOW}=== Test Summary ===${colors.NC}`);
747969
console.log(`${colors.GREEN}Passed: ${PASSED_TESTS}${colors.NC}`);

cli/src/cli.ts

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ type Args = {
1414
args: string[];
1515
envArgs: Record<string, string>;
1616
cli: boolean;
17+
transport?: "stdio" | "sse" | "streamable-http";
18+
serverUrl?: string;
1719
};
1820

1921
type CliOptions = {
@@ -23,11 +25,18 @@ type CliOptions = {
2325
cli?: boolean;
2426
};
2527

26-
type ServerConfig = {
27-
command: string;
28-
args?: string[];
29-
env?: Record<string, string>;
30-
};
28+
type ServerConfig =
29+
| {
30+
type: "stdio";
31+
command: string;
32+
args?: string[];
33+
env?: Record<string, string>;
34+
}
35+
| {
36+
type: "sse" | "streamable-http";
37+
url: string;
38+
note?: string;
39+
};
3140

3241
function handleError(error: unknown): never {
3342
let message: string;
@@ -74,6 +83,16 @@ async function runWebClient(args: Args): Promise<void> {
7483
startArgs.push("-e", `${key}=${value}`);
7584
}
7685

86+
// Pass transport type if specified
87+
if (args.transport) {
88+
startArgs.push("--transport", args.transport);
89+
}
90+
91+
// Pass server URL if specified
92+
if (args.serverUrl) {
93+
startArgs.push("--server-url", args.serverUrl);
94+
}
95+
7796
// Pass command and args (using -- to separate them)
7897
if (args.command) {
7998
startArgs.push("--", args.command, ...args.args);
@@ -217,12 +236,33 @@ function parseArgs(): Args {
217236
if (options.config && options.server) {
218237
const config = loadConfigFile(options.config, options.server);
219238

220-
return {
221-
command: config.command,
222-
args: [...(config.args || []), ...finalArgs],
223-
envArgs: { ...(config.env || {}), ...(options.e || {}) },
224-
cli: options.cli || false,
225-
};
239+
if (config.type === "stdio") {
240+
return {
241+
command: config.command,
242+
args: [...(config.args || []), ...finalArgs],
243+
envArgs: { ...(config.env || {}), ...(options.e || {}) },
244+
cli: options.cli || false,
245+
transport: "stdio",
246+
};
247+
} else if (config.type === "sse" || config.type === "streamable-http") {
248+
return {
249+
command: "",
250+
args: finalArgs,
251+
envArgs: options.e || {},
252+
cli: options.cli || false,
253+
transport: config.type,
254+
serverUrl: config.url,
255+
};
256+
} else {
257+
// Backwards compatibility: if no type field, assume stdio
258+
return {
259+
command: (config as any).command || "",
260+
args: [...((config as any).args || []), ...finalArgs],
261+
envArgs: { ...((config as any).env || {}), ...(options.e || {}) },
262+
cli: options.cli || false,
263+
transport: "stdio",
264+
};
265+
}
226266
}
227267

228268
// Otherwise use command line arguments

0 commit comments

Comments
 (0)