cli: two different basePath join rules, so basePath "/" makes the inspector mount and dev-API routes unreachable
Summary
The CLI carries two different rules for joining a child path onto a server basePath, and they disagree when basePath is exactly "/".
src/views/document.ts:3-6 handles the root explicitly:
function pathUnderBase(basePath: string, childPath: string): string {
const child = childPath.replace(/^\/+/, "");
return basePath === "/" ? `/${child}` : `${basePath}/${child}`;
}src/cli/inspector-route.ts:16 and src/cli/dev-api.ts:72-74 interpolate directly instead:
const inspectorPath = `${basePath}/inspector`; const infoPath = `${basePath}/inspector/api/dev/info`;
const startPath = `${basePath}/inspector/api/dev/start-tunnel`;
const stopPath = `${basePath}/inspector/api/dev/stop-tunnel`;With basePath === "/" those produce "//inspector" and "//inspector/api/dev/info". View assets resolve correctly at the root while inspector routes do not, from the same basePath value in the same package.
The Problem
basePath: "/"is valid, supported configuration.assertServerConfig(packages/server/src/config.ts:276-287) rejects a trailing slash only when the path is longer than one character, so"/"passes:(basePath.length > 1 && basePath.endsWith("/"))normalizePathnamehas an explicitif (pathname === "/") return "/"fast path, andfetch-app.ts,branding.tsandserver.tsall branch onbasePath === "/".src/cli/dev.ts:610reads it straight through:basePath = server.basePath ?? "/mcp".mcp-use start --with-inspectoradvertises a URL that does not reach the Inspector.src/bin/main.ts:295builds the printed URL by stripping a trailing slash and appending:`mcp-use inspector at ${started.url.replace(/\/$/, "")}/inspector`That normalises to a single slash,
http://localhost:3000/inspector. The mount guard atsrc/bin/start.ts:247usesisInspectorRequest, which requires"//inspector". The advertised path therefore falls throughnext()to the MCP handler. Verified directly:isInspectorPath("/inspector", "/") // false isInspectorPath("/inspector", "/mcp") // false (correct) viewAssetsBasePath("/", "widget") // "/_mcp-use/views/widget/" (correct)The dev API routes are unreachable at the root. Exercising the real handler with
getBasePath: () => "/"and a sentinel fallback:GET http://127.0.0.1:4242/inspector/api/dev/info -> 418 "FELL THROUGH TO FALLBACK" (expected 200 dev info)So the Inspector UI cannot read
fromCli, and the tunnel start/stop buttons POST into the MCP handler. Nothing is logged.
To be accurate about scope: mcp-use dev is not broken today. src/cli/dev.ts:995 and :1034 print and open `${basePath}/inspector`, the same doubled form the guard matches, so dev is self-consistently //inspector — a malformed-looking URL that happens to work. This is worth stating because it means a fix must move those two sites together with the predicate, or dev breaks.
Reproduced on main at dcaa0b8.
Test coverage: none. Every basePath literal in tests/ is "/mcp" or "/api/mcp" (tests/cli/dev-api.test.ts:39,64, tests/cli/dev.test.ts:190,217,386,454,478,866). There is no test for isInspectorPath at all.
Proposed fix
Give the CLI one join rule. Move pathUnderBase out of views/document.ts into a small internal module and route every consumer through it: the predicate in inspector-route.ts, the three routes in dev-api.ts, and the two URL-building sites in dev.ts:995 and dev.ts:1034.
export function pathUnderBase(basePath: string, childPath: string): string {
const child = childPath.replace(/^\/+/, "");
return basePath === "/" ? `/${child}` : `${basePath}/${child}`;
}All five inspector sites then agree on "/inspector" at the root, which is what start already advertises, and dev prints a clean URL instead of a doubled one. Nested basePath values are unaffected, since the helper's non-root branch is the current expression.
Two smaller options exist if you would rather not add a module: duplicate the ternary at each site, which triplicates the rule the issue is about, or normalise basePath once at the point it is read in dev.ts:610 and start.ts:236, which leaves the two naive joins as traps for the next caller. I went with the shared helper, but happy to follow your preference.
Source: mcp-use/mcp-use