* refactor(web): update OAuth E2E tests to use new authentication method - Replaced `runGuidedAuth` with `authenticate` in the InspectorClient OAuth end-to-end tests to support quick authentication with CIMD pre-registration. - Updated comments to reflect the change in authentication method and its support for HTTP test metadata URLs. - Ensured that the tests continue to validate the expected authorization URL after the change. Additionally, imported `ensureCimdClientRegistration` in the OAuthManager to streamline client registration during the authentication process. * Guided-auth removal, OAuth smoke testing, and TUI/CLI support * Added CLI OAuth runner flags tests * Added clarifying comment on port 0 binding * README notes on TUI oauth callback port, prettier fixes for CLI tests * CIMD validation uses inline like EMA IdP * Fixed TUI test failures from merge * test: bring 5 OAuth files over the 90 per-file coverage gate The guided-auth-removal work added/touched several files that fell below the repo's >=90 per-file coverage gate (npm run coverage). CI doesn't run the gate (it's local-only), so validate + test:integration passed while these gaps slipped through. Bring them all back over the line: - core/auth/node/runner-oauth-callback.ts: test bad-URL/non-http-scheme throws, no-port->80 default, and IPv6 hostname bracketing. Annotate the provably-dead guards (empty host, out-of-range port, String(err) fallback, pathname "/" fallback) -- all rejected by new URL() upstream -- with justified v8 ignore comments instead of lowering the gate. - core/auth/cimd.ts: test the AS-doesn't-support-CIMD early return. - core/react/useClientSettingsDraft.ts: test the functional-updater path, the stale-onChange-after-close prev===null guard, and the timer-fires-after-close value===null guard. - ClientSettingsForm.tsx: test the CIMD checkbox toggle and CIMD URL clear. - clients/tui/src/utils/oauthDisplay.ts: test static/dcr registration kinds, the expired IdP session, and the configured-scope/empty-scope branches of formatScopes. npm run coverage now passes (exit 0) across web, cli, tui, launcher. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: cliffhall <cliff@futurescale.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
176 lines
5 KiB
JavaScript
176 lines
5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { Command } from "commander";
|
|
import { render } from "ink";
|
|
import {
|
|
parseKeyValuePair,
|
|
parseHeaderPair,
|
|
} from "@inspector/core/mcp/node/index.js";
|
|
import { loadRunnerClientConfig } from "@inspector/core/client/runner.js";
|
|
import {
|
|
parseRunnerOAuthCallbackUrl,
|
|
DEFAULT_RUNNER_OAUTH_CALLBACK_URL,
|
|
} from "@inspector/core/auth/node/runner-oauth-callback.js";
|
|
import App from "./src/App.js";
|
|
import { loadTuiServers } from "./src/tui-servers.js";
|
|
|
|
export async function runTui(args?: string[]): Promise<void> {
|
|
const program = new Command();
|
|
|
|
program
|
|
.name("mcp-inspector-tui")
|
|
.description("Terminal UI for MCP Inspector")
|
|
.option(
|
|
"--catalog <path>",
|
|
"Writable catalog file (created if missing; default: ~/.mcp-inspector/mcp.json, or MCP_CATALOG_PATH)",
|
|
)
|
|
.option(
|
|
"--config <path>",
|
|
"Read-only session config file (served as-is, never written or seeded; errors if absent)",
|
|
)
|
|
.option(
|
|
"-e <key=value...>",
|
|
"Environment variables for stdio servers",
|
|
parseKeyValuePair,
|
|
{},
|
|
)
|
|
.option("--cwd <path>", "Working directory for stdio servers")
|
|
.option(
|
|
"--header <header...>",
|
|
'HTTP headers as "Name: Value"',
|
|
parseHeaderPair,
|
|
{},
|
|
)
|
|
.option(
|
|
"--client-id <id>",
|
|
"OAuth client ID (static client) for HTTP servers",
|
|
)
|
|
.option(
|
|
"--client-secret <secret>",
|
|
"OAuth client secret (for confidential clients)",
|
|
)
|
|
.option(
|
|
"--client-metadata-url <url>",
|
|
"OAuth Client ID Metadata Document URL (CIMD) for HTTP servers",
|
|
)
|
|
.option(
|
|
"--client-config <path>",
|
|
"Install-level client config (default: ~/.mcp-inspector/storage/client.json, or MCP_CLIENT_CONFIG_PATH)",
|
|
)
|
|
.option(
|
|
"--callback-url <url>",
|
|
`OAuth redirect/callback listener URL (default: ${DEFAULT_RUNNER_OAUTH_CALLBACK_URL}, or MCP_OAUTH_CALLBACK_URL)`,
|
|
)
|
|
.argument(
|
|
"[target...]",
|
|
"Command and args or URL for a single ad-hoc server (when not using --config)",
|
|
)
|
|
.option(
|
|
"--transport <type>",
|
|
"Transport: stdio, sse, or http (ad-hoc only)",
|
|
)
|
|
.option("--server-url <url>", "Server URL (ad-hoc only)")
|
|
.parse(args ?? process.argv);
|
|
|
|
const options = program.opts() as {
|
|
catalog?: string;
|
|
config?: string;
|
|
e?: Record<string, string>;
|
|
cwd?: string;
|
|
header?: Record<string, string>;
|
|
clientId?: string;
|
|
clientSecret?: string;
|
|
clientMetadataUrl?: string;
|
|
clientConfig?: string;
|
|
callbackUrl?: string;
|
|
transport?: "stdio" | "sse" | "http";
|
|
serverUrl?: string;
|
|
};
|
|
const targetArgs = program.args as string[];
|
|
|
|
const serverOptions = {
|
|
catalogPath: options.catalog?.trim() || process.env.MCP_CATALOG_PATH,
|
|
configPath: options.config?.trim() || undefined,
|
|
target: targetArgs.length > 0 ? targetArgs : undefined,
|
|
cwd: options.cwd?.trim() || undefined,
|
|
env: options.e,
|
|
headers: options.header,
|
|
transport: options.transport,
|
|
serverUrl: options.serverUrl?.trim() || undefined,
|
|
};
|
|
|
|
const mcpServers = await loadTuiServers(serverOptions);
|
|
|
|
const callbackUrlConfig = parseRunnerOAuthCallbackUrl(options.callbackUrl);
|
|
|
|
const clientConfig = await loadRunnerClientConfig({
|
|
clientConfigPath: options.clientConfig,
|
|
});
|
|
|
|
const ansiEraseSavedLines = new RegExp(
|
|
String.fromCharCode(0x1b) + "\\[3J",
|
|
"g",
|
|
);
|
|
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
process.stdout.write = function (
|
|
chunk: string | Buffer,
|
|
encoding?: BufferEncoding | ((err?: Error) => void),
|
|
cb?: (err?: Error) => void,
|
|
): boolean {
|
|
if (typeof chunk === "string") {
|
|
if (chunk.includes("\x1b[3J")) {
|
|
chunk = chunk.replace(ansiEraseSavedLines, "");
|
|
}
|
|
} else if (Buffer.isBuffer(chunk)) {
|
|
if (chunk.includes("\x1b[3J")) {
|
|
let str = chunk.toString("utf8");
|
|
str = str.replace(ansiEraseSavedLines, "");
|
|
chunk = Buffer.from(str, "utf8");
|
|
}
|
|
}
|
|
if (typeof encoding === "function") {
|
|
return (
|
|
originalWrite as (
|
|
chunk: string | Buffer,
|
|
cb?: (err?: Error) => void,
|
|
) => boolean
|
|
)(chunk, encoding);
|
|
}
|
|
return (
|
|
originalWrite as (
|
|
chunk: string | Buffer,
|
|
encoding?: BufferEncoding,
|
|
cb?: (err?: Error) => void,
|
|
) => boolean
|
|
)(chunk, encoding, cb);
|
|
};
|
|
|
|
if (process.stdout.isTTY) {
|
|
process.stdout.write("\x1b[?1049h");
|
|
}
|
|
|
|
const instance = render(
|
|
<App
|
|
mcpServers={mcpServers}
|
|
clientConfig={clientConfig}
|
|
clientId={options.clientId}
|
|
clientSecret={options.clientSecret}
|
|
clientMetadataUrl={options.clientMetadataUrl}
|
|
callbackUrlConfig={callbackUrlConfig}
|
|
/>,
|
|
);
|
|
|
|
try {
|
|
await instance.waitUntilExit();
|
|
if (process.stdout.isTTY) {
|
|
process.stdout.write("\x1b[?1049l");
|
|
}
|
|
process.exit(0);
|
|
} catch (error: unknown) {
|
|
if (process.stdout.isTTY) {
|
|
process.stdout.write("\x1b[?1049l");
|
|
}
|
|
console.error("TUI Error:", error);
|
|
process.exit(1);
|
|
}
|
|
}
|