* feat(cli): catalog listing, auth flags, and OAuth browser UX Add servers/list and servers/show, --relogin / --stored-auth-only, browser OAuth navigation with OSC 8 links, and extract method handlers under clients/cli/src/handlers. Tracks #1781. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): address #1782 review — relogin clear, stored-auth-only, docs Clear both raw and normalized OAuth keys on --relogin; check the shared store before bailing under --stored-auth-only mid-session; restore the tests README link target; document servers/show redaction limits; keep mcpi groundwork with TODO(#1432) markers. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): gate OAuth browser open and harden #1782 review follow-ups Arm auto-open only for CLI-owned interactive OAuth, honor MCP_AUTO_OPEN_ENABLED, move --relogin clear after validation, skip keychain on servers/list, and cover secret redaction after rehydrate. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): silence disarmed OAuth URLs and round-3 review nits Suppress authorize URL print when navigation is disarmed or --stored-auth-only; document MCP_AUTO_OPEN_ENABLED; redact Cookie/auth headers; keep servers/show unit tests off the OS keychain. Co-authored-by: Cursor <cursoragent@cursor.com> * test(cli): cover withArmedAutoOpen for the ≥90 branch gate CI failed on cliOAuth.ts branch coverage after the auto-open arming helper landed; exercise the armed path through runCliInteractiveOAuth. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): mid-RPC unauthorized recovery and clear round-4 nits Mirror connect's UnauthorizedError path in withCliAuthRecoveryRetry, let MCP_AUTO_OPEN_ENABLED=true force open off-TTY, mock open-url in the in-process runner, reject --relogin on catalog methods, restore the __tests__ README inventory, and narrow OAuth helpers to structural client types. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): reconnect after mid-RPC OAuth and finish round-5 nits Reconnect before retrying after unauthorized recovery, guard non-OAuth configs, decouple forceAutoOpen, move open-url mock to vitest setupFiles, and reject --relogin with stored-auth short-circuits. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): fail fast on non-TTY interactive OAuth and round-6 nits Skip interactive OAuth without a TTY unless MCP_AUTO_OPEN_ENABLED=true; document --stored-auth-only for CI; redact settings.metadata; reject --relogin for stdio; clarify AuthRecoveryRequired reconnect comments. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): admit interactive OAuth when stdin is a TTY (round-7) Gate on stdin||stderr so `2>&1 | tee` still works; fold confirm/auto-open/ TTY into CliOAuthConnectOptions; rename authRequiredFailure; document MCP_AUTO_OPEN_ENABLED=true as non-TTY admit. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): fail fast on step-up without stdin (round-8) Decline [y/N] on readline EOF/close, require a stdin TTY for the default step-up confirmer (force-open admit is not enough), document stderr-only browser auto-open vs stdin||stderr flow admit, and drop the alias wrapper. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): allow piped step-up answers; bound silent pipes (round-9) Drop the hard stdin-TTY gate that broke `echo y | …`. Keep EOF decline and add a short non-TTY timeout so an open pipe that never writes cannot hang. Clarify isTTY as admit-only; extract the readline mock factory. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): keep partial piped step-up answers (round-10) Capture readline `line` before close so `printf y` without a newline is not treated as decline; timeout is auth_required with a distinct message; document the 5s bound and post-confirm callback wait; pin the TTY no-timeout arm. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): tighten step-up timeout messaging and TTY test (round-11) Make the TTY no-timeout assertion use a settled flag (the Promise.race check was vacuous), say "timed out" instead of "declined" on the pipe bound, and document that piped answers need a newline or stdin close. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
106 lines
4 KiB
TypeScript
106 lines
4 KiB
TypeScript
import { OAuthStorageBase } from "../oauth-storage.js";
|
|
import { OAuthMemoryStore } from "../store.js";
|
|
import { createFileOAuthPersistBackend } from "./oauth-persist-file.js";
|
|
import {
|
|
getDefaultStorageDir,
|
|
getStoreFilePath,
|
|
} from "../../storage/store-io.js";
|
|
|
|
/** Default path: ~/.mcp-inspector/storage/oauth.json */
|
|
const DEFAULT_STATE_PATH = getStoreFilePath(getDefaultStorageDir(), "oauth");
|
|
|
|
/**
|
|
* Get path to OAuth state file. Resolution order:
|
|
* 1. explicit `customPath`
|
|
* 2. `MCP_INSPECTOR_OAUTH_STATE_PATH` — the per-file override (so tests and
|
|
* scripted runs can point at an isolated fixture without touching
|
|
* `~/.mcp-inspector`)
|
|
* 3. `<MCP_STORAGE_DIR>/oauth.json` — the storage-directory override for the
|
|
* OAuth state file specifically, so the CLI's stored-auth flows and the web
|
|
* backend agree on where `oauth.json` lives when the env var is set. Note
|
|
* this branch scopes `MCP_STORAGE_DIR` to the OAuth backend only; it does
|
|
* NOT relocate `client.json` / `mcp.json` (those still resolve from
|
|
* `getDefaultStorageDir()`, which reads `HOME`/`USERPROFILE`). Resolving it
|
|
* here (rather than in `getDefaultStorageDir()`) is deliberate: the default
|
|
* path constant above is evaluated at module load, so a runtime-set
|
|
* `MCP_STORAGE_DIR` — how the CLI stored-auth tests and the `--wait-for-auth`
|
|
* flow set it — is only honoured by reading the env var at call time.
|
|
* 4. the default `~/.mcp-inspector/storage/oauth.json`
|
|
*/
|
|
export function getStateFilePath(customPath?: string): string {
|
|
if (customPath) return customPath;
|
|
if (process.env.MCP_INSPECTOR_OAUTH_STATE_PATH) {
|
|
return process.env.MCP_INSPECTOR_OAUTH_STATE_PATH;
|
|
}
|
|
if (process.env.MCP_STORAGE_DIR) {
|
|
return getStoreFilePath(process.env.MCP_STORAGE_DIR, "oauth");
|
|
}
|
|
return DEFAULT_STATE_PATH;
|
|
}
|
|
|
|
const memoryCache = new Map<string, OAuthMemoryStore>();
|
|
const storageCache = new Map<string, NodeOAuthStorage>();
|
|
|
|
function getSharedMemory(stateFilePath?: string): OAuthMemoryStore {
|
|
const key = getStateFilePath(stateFilePath);
|
|
let memory = memoryCache.get(key);
|
|
if (!memory) {
|
|
memory = new OAuthMemoryStore();
|
|
memoryCache.set(key, memory);
|
|
}
|
|
return memory;
|
|
}
|
|
|
|
/**
|
|
* Drop cached in-memory and {@link NodeOAuthStorage} instances for a path.
|
|
* Used for test isolation and by CLI `--relogin` after clearing on-disk state
|
|
* so the next connect cannot reuse a stale in-process storage singleton.
|
|
*/
|
|
export function resetNodeOAuthStorageCache(stateFilePath?: string): void {
|
|
const key = getStateFilePath(stateFilePath);
|
|
memoryCache.delete(key);
|
|
storageCache.delete(key);
|
|
}
|
|
|
|
/**
|
|
* Clear all OAuth client state (all servers) in the default store.
|
|
* Useful for test isolation in E2E OAuth tests.
|
|
* Use a custom-path store and clear per serverUrl if you need to clear non-default storage.
|
|
*/
|
|
export async function clearAllOAuthClientState(): Promise<void> {
|
|
const storage = getNodeOAuthStorage();
|
|
const filePath = getStateFilePath();
|
|
const snapshot = await createFileOAuthPersistBackend({ filePath }).read();
|
|
const urls = Object.keys(snapshot?.servers ?? {});
|
|
for (const url of urls) {
|
|
await storage.clear(url);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Node.js storage implementation with file-based persistence.
|
|
* For InspectorClient, CLI, and TUI.
|
|
*/
|
|
export class NodeOAuthStorage extends OAuthStorageBase {
|
|
/**
|
|
* @param storagePath - Optional path to state file. Default: ~/.mcp-inspector/storage/oauth.json
|
|
*/
|
|
constructor(storagePath?: string) {
|
|
const filePath = getStateFilePath(storagePath);
|
|
super(
|
|
getSharedMemory(storagePath),
|
|
createFileOAuthPersistBackend({ filePath }),
|
|
);
|
|
}
|
|
}
|
|
|
|
/** Cached NodeOAuthStorage instances keyed by resolved file path. */
|
|
function getNodeOAuthStorage(storagePath?: string): NodeOAuthStorage {
|
|
const key = getStateFilePath(storagePath);
|
|
let storage = storageCache.get(key);
|
|
if (!storage) {
|
|
storage = new NodeOAuthStorage(storagePath);
|
|
storageCache.set(key, storage);
|
|
}
|
|
return storage;
|
|
}
|