diff --git a/CHANGELOG.md b/CHANGELOG.md index ac5cb146..3bfc6b25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### OAuth +- Add cache-friendly `disableOAuth` support across headless runtime, CLI, daemon, proxy, and `callOnce` paths so callers can suppress interactive OAuth without losing connection reuse. (Issues #197, #199, #201, thanks @feniix) - Recover cleanly from renamed OAuth server entries, invalid refresh tokens, and stale dynamic client registrations without reusing unrelated same-URL credentials. ### CLI diff --git a/docs/cli-reference.md b/docs/cli-reference.md index a9764fa6..6e4de18f 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -38,6 +38,8 @@ A quick reference for the primary `mcporter` subcommands. Each command inherits - `--exit-code` – exit 1 when any checked server is unhealthy. - `--quiet` – suppress output and exit 1 when any checked server is unhealthy. - `--timeout ` – per-server timeout when enumerating all servers. + - `--no-oauth` – never start an interactive OAuth flow; use cached + tokens only while keeping eligible connections pooled. ## `mcporter call ` @@ -52,6 +54,8 @@ A quick reference for the primary `mcporter` subcommands. Each command inherits - `--raw-strings` – disable numeric coercion for flag-style and positional values. - `--no-coerce` – disable all flag-style/positional value coercion. - `--tail-log` – stream tail output when the tool returns log handles. + - `--no-oauth` – never start an interactive OAuth flow; use cached + tokens only while keeping eligible connections pooled. ## `mcporter resource [uri]` @@ -63,6 +67,8 @@ A quick reference for the primary `mcporter` subcommands. Each command inherits - `--output auto|text|markdown|json|raw` – choose how to render the response. - `--json` – shortcut for `--output json`. - `--raw` – shortcut for `--output raw`. + - `--no-oauth` – never start an interactive OAuth flow; use cached + tokens only while keeping eligible connections pooled. ## `mcporter serve [--servers a,b,c] [--stdio | --http ]` diff --git a/docs/mcp.md b/docs/mcp.md index 4689be2f..d7d4d1f8 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -35,7 +35,7 @@ Use `createServerProxy(runtime, name)` inside scripts when you want ergonomic ca 2. Automatically merges default values. 3. Returns a `CallResult` helper so you can render `.text()`, `.markdown()`, or `.json()` without manual parsing. -When you need raw access (custom transports, streaming), use the bare `Client` from `@modelcontextprotocol/sdk` or inspect `runtime.connect(name)` for lower-level control. +When you need raw access (custom transports, streaming), use the bare `Client` from `@modelcontextprotocol/sdk` or inspect `runtime.connect(name)` for lower-level control. Headless callers that must rely on cached tokens without launching OAuth can pass `disableOAuth: true` to `connect`, `callTool`, `listTools`, resource helpers, and `callOnce`; this suppresses interactive OAuth while keeping eligible connections pooled. ## Debug + Support Docs diff --git a/examples/headless-pooling-demo.ts b/examples/headless-pooling-demo.ts new file mode 100644 index 00000000..1b7a2cc7 --- /dev/null +++ b/examples/headless-pooling-demo.ts @@ -0,0 +1,170 @@ +#!/usr/bin/env tsx + +/** + * Demonstration: `disableOAuth: true` provides cache-friendly OAuth + * suppression for headless callers. + * + * Spins up a local mock MCP server (no real auth), then exercises three + * patterns side-by-side and counts the distinct ClientContext objects + * the runtime hands out: + * + * 1. Legacy `maxOAuthAttempts: 0` — uncached (existing contract). + * 2. `disableOAuth: true` direct connects — pooled. + * 3. The documented headless setup — pre-connect with + * `disableOAuth: true`, then 5 `callTool` invocations. Verifies the + * pre-connected slot is preserved (no implicit eviction). + * + * Run: pnpm tsx examples/headless-pooling-demo.ts + * + * Counting strategy: ClientContext object identity. Each call to + * `createClientContext` inside the runtime returns a fresh object; + * cached calls return the same object. We track the set of unique + * objects and report cardinality. + */ + +import type { Server as HttpServer } from 'node:http'; +import type { AddressInfo } from 'node:net'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import express from 'express'; +import { z } from 'zod'; +import { createRuntime } from '../src/index.js'; + +const INVOCATIONS = 5; + +async function startMockServer(): Promise<{ baseUrl: URL; httpServer: HttpServer }> { + const app = express(); + app.use(express.json()); + + const mcp = new McpServer({ name: 'demo', version: '1.0.0' }); + mcp.registerTool( + 'add', + { + title: 'Addition', + description: 'Add two numbers', + inputSchema: { a: z.number(), b: z.number() }, + outputSchema: { result: z.number() }, + }, + async ({ a, b }) => { + const result = { result: a + b }; + return { + content: [{ type: 'text', text: JSON.stringify(result) }], + structuredContent: result, + }; + } + ); + + app.get('/mcp', (_req, res) => res.sendStatus(405)); + app.post('/mcp', async (req, res) => { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true, + }); + res.on('close', () => { + transport.close().catch(() => {}); + }); + await mcp.connect(transport); + await transport.handleRequest(req, res, req.body); + }); + + const httpServer = app.listen(0, '127.0.0.1'); + await new Promise((resolve, reject) => { + httpServer.once('listening', resolve); + httpServer.once('error', reject); + }); + const address = httpServer.address() as AddressInfo; + return { baseUrl: new URL(`http://127.0.0.1:${address.port}/mcp`), httpServer }; +} + +async function main(): Promise { + // The mock MCP server below has no `auth: 'oauth'` definition, so the + // OAuth flow is not exercised here. This demo focuses on the + // cache-behavior fix (the main fix in PR #198). OAuth-suppression + // semantics under `disableOAuth: true` are exercised by the unit + // tests in `tests/runtime-transport.test.ts` (shouldEstablishOAuth) + // and `tests/runtime-integration.test.ts` (cache + eviction). + const { baseUrl, httpServer } = await startMockServer(); + console.log(`[demo] Mock MCP server listening at ${baseUrl}\n`); + + try { + // ----- Pattern A: legacy maxOAuthAttempts: 0 (uncached) ------------ + { + const runtime = await createRuntime({ + servers: [ + { + name: 'demo', + description: 'Demo server', + command: { kind: 'http', url: baseUrl }, + }, + ], + }); + const contexts = new Set(); + for (let i = 0; i < INVOCATIONS; i++) { + contexts.add(await runtime.connect('demo', { maxOAuthAttempts: 0 })); + } + console.log(`[demo] Pattern A — legacy maxOAuthAttempts: 0`); + console.log(`[demo] ${INVOCATIONS} connect() calls → ${contexts.size} distinct ClientContexts`); + console.log(`[demo] Expected: ${INVOCATIONS} (legacy contract: cache disabled when maxOAuthAttempts is set)`); + console.log(`[demo] Result: ${contexts.size === INVOCATIONS ? 'OK' : 'UNEXPECTED'}\n`); + await runtime.close(); + } + + // ----- Pattern B: disableOAuth: true on every connect --------------- + { + const runtime = await createRuntime({ + servers: [ + { + name: 'demo', + description: 'Demo server', + command: { kind: 'http', url: baseUrl }, + }, + ], + }); + const contexts = new Set(); + for (let i = 0; i < INVOCATIONS; i++) { + contexts.add(await runtime.connect('demo', { disableOAuth: true })); + } + console.log(`[demo] Pattern B — disableOAuth: true on every connect`); + console.log(`[demo] ${INVOCATIONS} connect() calls → ${contexts.size} distinct ClientContexts`); + console.log(`[demo] Expected: 1 (cache reuse under cache-friendly suppression)`); + console.log(`[demo] Result: ${contexts.size === 1 ? 'PASS' : 'FAIL'}\n`); + await runtime.close(); + } + + // ----- Pattern C: documented headless setup + 5 callTool ------------ + { + const runtime = await createRuntime({ + servers: [ + { + name: 'demo', + description: 'Demo server', + command: { kind: 'http', url: baseUrl }, + }, + ], + }); + const initial = await runtime.connect('demo', { disableOAuth: true }); + let sum = 0; + for (let i = 0; i < INVOCATIONS; i++) { + const result = (await runtime.callTool('demo', 'add', { + args: { a: i, b: i + 1 }, + })) as { structuredContent?: { result: number } }; + sum += result.structuredContent?.result ?? 0; + } + const afterCalls = await runtime.connect('demo', { disableOAuth: true }); + const reused = afterCalls === initial; + console.log(`[demo] Pattern C — pre-connect(disableOAuth:true) + ${INVOCATIONS} callTool()`); + console.log(`[demo] Sum of ${INVOCATIONS} add() results: ${sum}`); + console.log(`[demo] Post-callTool connect() === pre-connect ClientContext: ${reused}`); + console.log(`[demo] Expected: true (no implicit eviction from callTool internals)`); + console.log(`[demo] Result: ${reused ? 'PASS' : 'FAIL'}\n`); + await runtime.close(); + } + } finally { + await new Promise((resolve) => httpServer.close(() => resolve())); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/src/cli.ts b/src/cli.ts index 36957547..c4a80e6f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -479,6 +479,7 @@ async function maybeHandleSimpleDaemonFastCall( tool: parsed.tool, args: Object.keys(parsed.args).length > 0 ? parsed.args : undefined, timeoutMs: resolveCallTimeout(parsed.timeoutMs), + disableOAuth: parsed.disableOAuth, }); const { callResult } = wrapCallResult(result); printCallOutput(callResult, result, parsed.output); @@ -583,6 +584,8 @@ function createDaemonOnlyRuntime(daemonClient: import('./daemon/client.js').Daem server, includeSchema: options?.includeSchema, autoAuthorize: options?.autoAuthorize, + allowCachedAuth: options?.allowCachedAuth, + disableOAuth: options?.disableOAuth, })) as Awaited>, callTool: (server, toolName, options) => daemonClient.callTool({ @@ -590,9 +593,27 @@ function createDaemonOnlyRuntime(daemonClient: import('./daemon/client.js').Daem tool: toolName, args: options?.args, timeoutMs: options?.timeoutMs, + disableOAuth: options?.disableOAuth, + }), + listResources: (server, options) => { + const params: Record = { ...options }; + delete params.allowCachedAuth; + delete params.disableOAuth; + delete params.oauthSessionOptions; + return daemonClient.listResources({ + server, + params, + allowCachedAuth: options?.allowCachedAuth, + disableOAuth: options?.disableOAuth, + }); + }, + readResource: (server, uri, options) => + daemonClient.readResource({ + server, + uri, + allowCachedAuth: options?.allowCachedAuth, + disableOAuth: options?.disableOAuth, }), - listResources: (server, options) => daemonClient.listResources({ server, params: options ?? {} }), - readResource: (server, uri) => daemonClient.readResource({ server, uri }), connect: async (server) => { throw new Error(`Server '${server}' is only available through daemon request methods.`); }, diff --git a/src/cli/call-arguments.ts b/src/cli/call-arguments.ts index 0aeb5dae..68181245 100644 --- a/src/cli/call-arguments.ts +++ b/src/cli/call-arguments.ts @@ -25,6 +25,7 @@ export interface CallArgsParseResult { tailLog: boolean; output: OutputFormat; timeoutMs?: number; + disableOAuth?: boolean; ephemeral?: EphemeralServerSpec; rawStrings?: boolean; saveImagesDir?: string; @@ -59,6 +60,7 @@ const FLAG_HANDLERS: Record = { '--tool': handleToolFlag, '--timeout': handleTimeoutFlag, '--tail-log': handleTailLogFlag, + '--no-oauth': handleDisableOAuthFlag, '--save-images': handleSaveImagesFlag, '--yes': handleNoopFlag, '--raw-strings': handleRawStringsFlag, @@ -256,6 +258,11 @@ function handleTailLogFlag(context: FlagHandlerContext): number { return context.index + 1; } +function handleDisableOAuthFlag(context: FlagHandlerContext): number { + context.result.disableOAuth = true; + return context.index + 1; +} + function handleSaveImagesFlag(context: FlagHandlerContext): number { context.result.saveImagesDir = consumeFlagValue( context.args, diff --git a/src/cli/call-command.ts b/src/cli/call-command.ts index ccebebaf..f2f95ec8 100644 --- a/src/cli/call-command.ts +++ b/src/cli/call-command.ts @@ -39,6 +39,7 @@ interface PreparedCallRequest extends ResolvedCallTarget { parsed: CallArgsParseResult; hydratedArgs: Record; timeoutMs: number; + disableOAuth?: boolean; ephemeralTarget?: PrepareEphemeralServerTargetResult; } @@ -66,12 +67,19 @@ async function prepareCallRequest(runtime: Runtime, args: string[]): Promise
. or --server.');
   }
   if (!tool) {
-    tool = await inferSingleToolName(runtime, server);
+    tool = await inferSingleToolName(runtime, server, parsed.disableOAuth);
     if (!tool) {
       throw new Error('Missing tool name. Provide it via . or --tool.');
     }
@@ -165,7 +182,8 @@ async function invokePreparedCall(
       prepared.tool,
       prepared.hydratedArgs,
       prepared.timeoutMs,
-      prepared.parsed.output
+      prepared.parsed.output,
+      prepared.disableOAuth
     );
   } catch (error) {
     const issue = maybeReportConnectionIssue(prepared.server, prepared.tool, error);
@@ -224,11 +242,15 @@ async function maybeDescribeServer(
   runtime: Awaited>,
   server: string,
   tool: string,
-  outputFormat: OutputFormat
+  outputFormat: OutputFormat,
+  disableOAuth: boolean | undefined
 ): Promise {
   if (tool === 'list_tools') {
     console.log(dimText(`[mcporter] ${server}.list_tools is a shortcut for 'mcporter list ${server}'.`));
     const listArgs = [server];
+    if (disableOAuth) {
+      listArgs.push('--no-oauth');
+    }
     if (outputFormat === 'json') {
       listArgs.push('--json');
     }
@@ -239,7 +261,9 @@ async function maybeDescribeServer(
   if (tool !== 'help') {
     return false;
   }
-  const tools = await runtime.listTools(server, { includeSchema: false, autoAuthorize: false }).catch(() => undefined);
+  const tools = await runtime
+    .listTools(server, { includeSchema: false, autoAuthorize: false, disableOAuth })
+    .catch(() => undefined);
   if (!tools) {
     return false;
   }
@@ -249,6 +273,9 @@ async function maybeDescribeServer(
   }
   console.log(dimText(`[mcporter] ${server} does not expose a 'help' tool; showing mcporter list output instead.`));
   const listArgs = [server];
+  if (disableOAuth) {
+    listArgs.push('--no-oauth');
+  }
   if (outputFormat === 'json') {
     listArgs.push('--json');
   }
@@ -296,7 +323,8 @@ async function enforceSchemaAwareArgumentTypes(
   args: Record,
   stringCandidates: Record | undefined,
   arrayCandidates: Record | undefined,
-  timeoutMs: number
+  timeoutMs: number,
+  disableOAuth: boolean | undefined
 ): Promise> {
   if (
     (!stringCandidates || Object.keys(stringCandidates).length === 0) &&
@@ -305,9 +333,10 @@ async function enforceSchemaAwareArgumentTypes(
     return args;
   }
 
-  const tools = await withTimeout(loadToolMetadata(runtime, server, { includeSchema: true }), timeoutMs).catch(
-    () => undefined
-  );
+  const tools = await withTimeout(
+    loadToolMetadata(runtime, server, { includeSchema: true, disableOAuth }),
+    timeoutMs
+  ).catch(() => undefined);
   if (!tools) {
     return args;
   }
@@ -389,14 +418,15 @@ async function hydratePositionalArguments(
   server: string,
   tool: string,
   namedArgs: Record,
-  positionalArgs: unknown[] | undefined
+  positionalArgs: unknown[] | undefined,
+  disableOAuth: boolean | undefined
 ): Promise> {
   if (!positionalArgs || positionalArgs.length === 0) {
     return namedArgs;
   }
   // We need the schema order to know which field each positional argument maps to; pull the
   // tool list with schemas instead of guessing locally so optional/required order stays correct.
-  const tools = await loadToolMetadata(runtime, server, { includeSchema: true }).catch(() => undefined);
+  const tools = await loadToolMetadata(runtime, server, { includeSchema: true, disableOAuth }).catch(() => undefined);
   if (!tools) {
     throw new Error('Unable to load tool metadata; name positional arguments explicitly.');
   }
@@ -436,9 +466,10 @@ type ToolResolution = IdentifierResolution;
 
 async function inferSingleToolName(
   runtime: Awaited>,
-  server: string
+  server: string,
+  disableOAuth: boolean | undefined
 ): Promise {
-  const tools = await loadToolMetadata(runtime, server, { includeSchema: false });
+  const tools = await loadToolMetadata(runtime, server, { includeSchema: false, disableOAuth });
   if (tools.length !== 1) {
     return undefined;
   }
@@ -456,10 +487,11 @@ async function invokeWithAutoCorrection(
   tool: string,
   args: Record,
   timeoutMs: number,
-  outputFormat: OutputFormat
+  outputFormat: OutputFormat,
+  disableOAuth: boolean | undefined
 ): Promise<{ result: unknown; resolvedTool: string }> {
   // Attempt the original request first; if it fails with a "tool not found" we opportunistically retry once with a better match.
-  return attemptCall(runtime, server, tool, args, timeoutMs, outputFormat, true);
+  return attemptCall(runtime, server, tool, args, timeoutMs, outputFormat, true, disableOAuth);
 }
 
 async function attemptCall(
@@ -469,14 +501,24 @@ async function attemptCall(
   args: Record,
   timeoutMs: number,
   outputFormat: OutputFormat,
-  allowCorrection: boolean
+  allowCorrection: boolean,
+  disableOAuth: boolean | undefined
 ): Promise<{ result: unknown; resolvedTool: string }> {
   try {
-    const result = await withTimeout(runtime.callTool(server, tool, { args, timeoutMs }), timeoutMs);
+    const result = await withTimeout(runtime.callTool(server, tool, { args, timeoutMs, disableOAuth }), timeoutMs);
     if (allowCorrection && isErrorCallResult(result)) {
-      const resolution = await maybeResolveToolName(runtime, server, tool, result);
+      const resolution = await maybeResolveToolName(runtime, server, tool, result, disableOAuth);
       if (resolution) {
-        const retry = await maybeRetryResolvedTool(runtime, server, tool, args, timeoutMs, outputFormat, resolution);
+        const retry = await maybeRetryResolvedTool(
+          runtime,
+          server,
+          tool,
+          args,
+          timeoutMs,
+          outputFormat,
+          resolution,
+          disableOAuth
+        );
         if (retry) {
           return retry;
         }
@@ -497,13 +539,22 @@ async function attemptCall(
       throw error;
     }
 
-    const resolution = await maybeResolveToolName(runtime, server, tool, error);
+    const resolution = await maybeResolveToolName(runtime, server, tool, error, disableOAuth);
     if (!resolution) {
       maybeReportConnectionIssue(server, tool, error);
       throw error;
     }
 
-    const retry = await maybeRetryResolvedTool(runtime, server, tool, args, timeoutMs, outputFormat, resolution);
+    const retry = await maybeRetryResolvedTool(
+      runtime,
+      server,
+      tool,
+      args,
+      timeoutMs,
+      outputFormat,
+      resolution,
+      disableOAuth
+    );
     if (!retry) {
       throw error;
     }
@@ -518,7 +569,8 @@ async function maybeRetryResolvedTool(
   args: Record,
   timeoutMs: number,
   outputFormat: OutputFormat,
-  resolution: ToolResolution
+  resolution: ToolResolution,
+  disableOAuth: boolean | undefined
 ): Promise<{ result: unknown; resolvedTool: string } | undefined> {
   const messages = renderIdentifierResolutionMessages({
     entity: 'tool',
@@ -536,14 +588,15 @@ async function maybeRetryResolvedTool(
     const emitAutoMessage = outputFormat === 'json' || outputFormat === 'raw' ? console.error : console.log;
     emitAutoMessage(dimText(messages.auto));
   }
-  return attemptCall(runtime, server, resolution.value, args, timeoutMs, outputFormat, false);
+  return attemptCall(runtime, server, resolution.value, args, timeoutMs, outputFormat, false, disableOAuth);
 }
 
 async function maybeResolveToolName(
   runtime: Awaited>,
   server: string,
   attemptedTool: string,
-  error: unknown
+  error: unknown,
+  disableOAuth: boolean | undefined
 ): Promise {
   const missingName = extractMissingToolFromError(error);
   if (!missingName) {
@@ -555,7 +608,7 @@ async function maybeResolveToolName(
     return undefined;
   }
 
-  const tools = await loadToolMetadata(runtime, server, { includeSchema: false }).catch(() => undefined);
+  const tools = await loadToolMetadata(runtime, server, { includeSchema: false, disableOAuth }).catch(() => undefined);
   if (!tools) {
     return undefined;
   }
diff --git a/src/cli/call-help.ts b/src/cli/call-help.ts
index 573a2c41..cc292fc5 100644
--- a/src/cli/call-help.ts
+++ b/src/cli/call-help.ts
@@ -10,6 +10,7 @@ export const CALL_HELP_RUNTIME_FLAG_LINES = [
   '  --timeout          Override the call timeout.',
   '  --output text|markdown|json|raw  Control formatting.',
   '  --save-images     Save image content blocks to a directory.',
+  '  --no-oauth             Never start OAuth; use cached tokens only.',
   '  --raw-strings          Keep numeric-looking argument values as strings.',
   '  --no-coerce            Keep all key/value and positional arguments as raw strings.',
   '  --tail-log             Stream returned log handles.',
diff --git a/src/cli/list-command.ts b/src/cli/list-command.ts
index 825a03c7..5a13812e 100644
--- a/src/cli/list-command.ts
+++ b/src/cli/list-command.ts
@@ -98,7 +98,7 @@ export async function handleList(
       let completedCount = 0;
 
       const tasks = servers.map((server, index) =>
-        checkListServer(runtime, server, perServerTimeoutMs).then((result) => {
+        checkListServer(runtime, server, perServerTimeoutMs, flags.disableOAuth).then((result) => {
           summaryResults[index] = result;
           if (renderedResults) {
             const rendered = renderServerListRow(result, perServerTimeoutMs, { verbose: flags.verbose });
@@ -190,7 +190,7 @@ export async function handleList(
   if (flags.statusOnly) {
     const previousStdioLogMode = flags.quiet || flags.format === 'json' ? setStdioLogMode('silent') : undefined;
     try {
-      const result = await checkListServer(runtime, definition, timeoutMs);
+      const result = await checkListServer(runtime, definition, timeoutMs, flags.disableOAuth);
       await persistPreparedEphemeralServer(runtime, prepared);
       const entry = buildJsonListEntry(result, Math.round(timeoutMs / 1000), {
         includeSchemas: false,
@@ -228,6 +228,7 @@ export async function handleList(
               includeSchema: true,
               autoAuthorize: false,
               allowCachedAuth: true,
+              disableOAuth: flags.disableOAuth,
             }),
             timeoutMs
           ),
@@ -298,6 +299,7 @@ export async function handleList(
             includeSchema: true,
             autoAuthorize: false,
             allowCachedAuth: true,
+            disableOAuth: flags.disableOAuth,
           }),
           timeoutMs
         ),
@@ -397,12 +399,13 @@ export async function handleList(
 async function checkListServer(
   runtime: Awaited>,
   server: ServerDefinition,
-  timeoutMs: number
+  timeoutMs: number,
+  disableOAuth: boolean
 ): Promise {
   const startedAt = Date.now();
   try {
     const tools = await withTimeout(
-      runtime.listTools(server.name, { autoAuthorize: false, allowCachedAuth: true }),
+      runtime.listTools(server.name, { autoAuthorize: false, allowCachedAuth: true, disableOAuth }),
       timeoutMs
     );
     return {
@@ -483,6 +486,7 @@ export function printListHelp(): void {
     '  --verbose              Show all config sources for matching servers.',
     '  --sources              Include source arrays in JSON output without other verbose details.',
     '  --timeout          Override the per-server discovery timeout.',
+    '  --no-oauth             Never start OAuth; use cached tokens only.',
     '',
     'Examples:',
     '  mcporter list',
diff --git a/src/cli/list-flags.ts b/src/cli/list-flags.ts
index 4f261d55..1b7c2e54 100644
--- a/src/cli/list-flags.ts
+++ b/src/cli/list-flags.ts
@@ -17,6 +17,7 @@ export function extractListFlags(args: string[]): {
   quiet: boolean;
   exitCode: boolean;
   statusOnly: boolean;
+  disableOAuth: boolean;
 } {
   let schema = false;
   let timeoutMs: number | undefined;
@@ -27,6 +28,7 @@ export function extractListFlags(args: string[]): {
   let quiet = false;
   let exitCode = false;
   let statusOnly = false;
+  let disableOAuth = false;
   const format = consumeOutputFormat(args, {
     defaultFormat: 'text',
     allowed: ['text', 'json'],
@@ -82,6 +84,11 @@ export function extractListFlags(args: string[]): {
       args.splice(index, 1);
       continue;
     }
+    if (token === '--no-oauth') {
+      disableOAuth = true;
+      args.splice(index, 1);
+      continue;
+    }
     if (token === '--timeout') {
       timeoutMs = consumeTimeoutFlag(args, index, { flagName: '--timeout' });
       continue;
@@ -133,5 +140,6 @@ export function extractListFlags(args: string[]): {
     quiet,
     exitCode,
     statusOnly,
+    disableOAuth,
   };
 }
diff --git a/src/cli/resource-command.ts b/src/cli/resource-command.ts
index ba494d6b..39e89247 100644
--- a/src/cli/resource-command.ts
+++ b/src/cli/resource-command.ts
@@ -13,6 +13,7 @@ export async function handleResource(runtime: Runtime, args: string[]): Promise<
     enableRawShortcut: true,
     jsonShortcutFlag: '--json',
   });
+  const disableOAuth = consumeDisableOAuthFlag(args);
   const server = args.shift();
   if (!server) {
     throw new Error('Missing server name. Usage: mcporter resource  [uri]');
@@ -24,7 +25,14 @@ export async function handleResource(runtime: Runtime, args: string[]): Promise<
 
   let result: unknown;
   try {
-    result = uri ? await runtime.readResource(server, uri) : await runtime.listResources(server);
+    if (disableOAuth === undefined) {
+      result = uri ? await runtime.readResource(server, uri) : await runtime.listResources(server);
+    } else {
+      const connectOptions = { disableOAuth };
+      result = uri
+        ? await runtime.readResource(server, uri, connectOptions)
+        : await runtime.listResources(server, connectOptions);
+    }
   } catch (error) {
     const issue = analyzeConnectionError(error);
     if (output === 'json' || output === 'raw') {
@@ -39,6 +47,20 @@ export async function handleResource(runtime: Runtime, args: string[]): Promise<
   printCallOutput(callResult, result, output);
 }
 
+function consumeDisableOAuthFlag(args: string[]): boolean | undefined {
+  let disableOAuth: boolean | undefined;
+  for (let index = 0; index < args.length; ) {
+    const token = args[index];
+    if (token === '--no-oauth') {
+      disableOAuth = true;
+      args.splice(index, 1);
+      continue;
+    }
+    index += 1;
+  }
+  return disableOAuth;
+}
+
 export function printResourceHelp(): void {
   console.error(
     [
@@ -51,6 +73,7 @@ export function printResourceHelp(): void {
       '  --output auto|text|markdown|json|raw  Choose output rendering.',
       '  --json                               Shortcut for --output json.',
       '  --raw                                Shortcut for --output raw.',
+      '  --no-oauth                          Never start OAuth; use cached tokens only.',
       '',
       'Examples:',
       '  mcporter resource docs',
diff --git a/src/cli/tool-cache.ts b/src/cli/tool-cache.ts
index 16f1cdc3..b6d39d29 100644
--- a/src/cli/tool-cache.ts
+++ b/src/cli/tool-cache.ts
@@ -5,6 +5,7 @@ interface LoadToolMetadataOptions {
   includeSchema?: boolean;
   autoAuthorize?: boolean;
   allowCachedAuth?: boolean;
+  disableOAuth?: boolean;
 }
 
 const runtimeCache = new WeakMap>>();
@@ -13,7 +14,8 @@ function cacheKey(serverName: string, options: LoadToolMetadataOptions): string
   const includeSchema = options.includeSchema !== false;
   const autoAuthorize = options.autoAuthorize !== false;
   const allowCachedAuth = options.allowCachedAuth !== false;
-  return `${serverName}::schema:${includeSchema ? '1' : '0'}::auth:${autoAuthorize ? '1' : '0'}::cached-auth:${allowCachedAuth ? '1' : '0'}`;
+  const disableOAuth = options.disableOAuth === true;
+  return `${serverName}::schema:${includeSchema ? '1' : '0'}::auth:${autoAuthorize ? '1' : '0'}::cached-auth:${allowCachedAuth ? '1' : '0'}::disable-oauth:${disableOAuth ? '1' : '0'}`;
 }
 
 export async function loadToolMetadata(
@@ -37,6 +39,7 @@ export async function loadToolMetadata(
     includeSchema,
     autoAuthorize,
     allowCachedAuth: options.allowCachedAuth ?? true,
+    disableOAuth: options.disableOAuth,
   };
   const promise = runtime
     .listTools(serverName, listOptions)
diff --git a/src/daemon/host.ts b/src/daemon/host.ts
index eed21db6..433fbe8e 100644
--- a/src/daemon/host.ts
+++ b/src/daemon/host.ts
@@ -503,6 +503,13 @@ async function handleSocketRequest(
   });
 }
 
+function normalizeDaemonDisableOAuth(value: boolean | undefined): boolean {
+  // Daemon messages are independent requests. Omission means the caller did
+  // not request OAuth suppression, so a previous --no-oauth pooled transport
+  // must not make later ordinary calls inherit the no-OAuth posture.
+  return value === true;
+}
+
 async function processRequest(
   rawPayload: string,
   runtime: Runtime,
@@ -554,6 +561,7 @@ async function processRequest(
           const result = await runtime.callTool(params.server, params.tool, {
             args: params.args ?? {},
             timeoutMs: params.timeoutMs,
+            disableOAuth: normalizeDaemonDisableOAuth(params.disableOAuth),
           });
           markActivity(params.server, activity);
           if (loggable) {
@@ -581,6 +589,7 @@ async function processRequest(
             includeSchema: params.includeSchema,
             autoAuthorize: resolveDaemonListToolsAutoAuthorize(params, definition),
             allowCachedAuth: params.allowCachedAuth ?? true,
+            disableOAuth: normalizeDaemonDisableOAuth(params.disableOAuth),
           });
           markActivity(params.server, activity);
           if (loggable) {
@@ -603,7 +612,11 @@ async function processRequest(
           logEvent(logContext, `listResources start server=${params.server}`);
         }
         try {
-          const result = await runtime.listResources(params.server, params.params);
+          const result = await runtime.listResources(params.server, {
+            ...params.params,
+            allowCachedAuth: params.allowCachedAuth,
+            disableOAuth: normalizeDaemonDisableOAuth(params.disableOAuth),
+          });
           markActivity(params.server, activity);
           if (loggable) {
             logEvent(logContext, `listResources success server=${params.server}`);
@@ -625,7 +638,10 @@ async function processRequest(
           logEvent(logContext, `readResource start server=${params.server} uri=${params.uri}`);
         }
         try {
-          const result = await runtime.readResource(params.server, params.uri);
+          const result = await runtime.readResource(params.server, params.uri, {
+            allowCachedAuth: params.allowCachedAuth,
+            disableOAuth: normalizeDaemonDisableOAuth(params.disableOAuth),
+          });
           markActivity(params.server, activity);
           if (loggable) {
             logEvent(logContext, `readResource success server=${params.server}`);
diff --git a/src/daemon/protocol.ts b/src/daemon/protocol.ts
index 77aa5318..925fc46a 100644
--- a/src/daemon/protocol.ts
+++ b/src/daemon/protocol.ts
@@ -28,6 +28,7 @@ export interface CallToolParams {
   readonly tool: string;
   readonly args?: Record;
   readonly timeoutMs?: number;
+  readonly disableOAuth?: boolean;
 }
 
 export interface ListToolsParams {
@@ -35,16 +36,21 @@ export interface ListToolsParams {
   readonly includeSchema?: boolean;
   readonly autoAuthorize?: boolean;
   readonly allowCachedAuth?: boolean;
+  readonly disableOAuth?: boolean;
 }
 
 export interface ListResourcesParams {
   readonly server: string;
   readonly params?: Record;
+  readonly allowCachedAuth?: boolean;
+  readonly disableOAuth?: boolean;
 }
 
 export interface ReadResourceParams {
   readonly server: string;
   readonly uri: string;
+  readonly allowCachedAuth?: boolean;
+  readonly disableOAuth?: boolean;
 }
 
 export interface CloseServerParams {
diff --git a/src/daemon/runtime-wrapper.ts b/src/daemon/runtime-wrapper.ts
index 0304233c..a84a8514 100644
--- a/src/daemon/runtime-wrapper.ts
+++ b/src/daemon/runtime-wrapper.ts
@@ -1,8 +1,14 @@
-import type { ListResourcesRequest } from '@modelcontextprotocol/sdk/types.js';
 import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
 import type { ServerDefinition } from '../config.js';
 import { isKeepAliveServer } from '../lifecycle.js';
-import type { CallOptions, ListToolsOptions, Runtime } from '../runtime.js';
+import type {
+  CallOptions,
+  ConnectOptions,
+  ListResourcesOptions,
+  ListToolsOptions,
+  ReadResourceOptions,
+  Runtime,
+} from '../runtime.js';
 import type { DaemonClient } from './client.js';
 
 interface KeepAliveRuntimeOptions {
@@ -62,6 +68,7 @@ class KeepAliveRuntime implements Runtime {
           includeSchema: options?.includeSchema,
           autoAuthorize: options?.autoAuthorize,
           allowCachedAuth: options?.allowCachedAuth ?? true,
+          disableOAuth: options?.disableOAuth,
         })
       )) as Awaited>;
     }
@@ -76,30 +83,45 @@ class KeepAliveRuntime implements Runtime {
           tool: toolName,
           args: options?.args,
           timeoutMs: options?.timeoutMs,
+          disableOAuth: options?.disableOAuth,
         })
       );
     }
     return this.base.callTool(server, toolName, options);
   }
 
-  async listResources(server: string, options?: Partial): Promise {
+  async listResources(server: string, options?: ListResourcesOptions): Promise {
+    if (options?.oauthSessionOptions) {
+      return this.base.listResources(server, options);
+    }
+    const { allowCachedAuth, disableOAuth, ...params } = options ?? {};
     if (this.shouldUseDaemon(server)) {
       return this.invokeWithRestart(server, 'listResources', () =>
-        this.daemon.listResources({ server, params: options ?? {} })
+        this.daemon.listResources({ server, params, allowCachedAuth, disableOAuth })
       );
     }
     return this.base.listResources(server, options);
   }
 
-  async readResource(server: string, uri: string): Promise {
+  async readResource(server: string, uri: string, options?: ReadResourceOptions): Promise {
+    if (options?.oauthSessionOptions) {
+      return this.base.readResource(server, uri, options);
+    }
     if (this.shouldUseDaemon(server)) {
-      return this.invokeWithRestart(server, 'readResource', () => this.daemon.readResource({ server, uri }));
+      return this.invokeWithRestart(server, 'readResource', () =>
+        this.daemon.readResource({
+          server,
+          uri,
+          allowCachedAuth: options?.allowCachedAuth,
+          disableOAuth: options?.disableOAuth,
+        })
+      );
     }
-    return this.base.readResource(server, uri);
+    return this.base.readResource(server, uri, options);
   }
 
-  async connect(server: string): Promise>> {
-    return this.base.connect(server);
+  async connect(server: string, options?: ConnectOptions): Promise>> {
+    return this.base.connect(server, options);
   }
 
   async close(server?: string): Promise {
diff --git a/src/index.ts b/src/index.ts
index d9c665d3..52accbc0 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -4,7 +4,10 @@ export type { CallResult, ConnectionIssue, ImageContent } from './result-utils.j
 export { createCallResult, describeConnectionIssue, wrapCallResult } from './result-utils.js';
 export type {
   CallOptions,
+  ConnectOptions,
+  ListResourcesOptions,
   ListToolsOptions,
+  ReadResourceOptions,
   Runtime,
   RuntimeLogger,
   RuntimeOptions,
diff --git a/src/runtime.ts b/src/runtime.ts
index 95d4769e..eb482e3d 100644
--- a/src/runtime.ts
+++ b/src/runtime.ts
@@ -18,6 +18,14 @@ export { MCPORTER_VERSION } from './version.js';
 const PACKAGE_NAME = 'mcporter';
 const OAUTH_CODE_TIMEOUT_MS = resolveOAuthTimeoutFromEnv();
 
+type CachedClientEntry = {
+  readonly server: string;
+  readonly promise: Promise;
+  readonly contextPromise?: Promise;
+  readonly allowCachedAuth: boolean | undefined;
+  readonly disableOAuth: boolean;
+};
+
 export interface RuntimeOptions {
   readonly configPath?: string;
   readonly servers?: ServerDefinition[];
@@ -35,6 +43,11 @@ export type RuntimeLogger = Logger;
 export interface CallOptions {
   readonly args?: CallToolRequest['params']['arguments'];
   readonly timeoutMs?: number;
+  /**
+   * Suppress interactive OAuth for this call while still allowing cached
+   * bearer tokens to be applied. Intended for headless callers.
+   */
+  readonly disableOAuth?: boolean;
 }
 
 export interface ListToolsOptions {
@@ -42,13 +55,49 @@ export interface ListToolsOptions {
   readonly autoAuthorize?: boolean;
   readonly allowCachedAuth?: boolean;
   readonly oauthSessionOptions?: OAuthSessionOptions;
+  /**
+   * Suppress interactive OAuth for this listing while keeping the connection
+   * cache available. Prefer this over `autoAuthorize: false` for long-running
+   * headless callers that need cached-token-only behavior.
+   */
+  readonly disableOAuth?: boolean;
 }
 
-interface ConnectOptions {
+export type ListResourcesOptions = Partial & {
+  readonly allowCachedAuth?: boolean;
+  readonly oauthSessionOptions?: OAuthSessionOptions;
+  readonly disableOAuth?: boolean;
+};
+
+export interface ReadResourceOptions {
+  readonly allowCachedAuth?: boolean;
+  readonly oauthSessionOptions?: OAuthSessionOptions;
+  readonly disableOAuth?: boolean;
+}
+
+export interface ConnectOptions {
   readonly maxOAuthAttempts?: number;
   readonly skipCache?: boolean;
   readonly allowCachedAuth?: boolean;
   readonly oauthSessionOptions?: OAuthSessionOptions;
+  /**
+   * When `true`, never start an OAuth flow for this server — equivalent
+   * to `maxOAuthAttempts: 0` for the purpose of avoiding interactive
+   * authorization. Unlike `maxOAuthAttempts: 0`, callers passing
+   * `disableOAuth: true` participate in connection caching: repeated
+   * `connect()` / `callTool()` / `listTools()` calls reuse the same
+   * `ClientContext`, and `close()` reaps it.
+   *
+   * Intended for long-running headless callers (daemons, scheduled jobs,
+   * CI workers) that have no browser and must rely on cached tokens.
+   *
+   * Cache identity: clients established with `disableOAuth: true` are
+   * stored in their own cache slot — sharing with a connection that
+   * could refresh into an OAuth flow would violate the no-browser-launch
+   * guarantee. Switching the flag between calls keeps both variants cached
+   * until the caller closes the server or runtime.
+   */
+  readonly disableOAuth?: boolean;
 }
 
 export interface Runtime {
@@ -59,9 +108,9 @@ export interface Runtime {
   getInstructions?(server: string): Promise;
   listTools(server: string, options?: ListToolsOptions): Promise;
   callTool(server: string, toolName: string, options?: CallOptions): Promise;
-  listResources(server: string, options?: Partial): Promise;
-  readResource(server: string, uri: string): Promise;
-  connect(server: string): Promise;
+  listResources(server: string, options?: ListResourcesOptions): Promise;
+  readResource(server: string, uri: string, options?: ReadResourceOptions): Promise;
+  connect(server: string, options?: ConnectOptions): Promise;
   close(server?: string): Promise;
 }
 
@@ -92,11 +141,13 @@ export async function callOnce(params: {
   toolName: string;
   args?: Record;
   configPath?: string;
+  disableOAuth?: boolean;
 }): Promise {
   const runtime = await createRuntime({ configPath: params.configPath });
   try {
     return await runtime.callTool(params.server, params.toolName, {
       args: params.args,
+      disableOAuth: params.disableOAuth,
     });
   } finally {
     await runtime.close(params.server);
@@ -105,13 +156,13 @@ export async function callOnce(params: {
 
 class McpRuntime implements Runtime {
   private readonly definitions: Map;
-  private readonly clients = new Map<
-    string,
-    {
-      readonly promise: Promise;
-      readonly allowCachedAuth: boolean | undefined;
-    }
-  >();
+  private readonly clients = new Map();
+  private readonly activeClientKeys = new Map();
+  private readonly contextCacheKeys = new WeakMap();
+  private readonly contextCachePromises = new WeakMap>();
+  private readonly connectionSetupTails = new Map>();
+  private readonly serverGenerations = new Map();
+  private readonly retirementPromises = new Map>>();
   private readonly logger: RuntimeLogger;
   private readonly clientInfo: { name: string; version: string };
   private readonly oauthTimeoutMs?: number;
@@ -162,12 +213,15 @@ class McpRuntime implements Runtime {
     if (!options.overwrite && this.definitions.has(definition.name)) {
       throw new Error(`MCP server '${definition.name}' already exists.`);
     }
+    this.bumpServerGeneration(definition.name);
     this.definitions.set(definition.name, definition);
-    this.clients.delete(definition.name);
+    this.retireCachedEntriesForServer(definition.name);
   }
 
   async getInstructions(server: string): Promise {
-    const cached = this.clients.get(server.trim());
+    const active = this.activeClientForServer(server);
+    const fallbackEntries = active ? [] : this.cachedEntriesForServer(server);
+    const cached = active ?? (fallbackEntries.length === 1 ? fallbackEntries[0] : undefined);
     if (!cached) {
       return undefined;
     }
@@ -188,12 +242,23 @@ class McpRuntime implements Runtime {
   // listTools queries tool metadata and optionally includes schemas when requested.
   async listTools(server: string, options: ListToolsOptions = {}): Promise {
     // Toggle auto authorization so list can run without forcing OAuth flows.
+    // `disableOAuth` is the cache-friendly suppression path; when present it
+    // supersedes the legacy `autoAuthorize: false` uncached behavior.
     const autoAuthorize = options.autoAuthorize !== false;
+    const disableOAuth = this.effectiveDisableOAuthForOperation(server, options.disableOAuth);
+    const allowCachedAuth = this.effectiveAllowCachedAuthForOperation(
+      server,
+      options.allowCachedAuth,
+      disableOAuth,
+      true
+    );
+    const useLegacyNoAuthorize = !autoAuthorize && disableOAuth !== true;
     const context = await this.connect(server, {
-      maxOAuthAttempts: autoAuthorize ? undefined : 0,
-      skipCache: !autoAuthorize,
-      allowCachedAuth: options.allowCachedAuth ?? true,
+      maxOAuthAttempts: useLegacyNoAuthorize ? 0 : undefined,
+      skipCache: useLegacyNoAuthorize,
+      allowCachedAuth,
       oauthSessionOptions: options.oauthSessionOptions,
+      disableOAuth,
     });
     let closeError: unknown;
     const tools: ServerToolInfo[] = [];
@@ -214,10 +279,10 @@ class McpRuntime implements Runtime {
     } catch (error) {
       // Keep-alive STDIO transports often die when Chrome closes; drop the cached client
       // so the next call spins up a fresh process instead of reusing the broken handle.
-      await this.resetConnectionOnError(server, error);
+      await this.resetConnectionOnError(server, error, context);
       throw error;
     } finally {
-      if (!autoAuthorize) {
+      if (useLegacyNoAuthorize) {
         try {
           await this.closeContext(context);
         } catch (error) {
@@ -240,10 +305,14 @@ class McpRuntime implements Runtime {
         `Tool '${toolName}' is not accessible on server '${definition.name}' (blocked by configuration).`
       );
     }
+    let context: ClientContext | undefined;
     try {
-      const { client } = await this.connect(server, {
-        allowCachedAuth: true,
+      const disableOAuth = this.effectiveDisableOAuthForOperation(server, options.disableOAuth);
+      context = await this.connect(server, {
+        allowCachedAuth: this.effectiveAllowCachedAuthForOperation(server, undefined, disableOAuth, true),
+        disableOAuth,
       });
+      const { client } = context;
       const params: CallToolRequest['params'] = {
         name: toolName,
         arguments: options.args ?? {},
@@ -264,102 +333,379 @@ class McpRuntime implements Runtime {
     } catch (error) {
       // Runtime timeouts and transport crashes should tear down the cached connection so
       // the daemon (or direct runtime) can relaunch the MCP server on the next attempt.
-      await this.resetConnectionOnError(server, error);
+      await this.resetConnectionOnError(server, error, context);
       throw error;
     }
   }
 
   // listResources delegates to the MCP resources/list method with passthrough params.
-  async listResources(server: string, options: Partial = {}): Promise {
+  async listResources(server: string, options: ListResourcesOptions = {}): Promise {
+    const { allowCachedAuth, disableOAuth, oauthSessionOptions, ...params } = options;
+    let context: ClientContext | undefined;
     try {
-      const { client } = await this.connect(server);
-      return await client.listResources(options as ListResourcesRequest['params']);
+      const effectiveDisableOAuth = this.effectiveDisableOAuthForOperation(server, disableOAuth);
+      context = await this.connect(server, {
+        allowCachedAuth: this.effectiveAllowCachedAuthForOperation(
+          server,
+          allowCachedAuth,
+          effectiveDisableOAuth,
+          undefined
+        ),
+        oauthSessionOptions,
+        disableOAuth: effectiveDisableOAuth,
+      });
+      const { client } = context;
+      return await client.listResources(params as ListResourcesRequest['params']);
     } catch (error) {
       // Fatal listResources errors usually mean the underlying transport has gone away.
-      await this.resetConnectionOnError(server, error);
+      await this.resetConnectionOnError(server, error, context);
       throw error;
     }
   }
 
-  async readResource(server: string, uri: string): Promise {
+  async readResource(server: string, uri: string, options: ReadResourceOptions = {}): Promise {
+    let context: ClientContext | undefined;
     try {
-      const { client } = await this.connect(server);
+      const effectiveDisableOAuth = this.effectiveDisableOAuthForOperation(server, options.disableOAuth);
+      context = await this.connect(server, {
+        allowCachedAuth: this.effectiveAllowCachedAuthForOperation(
+          server,
+          options.allowCachedAuth,
+          effectiveDisableOAuth,
+          undefined
+        ),
+        oauthSessionOptions: options.oauthSessionOptions,
+        disableOAuth: effectiveDisableOAuth,
+      });
+      const { client } = context;
       return await client.readResource({ uri } satisfies ReadResourceRequest['params']);
     } catch (error) {
-      await this.resetConnectionOnError(server, error);
+      await this.resetConnectionOnError(server, error, context);
       throw error;
     }
   }
 
+  private effectiveDisableOAuthForOperation(server: string, requested: boolean | undefined): boolean | undefined {
+    if (requested !== undefined) {
+      return requested;
+    }
+    const cached = this.cachedEntriesForServer(server);
+    const active = this.activeClientForServer(server);
+    if (active) {
+      return active.disableOAuth;
+    }
+    if (cached.length === 0) {
+      return undefined;
+    }
+    const [first] = cached;
+    return cached.every((entry) => entry.disableOAuth === first?.disableOAuth) ? first?.disableOAuth : undefined;
+  }
+
+  private effectiveAllowCachedAuthForOperation(
+    server: string,
+    requested: boolean | undefined,
+    disableOAuth: boolean | undefined,
+    defaultValue: boolean | undefined
+  ): boolean | undefined {
+    if (requested !== undefined) {
+      return requested;
+    }
+    if (disableOAuth !== true) {
+      return defaultValue;
+    }
+    const active = this.activeClientForServer(server);
+    if (active?.disableOAuth === true) {
+      return active.allowCachedAuth;
+    }
+    const cached = this.cachedEntriesForServer(server).filter((entry) => entry.disableOAuth);
+    return cached.length === 1 ? cached[0]?.allowCachedAuth : defaultValue;
+  }
+
+  private cachedEntriesForServer(server: string): CachedClientEntry[] {
+    const normalized = server.trim();
+    return [...this.clients.values()].filter((entry) => entry.server === normalized);
+  }
+
+  private retireCachedEntriesForServer(server: string): void {
+    const normalized = server.trim();
+    const retired: CachedClientEntry[] = [];
+    for (const [key, cached] of this.clients.entries()) {
+      if (cached.server === normalized) {
+        this.clients.delete(key);
+        retired.push(cached);
+      }
+    }
+    this.activeClientKeys.delete(normalized);
+    if (retired.length > 0) {
+      const retirement = this.trackRetirement(normalized, this.closeCachedEntries(retired));
+      void retirement.catch((error) => {
+        const detail = error instanceof Error ? error.message : String(error);
+        this.logger.warn(`Failed to close retired '${normalized}' connection: ${detail}`);
+      });
+    }
+  }
+
+  private activeClientForServer(server: string): CachedClientEntry | undefined {
+    const normalized = server.trim();
+    const activeKey = this.activeClientKeys.get(normalized);
+    if (!activeKey) {
+      return undefined;
+    }
+    const active = this.clients.get(activeKey);
+    return active?.server === normalized ? active : undefined;
+  }
+
+  private serverGeneration(server: string): number {
+    return this.serverGenerations.get(server.trim()) ?? 0;
+  }
+
+  private bumpServerGeneration(server: string): void {
+    const normalized = server.trim();
+    this.serverGenerations.set(normalized, this.serverGeneration(normalized) + 1);
+  }
+
+  private bumpAllServerGenerations(): void {
+    const servers = new Set([
+      ...this.definitions.keys(),
+      ...[...this.clients.values()].map((entry) => entry.server),
+      ...this.connectionSetupTails.keys(),
+    ]);
+    for (const server of servers) {
+      this.bumpServerGeneration(server);
+    }
+  }
+
   // connect lazily instantiates a client context per server and memoizes it.
   async connect(server: string, options: ConnectOptions = {}): Promise {
     // Reuse cached connections unless the caller explicitly opted out.
     const normalized = server.trim();
-
+    let definition = this.definitions.get(normalized);
+    if (!definition) {
+      throw new Error(`Unknown MCP server '${normalized}'.`);
+    }
+    const generation = this.serverGeneration(normalized);
+
+    // `maxOAuthAttempts: 0` keeps its legacy escape-the-cache contract.
+    // `disableOAuth: true` is the cache-friendly OAuth-suppression knob:
+    // it disables the interactive OAuth flow at the transport layer but
+    // participates in caching (own slot, see the eviction rule below).
+    const disableOAuth = options.disableOAuth === true;
+    // Normalize: a caller asking for `disableOAuth: true` has no path to
+    // OAuth, so cached-token application is the only auth they can ever
+    // use — default `allowCachedAuth: true` when the caller didn't pick
+    // a side. Without this, the documented headless setup
+    // `connect(server, { disableOAuth: true })` stored
+    // `allowCachedAuth: undefined`, and the next internal `callTool` /
+    // `listTools` (which force `allowCachedAuth: true`) immediately
+    // evicted and reopened the transport. Explicit `false` is honored
+    // (header-only / anonymous callers).
+    const effectiveAllowCachedAuth = options.allowCachedAuth ?? (disableOAuth ? true : undefined);
     const useCache = options.skipCache !== true && options.maxOAuthAttempts === undefined;
+    let ignoresAuthCachePolicy = this.ignoresAuthCachePolicy(definition);
+    let cacheAllowCachedAuth = ignoresAuthCachePolicy ? undefined : effectiveAllowCachedAuth;
+    let cacheDisableOAuth = ignoresAuthCachePolicy ? false : disableOAuth;
+    let cacheKey = this.cacheKey(normalized, cacheAllowCachedAuth, cacheDisableOAuth);
 
     if (useCache) {
-      const existing = this.clients.get(normalized);
+      const existing = this.findCachedEntryForRequest(
+        normalized,
+        definition,
+        ignoresAuthCachePolicy ? undefined : options.allowCachedAuth,
+        cacheAllowCachedAuth,
+        cacheDisableOAuth
+      );
       if (existing) {
-        if (existing.allowCachedAuth === options.allowCachedAuth || options.allowCachedAuth === undefined) {
-          return existing.promise;
+        const [existingKey, cached] = existing;
+        const activeEntry = ignoresAuthCachePolicy
+          ? {
+              ...cached,
+              allowCachedAuth: effectiveAllowCachedAuth,
+              disableOAuth,
+            }
+          : cached;
+        if (activeEntry !== cached) {
+          this.clients.set(existingKey, activeEntry);
         }
-        await this.close(normalized).catch(() => {});
+        this.activeClientKeys.set(normalized, existingKey);
+        return activeEntry.promise;
       }
     }
 
-    const definition = this.definitions.get(normalized);
-    if (!definition) {
-      throw new Error(`Unknown MCP server '${normalized}'.`);
+    let releaseConnectionSetup: (() => void) | undefined;
+    if (useCache && this.shouldSerializeConnectionSetup(definition, disableOAuth)) {
+      releaseConnectionSetup = await this.enterConnectionSetup(normalized);
+      try {
+        if (this.serverGeneration(normalized) !== generation) {
+          throw new Error(`Connection setup for MCP server '${normalized}' was superseded.`);
+        }
+        const refreshedDefinition = this.definitions.get(normalized);
+        if (!refreshedDefinition) {
+          throw new Error(`Unknown MCP server '${normalized}'.`);
+        }
+        definition = refreshedDefinition;
+        ignoresAuthCachePolicy = this.ignoresAuthCachePolicy(definition);
+        cacheAllowCachedAuth = ignoresAuthCachePolicy ? undefined : effectiveAllowCachedAuth;
+        cacheDisableOAuth = ignoresAuthCachePolicy ? false : disableOAuth;
+        cacheKey = this.cacheKey(normalized, cacheAllowCachedAuth, cacheDisableOAuth);
+        const existing = this.findCachedEntryForRequest(
+          normalized,
+          definition,
+          ignoresAuthCachePolicy ? undefined : options.allowCachedAuth,
+          cacheAllowCachedAuth,
+          cacheDisableOAuth
+        );
+        if (existing) {
+          releaseConnectionSetup();
+          releaseConnectionSetup = undefined;
+          const [existingKey, cached] = existing;
+          this.activeClientKeys.set(normalized, existingKey);
+          return cached.promise;
+        }
+        await this.retireConflictingOAuthEntries(normalized, cacheKey);
+        if (this.serverGeneration(normalized) !== generation) {
+          throw new Error(`Connection setup for MCP server '${normalized}' was superseded.`);
+        }
+        const latestDefinition = this.definitions.get(normalized);
+        if (!latestDefinition) {
+          throw new Error(`Unknown MCP server '${normalized}'.`);
+        }
+        definition = latestDefinition;
+      } catch (error) {
+        releaseConnectionSetup?.();
+        releaseConnectionSetup = undefined;
+        throw error;
+      }
     }
 
-    const connection = createClientContext(definition, this.logger, this.clientInfo, {
+    let connectionDefinition = definition;
+    let contextPromise = createClientContext(definition, this.logger, this.clientInfo, {
       maxOAuthAttempts: options.maxOAuthAttempts,
       oauthTimeoutMs: this.oauthTimeoutMs ?? OAUTH_CODE_TIMEOUT_MS,
-      onDefinitionPromoted: (promoted) => this.definitions.set(promoted.name, promoted),
-      allowCachedAuth: options.allowCachedAuth,
+      onDefinitionPromoted: (promoted) => {
+        if (
+          this.serverGeneration(normalized) === generation &&
+          this.definitions.get(normalized) === connectionDefinition
+        ) {
+          this.definitions.set(promoted.name, promoted);
+          connectionDefinition = promoted;
+        }
+      },
+      allowCachedAuth: effectiveAllowCachedAuth,
       oauthSessionOptions: options.oauthSessionOptions,
+      disableOAuth,
       recordPath: this.recordPath,
       replayPath: this.replayPath,
     });
 
     if (useCache) {
-      this.clients.set(normalized, { promise: connection, allowCachedAuth: options.allowCachedAuth });
+      const previousActiveKey = this.activeClientKeys.get(normalized);
+      contextPromise = contextPromise.then((context) => {
+        this.contextCacheKeys.set(context, cacheKey);
+        this.contextCachePromises.set(context, contextPromise);
+        return context;
+      });
+      let connection!: Promise;
+      connection = contextPromise.then((context) => {
+        const stillCached = this.clients.get(cacheKey)?.promise === connection;
+        if (this.serverGeneration(normalized) !== generation || !stillCached) {
+          this.contextCacheKeys.delete(context);
+          this.contextCachePromises.delete(context);
+          throw new Error(`Connection setup for MCP server '${normalized}' was superseded.`);
+        }
+        return context;
+      });
+      this.activeClientKeys.set(normalized, cacheKey);
+      this.clients.set(cacheKey, {
+        server: normalized,
+        promise: connection,
+        contextPromise,
+        allowCachedAuth: ignoresAuthCachePolicy ? effectiveAllowCachedAuth : cacheAllowCachedAuth,
+        disableOAuth: ignoresAuthCachePolicy ? disableOAuth : cacheDisableOAuth,
+      });
       try {
         return await connection;
       } catch (error) {
-        this.clients.delete(normalized);
+        const ownsCacheEntry = this.clients.get(cacheKey)?.promise === connection;
+        if (ownsCacheEntry) {
+          this.clients.delete(cacheKey);
+          if (
+            this.activeClientKeys.get(normalized) === cacheKey &&
+            previousActiveKey &&
+            this.clients.has(previousActiveKey)
+          ) {
+            this.activeClientKeys.set(normalized, previousActiveKey);
+          } else if (
+            this.activeClientKeys.get(normalized) === cacheKey ||
+            this.cachedEntriesForServer(normalized).length === 0
+          ) {
+            this.activeClientKeys.delete(normalized);
+          }
+        }
         throw error;
+      } finally {
+        releaseConnectionSetup?.();
       }
     }
 
-    return connection;
+    releaseConnectionSetup?.();
+    return contextPromise;
   }
 
   // close tears down transports (and OAuth sessions) for a single server or all servers.
   async close(server?: string): Promise {
     if (server) {
       const normalized = server.trim();
-      const cached = this.clients.get(normalized);
-      if (!cached) {
-        return;
+      this.bumpServerGeneration(normalized);
+      const entries = [...this.clients.entries()].filter(([, cached]) => cached.server === normalized);
+      if (entries.length === 0) {
+        this.activeClientKeys.delete(normalized);
       }
-      const context = await cached.promise;
-      try {
-        await this.closeContext(context);
-      } finally {
-        this.clients.delete(normalized);
+      for (const [key] of entries) {
+        this.clients.delete(key);
       }
+      this.activeClientKeys.delete(normalized);
+      if (entries.length > 0) {
+        void this.trackRetirement(normalized, this.closeCachedEntries(entries.map(([, cached]) => cached)));
+      }
+      await this.awaitRetirements(normalized);
       return;
     }
 
-    for (const [name, cached] of this.clients.entries()) {
-      try {
-        const context = await cached.promise;
-        await this.closeContext(context);
-      } finally {
-        this.clients.delete(name);
-      }
+    this.bumpAllServerGenerations();
+    const entries = [...this.clients.entries()];
+    this.clients.clear();
+    this.activeClientKeys.clear();
+    const byServer = new Map();
+    for (const [, cached] of entries) {
+      const serverEntries = byServer.get(cached.server) ?? [];
+      serverEntries.push(cached);
+      byServer.set(cached.server, serverEntries);
+    }
+    for (const [serverName, serverEntries] of byServer) {
+      void this.trackRetirement(serverName, this.closeCachedEntries(serverEntries));
+    }
+    await this.awaitRetirements();
+  }
+
+  private contextPromiseFor(cached: CachedClientEntry): Promise {
+    return cached.contextPromise ?? cached.promise;
+  }
+
+  private async closeCachedEntries(entries: CachedClientEntry[]): Promise {
+    const results = await Promise.allSettled(
+      entries.map(async (cached) => {
+        const context = await this.contextPromiseFor(cached);
+        try {
+          await this.closeContext(context);
+        } finally {
+          this.contextCacheKeys.delete(context);
+          this.contextCachePromises.delete(context);
+        }
+      })
+    );
+    const firstFailure = results.find((result): result is PromiseRejectedResult => result.status === 'rejected');
+    if (firstFailure) {
+      throw firstFailure.reason;
     }
   }
 
@@ -392,23 +738,165 @@ class McpRuntime implements Runtime {
     }
   }
 
-  private async resetConnectionOnError(server: string, error: unknown): Promise {
+  private async resetConnectionOnError(server: string, error: unknown, failedContext?: ClientContext): Promise {
     if (!shouldResetConnection(error)) {
       return;
     }
     const normalized = server.trim();
-    if (!this.clients.has(normalized)) {
+    if (!failedContext) {
       return;
     }
     try {
-      // Reuse the existing close() helper so transport shutdown stays consistent with
-      // normal runtime disposal (wait for STDIO children, close OAuth sessions, etc.).
-      await this.close(normalized);
+      const failedKey = this.contextCacheKeys.get(failedContext);
+      const failedEntry = failedKey ? this.clients.get(failedKey) : undefined;
+      const failedContextPromise = this.contextCachePromises.get(failedContext);
+      if (
+        !failedKey ||
+        failedEntry?.server !== normalized ||
+        !failedContextPromise ||
+        this.contextPromiseFor(failedEntry) !== failedContextPromise
+      ) {
+        return;
+      }
+      if (this.clients.get(failedKey)?.promise !== failedEntry.promise) {
+        return;
+      }
+      this.clients.delete(failedKey);
+      if (this.activeClientKeys.get(normalized) === failedKey || this.cachedEntriesForServer(normalized).length === 0) {
+        this.activeClientKeys.delete(normalized);
+      }
+      try {
+        await this.closeContext(failedContext);
+      } finally {
+        this.contextCacheKeys.delete(failedContext);
+        this.contextCachePromises.delete(failedContext);
+      }
     } catch (closeError) {
       const detail = closeError instanceof Error ? closeError.message : String(closeError);
       this.logger.warn(`Failed to reset '${normalized}' after error: ${detail}`);
     }
   }
+
+  private findCachedEntryForRequest(
+    server: string,
+    definition: ServerDefinition,
+    requestedAllowCachedAuth: boolean | undefined,
+    effectiveAllowCachedAuth: boolean | undefined,
+    disableOAuth: boolean
+  ): [string, CachedClientEntry] | undefined {
+    const exactKey = this.cacheKey(server, effectiveAllowCachedAuth, disableOAuth);
+    if (this.ignoresAuthCachePolicy(definition)) {
+      const exact = this.clients.get(exactKey);
+      return exact ? [exactKey, exact] : undefined;
+    }
+    if (requestedAllowCachedAuth !== undefined) {
+      const exact = this.clients.get(exactKey);
+      return exact ? [exactKey, exact] : undefined;
+    }
+
+    const activeKey = this.activeClientKeys.get(server);
+    const active = activeKey ? this.clients.get(activeKey) : undefined;
+    const policyMatches = (cached: CachedClientEntry) =>
+      effectiveAllowCachedAuth === undefined || cached.allowCachedAuth === effectiveAllowCachedAuth;
+    if (activeKey && active?.server === server && active.disableOAuth === disableOAuth && policyMatches(active)) {
+      return [activeKey, active];
+    }
+
+    const matches = [...this.clients.entries()].filter(
+      ([, cached]) => cached.server === server && cached.disableOAuth === disableOAuth && policyMatches(cached)
+    );
+    if (matches.length === 1) {
+      return matches[0];
+    }
+
+    const exact = this.clients.get(exactKey);
+    return exact ? [exactKey, exact] : undefined;
+  }
+
+  private async retireConflictingOAuthEntries(server: string, keepKey: string): Promise {
+    const conflicting = [...this.clients.entries()].filter(
+      ([key, cached]) => key !== keepKey && cached.server === server && !cached.disableOAuth
+    );
+    if (conflicting.length === 0) {
+      return;
+    }
+    for (const [key] of conflicting) {
+      this.clients.delete(key);
+      if (this.activeClientKeys.get(server) === key) {
+        this.activeClientKeys.delete(server);
+      }
+    }
+    await this.trackRetirement(server, this.closeCachedEntries(conflicting.map(([, cached]) => cached)));
+  }
+
+  private shouldSerializeConnectionSetup(definition: ServerDefinition, disableOAuth: boolean): boolean {
+    return definition.command.kind === 'http' && !disableOAuth && !this.ignoresAuthCachePolicy(definition);
+  }
+
+  private ignoresAuthCachePolicy(definition: ServerDefinition): boolean {
+    const replayServer = process.env.MCPORTER_REPLAY_SERVER;
+    const replaysDefinition = Boolean(this.replayPath) && (!replayServer || replayServer === definition.name);
+    return definition.command.kind === 'stdio' || replaysDefinition;
+  }
+
+  private trackRetirement(server: string, retirement: Promise): Promise {
+    const pending = this.retirementPromises.get(server) ?? new Set>();
+    pending.add(retirement);
+    this.retirementPromises.set(server, pending);
+    const cleanup = () => {
+      pending.delete(retirement);
+      if (pending.size === 0) {
+        this.retirementPromises.delete(server);
+      }
+    };
+    retirement.then(cleanup, cleanup);
+    return retirement;
+  }
+
+  private async awaitRetirements(server?: string): Promise {
+    const pending = server ? [...(this.retirementPromises.get(server) ?? [])] : [];
+    if (!server) {
+      for (const retirements of this.retirementPromises.values()) {
+        pending.push(...retirements);
+      }
+    }
+    const results = await Promise.allSettled(pending);
+    const firstFailure = results.find((result): result is PromiseRejectedResult => result.status === 'rejected');
+    if (firstFailure) {
+      throw firstFailure.reason;
+    }
+  }
+
+  private async enterConnectionSetup(server: string): Promise<() => void> {
+    const previous = this.connectionSetupTails.get(server) ?? Promise.resolve();
+    let releaseCurrent!: () => void;
+    const current = new Promise((resolve) => {
+      releaseCurrent = resolve;
+    });
+    const tail = previous.catch(() => {}).then(() => current);
+    this.connectionSetupTails.set(server, tail);
+    await previous.catch(() => {});
+
+    let released = false;
+    return () => {
+      if (released) {
+        return;
+      }
+      released = true;
+      releaseCurrent();
+      void tail.finally(() => {
+        if (this.connectionSetupTails.get(server) === tail) {
+          this.connectionSetupTails.delete(server);
+        }
+      });
+    };
+  }
+
+  private cacheKey(server: string, allowCachedAuth: boolean | undefined, disableOAuth: boolean): string {
+    const cachedAuthKey =
+      allowCachedAuth === true ? 'cached-auth-on' : allowCachedAuth === false ? 'cached-auth-off' : 'cached-auth-unset';
+    return `${server}\u0000oauth-disabled:${disableOAuth ? '1' : '0'}\u0000${cachedAuthKey}`;
+  }
 }
 
 // createConsoleLogger produces the default runtime logger honoring MCPORTER_LOG_LEVEL.
diff --git a/src/runtime/transport.ts b/src/runtime/transport.ts
index 339fc89a..4befebab 100644
--- a/src/runtime/transport.ts
+++ b/src/runtime/transport.ts
@@ -86,6 +86,14 @@ export interface CreateClientContextOptions {
   readonly onDefinitionPromoted?: (definition: ServerDefinition) => void;
   readonly allowCachedAuth?: boolean;
   readonly oauthSessionOptions?: OAuthSessionOptions;
+  /**
+   * When `true`, suppress the interactive OAuth flow entirely. See
+   * `ConnectOptions.disableOAuth` in `runtime.ts` for the caller-facing
+   * semantics. Internally this short-circuits `shouldEstablishOAuth` and
+   * `maybePromoteHttpDefinition` so the unauthorized-fallback path
+   * cannot re-enable OAuth on a daemon-shaped caller.
+   */
+  readonly disableOAuth?: boolean;
   readonly recordPath?: string;
   readonly replayPath?: string;
 }
@@ -188,7 +196,11 @@ function maybePromoteHttpDefinition(
   logger: Logger,
   options: CreateClientContextOptions
 ): ServerDefinition | undefined {
-  if (options.maxOAuthAttempts === 0) {
+  // Both flags suppress promotion-to-OAuth on a 401 fallback. Without
+  // this guard, a daemon-mode caller hitting an unauthorized response
+  // could trigger `maybeEnableOAuth` and effectively re-enable OAuth
+  // on the next attempt — defeating the no-browser-launch contract.
+  if (options.maxOAuthAttempts === 0 || options.disableOAuth === true) {
     return undefined;
   }
   return maybeEnableOAuth(definition, logger);
@@ -355,7 +367,8 @@ async function attemptHttpClientContext(
     throw new Error(`Server '${activeDefinition.name}' is not configured for HTTP transport.`);
   }
   let oauthSession: OAuthSession | undefined;
-  const shouldEstablishOAuth = activeDefinition.auth === 'oauth' && options.maxOAuthAttempts !== 0;
+  const shouldEstablishOAuth =
+    activeDefinition.auth === 'oauth' && options.maxOAuthAttempts !== 0 && options.disableOAuth !== true;
   if (shouldEstablishOAuth) {
     oauthSession = await createOAuthSession(activeDefinition, logger, options.oauthSessionOptions);
   }
diff --git a/src/server-proxy.ts b/src/server-proxy.ts
index dc8d3699..e351fe1f 100644
--- a/src/server-proxy.ts
+++ b/src/server-proxy.ts
@@ -17,7 +17,16 @@ type ToolSchemaInfo = {
   propertySet: Set;
 };
 
-const KNOWN_OPTION_KEYS = new Set(['tailLog', 'timeout', 'stream', 'streamLog', 'mimeType', 'metadata', 'log']);
+const KNOWN_OPTION_KEYS = new Set([
+  'disableOAuth',
+  'tailLog',
+  'timeout',
+  'stream',
+  'streamLog',
+  'mimeType',
+  'metadata',
+  'log',
+]);
 
 export interface ServerProxyOptions {
   readonly mapPropertyToTool?: (property: string | symbol) => string;
@@ -43,6 +52,51 @@ function isPlainObject(value: unknown): value is Record {
   return typeof value === 'object' && value !== null && !Array.isArray(value);
 }
 
+function isProxyOptionKey(key: string): boolean {
+  return key === 'args' || KNOWN_OPTION_KEYS.has(key);
+}
+
+function inferMetadataOptions(callArgs: unknown[]): {
+  options: { autoAuthorize?: false; disableOAuth?: boolean };
+  optionObjects: Set>;
+} {
+  const options: { autoAuthorize?: false; disableOAuth?: boolean } = {};
+  const optionObjects = new Set>();
+
+  for (const [index, arg] of callArgs.entries()) {
+    if (!isPlainObject(arg) || arg.disableOAuth !== true) {
+      continue;
+    }
+    const keys = Object.keys(arg);
+    const isOptionsOnlyObject = keys.length > 0 && keys.every(isProxyOptionKey);
+    const hasClearlySeparateToolArgs = callArgs.some((other, otherIndex) => {
+      if (otherIndex === index) {
+        return false;
+      }
+      if (!isPlainObject(other)) {
+        return false;
+      }
+      return Object.hasOwn(other, 'args') || Object.keys(other).some((key) => !isProxyOptionKey(key));
+    });
+    // `args` plus proxy options is reserved envelope syntax; use proxy.call()
+    // when a tool schema itself owns both `args` and `disableOAuth`.
+    const hasExplicitArgsEnvelope = Object.hasOwn(arg, 'args');
+    // A sole object can be a tool argument whose schema owns `disableOAuth`.
+    // Multi-argument calls suppress discovery defensively, then let the schema
+    // classify option-only objects unless another argument is clearly tool input.
+    const isUnambiguousOptionsObject = isOptionsOnlyObject && (hasClearlySeparateToolArgs || hasExplicitArgsEnvelope);
+    if (isUnambiguousOptionsObject) {
+      options.disableOAuth = true;
+    } else if (isOptionsOnlyObject && callArgs.length > 1 && options.disableOAuth !== true) {
+      options.autoAuthorize = false;
+    }
+    if (isUnambiguousOptionsObject) {
+      optionObjects.add(arg);
+    }
+  }
+  return { options, optionObjects };
+}
+
 // createToolSchemaInfo normalizes schema metadata used for argument mapping.
 function createToolSchemaInfo(schemaRaw: unknown): ToolSchemaInfo | undefined {
   if (!schemaRaw || typeof schemaRaw !== 'object') {
@@ -145,7 +199,7 @@ export function createServerProxy(
   const toolSchemaCache = new Map();
   const persistedSchemas = new Map>();
   const toolAliasMap = new Map();
-  let schemaFetch: Promise | null = null;
+  const schemaFetches = new Map>();
   let diskLoad: Promise | null = null;
   let persistPromise: Promise | null = null;
   let refreshPending = false;
@@ -184,7 +238,13 @@ export function createServerProxy(
   }
 
   // ensureMetadata loads schema information for the requested tool, optionally refreshing from the server.
-  async function ensureMetadata(toolName: string): Promise {
+  // Unambiguous proxy options use cache-friendly OAuth suppression. Ambiguous
+  // option-shaped arguments use an uncached no-authorize fetch so discovery
+  // cannot launch OAuth or change the runtime's active connection posture.
+  async function ensureMetadata(
+    toolName: string,
+    metadataOptions: { autoAuthorize?: false; disableOAuth?: boolean } = {}
+  ): Promise {
     await consumePersist();
     const cached = toolSchemaCache.get(toolName);
     if (cached && !refreshPending) {
@@ -202,9 +262,28 @@ export function createServerProxy(
       }
     }
 
+    const disableOAuth = metadataOptions.disableOAuth === true;
+    const schemaFetchKey = disableOAuth
+      ? 'disable-oauth'
+      : metadataOptions.autoAuthorize === false
+        ? 'no-authorize'
+        : 'default';
+    let schemaFetch = schemaFetches.get(schemaFetchKey);
     if (!schemaFetch) {
+      const listToolsOptions: {
+        includeSchema: true;
+        autoAuthorize?: false;
+        disableOAuth?: boolean;
+      } = {
+        includeSchema: true,
+      };
+      if (disableOAuth) {
+        listToolsOptions.disableOAuth = true;
+      } else if (metadataOptions.autoAuthorize === false) {
+        listToolsOptions.autoAuthorize = false;
+      }
       schemaFetch = runtime
-        .listTools(serverName, { includeSchema: true })
+        .listTools(serverName, listToolsOptions)
         .then((tools) => {
           for (const tool of tools) {
             if (!tool.inputSchema || typeof tool.inputSchema !== 'object') {
@@ -216,9 +295,12 @@ export function createServerProxy(
           refreshPending = false;
         })
         .catch((error) => {
-          schemaFetch = null;
+          if (schemaFetches.get(schemaFetchKey) === schemaFetch) {
+            schemaFetches.delete(schemaFetchKey);
+          }
           throw error;
         });
+      schemaFetches.set(schemaFetchKey, schemaFetch);
     }
 
     await schemaFetch;
@@ -301,9 +383,11 @@ export function createServerProxy(
           : mapPropertyToTool(propertyKey);
 
       return async (...callArgs: unknown[]) => {
+        const { options: metadataOptions, optionObjects } = inferMetadataOptions(callArgs);
+
         let schemaInfo: ToolSchemaInfo | undefined;
         try {
-          schemaInfo = await ensureMetadata(resolvedToolName);
+          schemaInfo = await ensureMetadata(resolvedToolName, metadataOptions);
         } catch {
           schemaInfo = undefined;
         }
@@ -312,7 +396,7 @@ export function createServerProxy(
           if (alias && alias !== resolvedToolName) {
             resolvedToolName = alias;
             try {
-              schemaInfo = await ensureMetadata(resolvedToolName);
+              schemaInfo = await ensureMetadata(resolvedToolName, metadataOptions);
             } catch {
               // ignore and keep prior schema if available
             }
@@ -327,6 +411,7 @@ export function createServerProxy(
           if (isPlainObject(arg)) {
             const keys = Object.keys(arg);
             const treatAsArgs =
+              !optionObjects.has(arg) &&
               schemaInfo !== undefined &&
               keys.length > 0 &&
               (keys.every((key) => schemaInfo.propertySet.has(key)) ||
diff --git a/tests/call-arguments.test.ts b/tests/call-arguments.test.ts
index 9373b9b6..2ae27868 100644
--- a/tests/call-arguments.test.ts
+++ b/tests/call-arguments.test.ts
@@ -175,6 +175,12 @@ describe('parseCallArguments', () => {
     expect(parsed.positionalArgs).toEqual(['123']);
   });
 
+  it('captures --no-oauth as a runtime flag instead of a tool argument', () => {
+    const parsed = parseCallArguments(['server.tool', '--no-oauth', 'limit=5']);
+    expect(parsed.disableOAuth).toBe(true);
+    expect(parsed.args).toEqual({ limit: 5 });
+  });
+
   it('captures --save-images output directory', () => {
     const parsed = parseCallArguments(['--save-images', './tmp/images', 'server.tool']);
     expect(parsed.saveImagesDir).toBe('./tmp/images');
diff --git a/tests/cli-call-execution.test.ts b/tests/cli-call-execution.test.ts
index be55aca7..32569e1d 100644
--- a/tests/cli-call-execution.test.ts
+++ b/tests/cli-call-execution.test.ts
@@ -86,6 +86,7 @@ describe('CLI call execution behavior', () => {
       autoAuthorize: true,
       includeSchema: true,
       allowCachedAuth: true,
+      disableOAuth: undefined,
     });
     logSpy.mockRestore();
   });
@@ -125,6 +126,7 @@ describe('CLI call execution behavior', () => {
       autoAuthorize: true,
       includeSchema: true,
       allowCachedAuth: true,
+      disableOAuth: undefined,
     });
     logSpy.mockRestore();
   });
@@ -338,6 +340,7 @@ describe('CLI call execution behavior', () => {
       autoAuthorize: true,
       includeSchema: false,
       allowCachedAuth: true,
+      disableOAuth: undefined,
     });
 
     logSpy.mockRestore();
diff --git a/tests/cli-list-classification.test.ts b/tests/cli-list-classification.test.ts
index 58e1b8a2..b4f9121e 100644
--- a/tests/cli-list-classification.test.ts
+++ b/tests/cli-list-classification.test.ts
@@ -260,6 +260,7 @@ describe('CLI list classification and routing', () => {
     expect(listTools).toHaveBeenCalledWith('linear', {
       autoAuthorize: false,
       allowCachedAuth: true,
+      disableOAuth: false,
     });
   });
 
diff --git a/tests/cli-list-flags.test.ts b/tests/cli-list-flags.test.ts
index 66b451d9..20762702 100644
--- a/tests/cli-list-flags.test.ts
+++ b/tests/cli-list-flags.test.ts
@@ -19,6 +19,7 @@ describe('CLI list flag parsing', () => {
       quiet: false,
       exitCode: false,
       statusOnly: false,
+      disableOAuth: false,
     });
     expect(args).toEqual(['server']);
   });
@@ -39,10 +40,19 @@ describe('CLI list flag parsing', () => {
       quiet: false,
       exitCode: false,
       statusOnly: false,
+      disableOAuth: false,
     });
     expect(args).toEqual(['server']);
   });
 
+  it('parses --no-oauth and removes it from args', async () => {
+    const { extractListFlags } = await cliModulePromise;
+    const args = ['--no-oauth', 'server'];
+    const flags = extractListFlags(args);
+    expect(flags.disableOAuth).toBe(true);
+    expect(args).toEqual(['server']);
+  });
+
   it('parses --json flag and removes it from args', async () => {
     const { extractListFlags } = await cliModulePromise;
     const args = ['--json', 'server'];
diff --git a/tests/cli-resource-command.test.ts b/tests/cli-resource-command.test.ts
index 489e02f2..b0e095f5 100644
--- a/tests/cli-resource-command.test.ts
+++ b/tests/cli-resource-command.test.ts
@@ -53,6 +53,17 @@ describe('handleResource', () => {
     }
   });
 
+  it('passes disableOAuth to resource helpers when requested', async () => {
+    const runtime = createRuntime();
+    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
+    try {
+      await handleResource(runtime, ['docs', 'memo://one', '--no-oauth']);
+      expect(runtime.readResource).toHaveBeenCalledWith('docs', 'memo://one', { disableOAuth: true });
+    } finally {
+      logSpy.mockRestore();
+    }
+  });
+
   it('prints structured JSON for resource listing failures', async () => {
     const runtime = createRuntime();
     runtime.listResources.mockRejectedValue(new Error('MCP error -32601: Method not found'));
diff --git a/tests/daemon-host.test.ts b/tests/daemon-host.test.ts
index 1d4be60d..93cf0c63 100644
--- a/tests/daemon-host.test.ts
+++ b/tests/daemon-host.test.ts
@@ -49,6 +49,7 @@ describe('daemon host request handling', () => {
     expect(runtime.callTool).toHaveBeenCalledWith('oauth', 'ping', {
       args: {},
       timeoutMs: undefined,
+      disableOAuth: false,
     });
 
     await __testProcessRequest('', runtime as unknown as Runtime, managedServers, new Map(), metadata, logContext, {
@@ -61,6 +62,7 @@ describe('daemon host request handling', () => {
       includeSchema: true,
       autoAuthorize: undefined,
       allowCachedAuth: true,
+      disableOAuth: false,
     });
   });
 
@@ -78,6 +80,7 @@ describe('daemon host request handling', () => {
       includeSchema: true,
       autoAuthorize: undefined,
       allowCachedAuth: true,
+      disableOAuth: false,
     });
   });
 
@@ -95,6 +98,37 @@ describe('daemon host request handling', () => {
       includeSchema: true,
       autoAuthorize: false,
       allowCachedAuth: true,
+      disableOAuth: false,
+    });
+  });
+
+  it('forwards disableOAuth on daemon callTool and listTools requests', async () => {
+    const runtime = createRuntimeDouble();
+    const managedServers = createManagedServers();
+
+    await __testProcessRequest('', runtime as unknown as Runtime, managedServers, new Map(), metadata, logContext, {
+      id: 'call',
+      method: 'callTool',
+      params: { server: 'oauth', tool: 'ping', disableOAuth: true },
+    });
+
+    expect(runtime.callTool).toHaveBeenCalledWith('oauth', 'ping', {
+      args: {},
+      timeoutMs: undefined,
+      disableOAuth: true,
+    });
+
+    await __testProcessRequest('', runtime as unknown as Runtime, managedServers, new Map(), metadata, logContext, {
+      id: 'list',
+      method: 'listTools',
+      params: { server: 'oauth', includeSchema: true, disableOAuth: true },
+    });
+
+    expect(runtime.listTools).toHaveBeenCalledWith('oauth', {
+      includeSchema: true,
+      autoAuthorize: undefined,
+      allowCachedAuth: true,
+      disableOAuth: true,
     });
   });
 
@@ -112,6 +146,7 @@ describe('daemon host request handling', () => {
       includeSchema: undefined,
       autoAuthorize: undefined,
       allowCachedAuth: false,
+      disableOAuth: false,
     });
   });
 });
diff --git a/tests/keep-alive-runtime.test.ts b/tests/keep-alive-runtime.test.ts
index b886131e..d922c061 100644
--- a/tests/keep-alive-runtime.test.ts
+++ b/tests/keep-alive-runtime.test.ts
@@ -2,7 +2,7 @@ import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
 import { describe, expect, it, vi } from 'vitest';
 import type { ServerDefinition } from '../src/config.js';
 import { createKeepAliveRuntime } from '../src/daemon/runtime-wrapper.js';
-import type { CallOptions, ListToolsOptions, Runtime } from '../src/runtime.js';
+import type { CallOptions, ConnectOptions, ListToolsOptions, Runtime } from '../src/runtime.js';
 
 class FakeRuntime implements Runtime {
   private readonly definitions: ServerDefinition[];
@@ -10,6 +10,7 @@ class FakeRuntime implements Runtime {
   public readonly listToolsMock = vi.fn().mockResolvedValue([{ name: 'local-tool' }]);
   public readonly listResourcesMock = vi.fn().mockResolvedValue([]);
   public readonly readResourceMock = vi.fn().mockResolvedValue({ contents: [] });
+  public readonly connectMock = vi.fn().mockResolvedValue({ client: {}, transport: {}, definition: {} });
   public readonly closeMock = vi.fn().mockResolvedValue(undefined);
 
   constructor(definitions: ServerDefinition[]) {
@@ -56,8 +57,8 @@ class FakeRuntime implements Runtime {
     return await this.readResourceMock(server, uri);
   }
 
-  async connect(): Promise {
-    throw new Error('not implemented');
+  async connect(server: string, options?: ConnectOptions): Promise>> {
+    return await this.connectMock(server, options);
   }
 
   async close(server?: string): Promise {
@@ -102,6 +103,7 @@ describe('createKeepAliveRuntime', () => {
       tool: 'ping',
       args: { value: 1 },
       timeoutMs: 4_200,
+      disableOAuth: undefined,
     });
 
     await keepAliveRuntime.listTools('alpha', { includeSchema: true });
@@ -110,6 +112,7 @@ describe('createKeepAliveRuntime', () => {
       includeSchema: true,
       autoAuthorize: undefined,
       allowCachedAuth: true,
+      disableOAuth: undefined,
     });
 
     await keepAliveRuntime.listTools('alpha', { allowCachedAuth: false });
@@ -118,15 +121,26 @@ describe('createKeepAliveRuntime', () => {
       includeSchema: undefined,
       autoAuthorize: undefined,
       allowCachedAuth: false,
+      disableOAuth: undefined,
     });
 
     await keepAliveRuntime.listResources('alpha', { cursor: '1' });
-    expect(daemon.listResources).toHaveBeenCalledWith({ server: 'alpha', params: { cursor: '1' } });
+    expect(daemon.listResources).toHaveBeenCalledWith({
+      server: 'alpha',
+      params: { cursor: '1' },
+      allowCachedAuth: undefined,
+      disableOAuth: undefined,
+    });
 
     await expect(keepAliveRuntime.readResource('alpha', 'memo://1')).resolves.toEqual({
       contents: [{ uri: 'memo://1', text: 'daemon-resource' }],
     });
-    expect(daemon.readResource).toHaveBeenCalledWith({ server: 'alpha', uri: 'memo://1' });
+    expect(daemon.readResource).toHaveBeenCalledWith({
+      server: 'alpha',
+      uri: 'memo://1',
+      allowCachedAuth: undefined,
+      disableOAuth: undefined,
+    });
 
     await keepAliveRuntime.close('alpha');
     expect(daemon.closeServer).toHaveBeenCalledWith({ server: 'alpha' });
@@ -138,6 +152,58 @@ describe('createKeepAliveRuntime', () => {
     expect(runtime.closeMock).toHaveBeenCalledWith(undefined);
   });
 
+  it('forwards disableOAuth through daemon requests and connect wrappers', async () => {
+    const runtime = new FakeRuntime(definitions);
+    const daemon = {
+      callTool: vi.fn().mockResolvedValue('daemon-call'),
+      listTools: vi.fn().mockResolvedValue([{ name: 'remote-tool' }]),
+      listResources: vi.fn().mockResolvedValue(['resource']),
+      readResource: vi.fn().mockResolvedValue({ contents: [] }),
+      closeServer: vi.fn().mockResolvedValue(undefined),
+    };
+    const keepAliveRuntime = createKeepAliveRuntime(runtime as unknown as Runtime, {
+      daemonClient: daemon as never,
+      keepAliveServers: new Set(['alpha']),
+    });
+
+    await keepAliveRuntime.callTool('alpha', 'ping', { disableOAuth: true });
+    expect(daemon.callTool).toHaveBeenCalledWith({
+      server: 'alpha',
+      tool: 'ping',
+      args: undefined,
+      timeoutMs: undefined,
+      disableOAuth: true,
+    });
+
+    await keepAliveRuntime.listTools('alpha', { disableOAuth: true });
+    expect(daemon.listTools).toHaveBeenCalledWith({
+      server: 'alpha',
+      includeSchema: undefined,
+      autoAuthorize: undefined,
+      allowCachedAuth: true,
+      disableOAuth: true,
+    });
+
+    await keepAliveRuntime.listResources('alpha', { cursor: '1', disableOAuth: true });
+    expect(daemon.listResources).toHaveBeenCalledWith({
+      server: 'alpha',
+      params: { cursor: '1' },
+      allowCachedAuth: undefined,
+      disableOAuth: true,
+    });
+
+    await keepAliveRuntime.readResource('alpha', 'memo://1', { disableOAuth: true });
+    expect(daemon.readResource).toHaveBeenCalledWith({
+      server: 'alpha',
+      uri: 'memo://1',
+      allowCachedAuth: undefined,
+      disableOAuth: true,
+    });
+
+    await keepAliveRuntime.connect('alpha', { disableOAuth: true });
+    expect(runtime.connectMock).toHaveBeenCalledWith('alpha', { disableOAuth: true });
+  });
+
   it('restarts daemon servers after fatal errors and retries the operation', async () => {
     const runtime = new FakeRuntime(definitions);
     const daemon = {
diff --git a/tests/runtime-cache-policy.test.ts b/tests/runtime-cache-policy.test.ts
new file mode 100644
index 00000000..ec501777
--- /dev/null
+++ b/tests/runtime-cache-policy.test.ts
@@ -0,0 +1,125 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+const mocks = vi.hoisted(() => ({
+  createClientContext: vi.fn(),
+}));
+
+vi.mock('../src/runtime/transport.js', () => ({
+  createClientContext: mocks.createClientContext,
+}));
+
+import type { ServerDefinition } from '../src/config.js';
+import { createRuntime } from '../src/runtime.js';
+
+type ClientContext = Awaited>['connect']>>;
+
+function fakeContext(
+  definition: ServerDefinition,
+  clientClose: ReturnType = vi.fn().mockResolvedValue(undefined)
+): ClientContext {
+  return {
+    client: {
+      close: clientClose,
+    },
+    transport: {
+      close: vi.fn().mockResolvedValue(undefined),
+    },
+    definition,
+    oauthSession: undefined,
+  } as unknown as ClientContext;
+}
+
+describe('runtime cache policy', () => {
+  beforeEach(() => {
+    mocks.createClientContext.mockReset();
+  });
+
+  afterEach(() => {
+    vi.unstubAllEnvs();
+  });
+
+  it('does not let stale OAuth promotion overwrite a replacement definition', async () => {
+    const initial: ServerDefinition = {
+      name: 'oauth',
+      command: { kind: 'http', url: new URL('https://old.example.com/mcp') },
+    };
+    let resolveConnection!: (context: ClientContext) => void;
+    let promote!: (definition: ServerDefinition) => void;
+    mocks.createClientContext.mockImplementation(
+      (
+        _definition: ServerDefinition,
+        _logger: unknown,
+        _clientInfo: unknown,
+        options: { onDefinitionPromoted?: (definition: ServerDefinition) => void }
+      ) => {
+        promote = options.onDefinitionPromoted ?? (() => {});
+        return new Promise((resolve) => {
+          resolveConnection = resolve;
+        });
+      }
+    );
+    const runtime = await createRuntime({ servers: [initial] });
+    const connecting = runtime.connect('oauth');
+    const expectation = expect(connecting).rejects.toThrow('superseded');
+    await vi.waitFor(() => expect(mocks.createClientContext).toHaveBeenCalled());
+
+    const replacement: ServerDefinition = {
+      name: 'oauth',
+      command: { kind: 'http', url: new URL('https://new.example.com/mcp') },
+    };
+    runtime.registerDefinition(replacement, { overwrite: true });
+    promote({ ...initial, auth: 'oauth' });
+    resolveConnection(fakeContext(initial));
+
+    await expectation;
+    expect(runtime.getDefinition('oauth')).toBe(replacement);
+  });
+
+  it('uses one replay client across auth posture changes', async () => {
+    vi.stubEnv('MCPORTER_REPLAY', 'cache-policy-test');
+    const definition: ServerDefinition = {
+      name: 'replay',
+      command: { kind: 'http', url: new URL('https://replay.example.com/mcp') },
+    };
+    const context = fakeContext(definition);
+    mocks.createClientContext.mockResolvedValue(context);
+    const runtime = await createRuntime({ servers: [definition] });
+
+    const first = await runtime.connect('replay');
+    const second = await runtime.connect('replay', {
+      allowCachedAuth: false,
+      disableOAuth: true,
+    });
+
+    expect(second).toBe(first);
+    expect(mocks.createClientContext).toHaveBeenCalledOnce();
+    await runtime.close();
+  });
+
+  it('keeps auth posture isolation for servers excluded by the replay filter', async () => {
+    vi.stubEnv('MCPORTER_REPLAY', 'cache-policy-test');
+    vi.stubEnv('MCPORTER_REPLAY_SERVER', 'other-server');
+    const definition: ServerDefinition = {
+      name: 'live',
+      command: { kind: 'http', url: new URL('https://live.example.com/mcp') },
+    };
+    const closeMocks: Array> = [];
+    mocks.createClientContext.mockImplementation((current: ServerDefinition) => {
+      const closeMock = vi.fn().mockResolvedValue(undefined);
+      const context = fakeContext(current, closeMock);
+      closeMocks.push(closeMock);
+      return Promise.resolve(context);
+    });
+    const runtime = await createRuntime({ servers: [definition] });
+
+    const first = await runtime.connect('live');
+    const second = await runtime.connect('live', {
+      allowCachedAuth: false,
+    });
+
+    expect(second).not.toBe(first);
+    expect(mocks.createClientContext).toHaveBeenCalledTimes(2);
+    expect(closeMocks[0]).toHaveBeenCalled();
+    await runtime.close();
+  });
+});
diff --git a/tests/runtime-cache.test.ts b/tests/runtime-cache.test.ts
new file mode 100644
index 00000000..c3b60bdb
--- /dev/null
+++ b/tests/runtime-cache.test.ts
@@ -0,0 +1,157 @@
+import { describe, expect, it, vi } from 'vitest';
+import { createRuntime } from '../src/runtime.js';
+
+type TestRuntime = Awaited>;
+type ClientContext = Awaited>;
+type CachedClientEntry = {
+  readonly server: string;
+  readonly promise: Promise;
+  readonly allowCachedAuth: boolean | undefined;
+  readonly disableOAuth: boolean;
+};
+
+function fakeContext(instructions: string): ClientContext {
+  return {
+    client: {
+      close: vi.fn().mockResolvedValue(undefined),
+      getInstructions: vi.fn(() => instructions),
+    },
+    transport: { close: vi.fn().mockResolvedValue(undefined) },
+    definition: {
+      name: 'temp',
+      description: 'test',
+      command: { kind: 'stdio', command: 'node', args: [], cwd: process.cwd() },
+      source: { kind: 'local', path: '' },
+    },
+    oauthSession: undefined,
+  } as unknown as ClientContext;
+}
+
+describe('runtime cache entries', () => {
+  it('reads instructions from the active cached entry', async () => {
+    const runtime = await createRuntime({ servers: [] });
+    const older = fakeContext('older instructions');
+    const active = fakeContext('active instructions');
+    const internals = runtime as unknown as {
+      clients: Map;
+      activeClientKeys: Map;
+    };
+
+    internals.clients.set('temp:older', {
+      server: 'temp',
+      promise: Promise.resolve(older),
+      allowCachedAuth: true,
+      disableOAuth: false,
+    });
+    internals.clients.set('temp:active', {
+      server: 'temp',
+      promise: Promise.resolve(active),
+      allowCachedAuth: true,
+      disableOAuth: true,
+    });
+    internals.activeClientKeys.set('temp', 'temp:active');
+
+    await expect(runtime.getInstructions?.('temp')).resolves.toBe('active instructions');
+  });
+
+  it('closes cached entries when replacing a server definition', async () => {
+    const runtime = await createRuntime({ servers: [] });
+    const context = fakeContext('old instructions');
+    const transport = context.transport as unknown as { close: ReturnType };
+    const internals = runtime as unknown as {
+      clients: Map;
+      contextCacheKeys: WeakMap;
+    };
+
+    internals.clients.set('temp:old', {
+      server: 'temp',
+      promise: Promise.resolve(context),
+      allowCachedAuth: undefined,
+      disableOAuth: false,
+    });
+    internals.contextCacheKeys.set(context, 'temp:old');
+
+    runtime.registerDefinition(
+      {
+        name: 'temp',
+        command: { kind: 'stdio', command: 'node', args: ['-v'], cwd: process.cwd() },
+        source: { kind: 'local', path: '' },
+      },
+      { overwrite: true }
+    );
+
+    await vi.waitFor(() => expect(transport.close).toHaveBeenCalled());
+    expect(internals.clients.has('temp:old')).toBe(false);
+  });
+
+  it('removes cached entries before awaiting shutdown', async () => {
+    const runtime = await createRuntime({ servers: [] });
+    let releaseClose!: () => void;
+    const clientClose = vi.fn(
+      () =>
+        new Promise((resolve) => {
+          releaseClose = resolve;
+        })
+    );
+    const context = {
+      ...fakeContext('closing instructions'),
+      client: {
+        close: clientClose,
+        getInstructions: vi.fn(() => 'closing instructions'),
+      },
+    } as unknown as ClientContext;
+    const internals = runtime as unknown as {
+      clients: Map;
+      activeClientKeys: Map;
+      contextCacheKeys: WeakMap;
+    };
+    internals.clients.set('temp:closing', {
+      server: 'temp',
+      promise: Promise.resolve(context),
+      allowCachedAuth: undefined,
+      disableOAuth: false,
+    });
+    internals.activeClientKeys.set('temp', 'temp:closing');
+    internals.contextCacheKeys.set(context, 'temp:closing');
+
+    const closing = runtime.close('temp');
+
+    expect(internals.clients.has('temp:closing')).toBe(false);
+    expect(internals.activeClientKeys.has('temp')).toBe(false);
+    await vi.waitFor(() => expect(clientClose).toHaveBeenCalled());
+    releaseClose();
+    await closing;
+  });
+
+  it('starts closing cached variants concurrently', async () => {
+    const runtime = await createRuntime({ servers: [] });
+    let resolvePending!: (context: ClientContext) => void;
+    const pending = new Promise((resolve) => {
+      resolvePending = resolve;
+    });
+    const pendingContext = fakeContext('pending instructions');
+    const readyContext = fakeContext('ready instructions');
+    const readyTransport = readyContext.transport as unknown as { close: ReturnType };
+    const internals = runtime as unknown as {
+      clients: Map;
+    };
+    internals.clients.set('temp:pending', {
+      server: 'temp',
+      promise: pending,
+      allowCachedAuth: false,
+      disableOAuth: false,
+    });
+    internals.clients.set('temp:ready', {
+      server: 'temp',
+      promise: Promise.resolve(readyContext),
+      allowCachedAuth: true,
+      disableOAuth: true,
+    });
+
+    const closing = runtime.close('temp');
+
+    await vi.waitFor(() => expect(readyTransport.close).toHaveBeenCalled());
+    resolvePending(pendingContext);
+    await closing;
+  });
+});
diff --git a/tests/runtime-call-timeout.test.ts b/tests/runtime-call-timeout.test.ts
index fc1b79da..2593dfe5 100644
--- a/tests/runtime-call-timeout.test.ts
+++ b/tests/runtime-call-timeout.test.ts
@@ -48,9 +48,10 @@ describe('runtime callTool timeouts', () => {
     const runtime = await createRuntime({ servers: [] });
     const callTool = vi.fn(() => new Promise(() => {}));
     type ClientContext = Awaited>;
+    const transport = { close: vi.fn().mockResolvedValue(undefined) };
     const fakeContext = {
       client: { callTool },
-      transport: { close: vi.fn().mockResolvedValue(undefined) },
+      transport,
       definition: {
         name: 'temp',
         description: 'test',
@@ -60,16 +61,42 @@ describe('runtime callTool timeouts', () => {
       oauthSession: undefined,
     } as unknown as ClientContext;
     vi.spyOn(runtime, 'connect').mockResolvedValue(fakeContext);
-    (runtime as unknown as { clients: Map> }).clients.set(
-      'temp',
-      Promise.resolve(fakeContext)
-    );
+    const cachedPromise = Promise.resolve(fakeContext);
+    (
+      runtime as unknown as {
+        clients: Map<
+          string,
+          {
+            server: string;
+            promise: Promise;
+            allowCachedAuth: boolean | undefined;
+            disableOAuth: boolean;
+          }
+        >;
+      }
+    ).clients.set('temp:test', {
+      server: 'temp',
+      promise: cachedPromise,
+      allowCachedAuth: true,
+      disableOAuth: false,
+    });
+    (
+      runtime as unknown as {
+        contextCacheKeys: WeakMap;
+      }
+    ).contextCacheKeys.set(fakeContext, 'temp:test');
+    (
+      runtime as unknown as {
+        contextCachePromises: WeakMap>;
+      }
+    ).contextCachePromises.set(fakeContext, cachedPromise);
     const closeSpy = vi.spyOn(runtime, 'close').mockResolvedValue();
 
     const promise = runtime.callTool('temp', 'ping', { timeoutMs: 123 });
     const expectation = expect(promise).rejects.toThrow('Timeout');
     await vi.advanceTimersByTimeAsync(200);
     await expectation;
-    expect(closeSpy).toHaveBeenCalledWith('temp');
+    expect(closeSpy).not.toHaveBeenCalled();
+    expect(transport.close).toHaveBeenCalled();
   });
 });
diff --git a/tests/runtime-compose.test.ts b/tests/runtime-compose.test.ts
index 1d1b02eb..b75b109f 100644
--- a/tests/runtime-compose.test.ts
+++ b/tests/runtime-compose.test.ts
@@ -1,5 +1,12 @@
+import fs from 'node:fs/promises';
+import os from 'node:os';
+import path from 'node:path';
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
 
+function throwConnectBoom(): never {
+  throw new Error('connect boom');
+}
+
 const mocks = vi.hoisted(() => {
   const connectMock = vi.fn();
   const listToolsMock = vi.fn();
@@ -108,7 +115,7 @@ vi.mock('../src/oauth-persistence.js', () => ({
   readCachedAccessToken: mocks.readCachedAccessTokenMock,
 }));
 
-import { createRuntime } from '../src/runtime.js';
+import { callOnce, createRuntime } from '../src/runtime.js';
 
 describe('mcporter composability', () => {
   beforeEach(() => {
@@ -228,6 +235,30 @@ describe('mcporter composability', () => {
     expect(instance?.options?.env?.MCPORTER_STDIO_TEST).toBe('from-parent');
   });
 
+  it('reuses stdio clients across auth-policy no-op differences', async () => {
+    const runtime = await createRuntime({
+      servers: [
+        {
+          name: 'local',
+          command: { kind: 'stdio', command: 'node', args: ['-v'], cwd: process.cwd() },
+          source: { kind: 'local', path: '' },
+        },
+      ],
+    });
+
+    try {
+      await runtime.connect('local');
+      await runtime.callTool('local', 'echo', {});
+      await runtime.connect('local', { disableOAuth: true });
+      await runtime.listTools('local', { autoAuthorize: false });
+
+      expect(mocks.stdioInstances).toHaveLength(1);
+      expect(mocks.connectMock).toHaveBeenCalledTimes(1);
+    } finally {
+      await runtime.close();
+    }
+  });
+
   it('overrides inherited env vars with server-specific values', async () => {
     vi.stubEnv('MCPORTER_STDIO_TEST', 'parent');
     const runtime = await createRuntime({
@@ -271,6 +302,375 @@ describe('mcporter composability', () => {
     }
   });
 
+  it('preserves a disabled-OAuth cached connection through high-level helpers', async () => {
+    const runtime = await createRuntime({
+      servers: [
+        {
+          name: 'oauth',
+          command: { kind: 'http' as const, url: new URL('https://oauth.example.com/mcp') },
+        },
+      ],
+    });
+
+    try {
+      await runtime.connect('oauth', { disableOAuth: true, allowCachedAuth: true });
+      await runtime.callTool('oauth', 'ping');
+      await runtime.listTools('oauth');
+      await runtime.listResources('oauth');
+
+      expect(mocks.streamableInstances).toHaveLength(1);
+      expect(mocks.connectMock).toHaveBeenCalledTimes(1);
+    } finally {
+      await runtime.close();
+    }
+  });
+
+  it('reuses active cached-auth connections for resource helpers with unspecified auth policy', async () => {
+    const runtime = await createRuntime({
+      servers: [
+        {
+          name: 'oauth',
+          command: { kind: 'http' as const, url: new URL('https://oauth.example.com/mcp') },
+        },
+      ],
+    });
+
+    try {
+      mocks.readCachedAccessTokenMock.mockResolvedValue('cached-token');
+      await runtime.listTools('oauth');
+      await runtime.listResources('oauth');
+
+      expect(mocks.streamableInstances).toHaveLength(1);
+      expect(mocks.connectMock).toHaveBeenCalledTimes(1);
+    } finally {
+      await runtime.close();
+    }
+  });
+
+  it('uses disableOAuth on cold callTool/listTools helper connections', async () => {
+    const runtime = await createRuntime({
+      servers: [
+        {
+          name: 'oauth',
+          command: { kind: 'http' as const, url: new URL('https://oauth.example.com/mcp') },
+          auth: 'oauth' as const,
+        },
+      ],
+    });
+
+    try {
+      await runtime.callTool('oauth', 'ping', { disableOAuth: true });
+      await runtime.listTools('oauth', { disableOAuth: true });
+
+      expect(mocks.streamableInstances).toHaveLength(1);
+      expect(mocks.connectMock).toHaveBeenCalledTimes(1);
+    } finally {
+      await runtime.close();
+    }
+  });
+
+  it('preserves cached-auth opt out for disabled-OAuth helper calls', async () => {
+    const runtime = await createRuntime({
+      servers: [
+        {
+          name: 'oauth',
+          command: { kind: 'http' as const, url: new URL('https://oauth.example.com/mcp') },
+          auth: 'oauth' as const,
+        },
+      ],
+    });
+
+    try {
+      await runtime.connect('oauth', { disableOAuth: true, allowCachedAuth: false });
+      await runtime.callTool('oauth', 'ping');
+      await runtime.listTools('oauth');
+      await runtime.listResources('oauth');
+
+      expect(mocks.streamableInstances).toHaveLength(1);
+      expect(mocks.readCachedAccessTokenMock).not.toHaveBeenCalled();
+      await runtime.connect('oauth', { disableOAuth: true });
+      expect(mocks.streamableInstances).toHaveLength(2);
+    } finally {
+      await runtime.close();
+    }
+  });
+
+  it('keeps separate cached transports for OAuth posture changes', async () => {
+    const runtime = await createRuntime({
+      servers: [
+        {
+          name: 'oauth',
+          command: { kind: 'http' as const, url: new URL('https://oauth.example.com/mcp') },
+        },
+      ],
+    });
+
+    try {
+      const disabled = await runtime.connect('oauth', { disableOAuth: true });
+      const disabledTransport = mocks.streamableInstances[0] as { close: ReturnType };
+      const normal = await runtime.connect('oauth');
+
+      expect(normal).not.toBe(disabled);
+      expect(mocks.streamableInstances).toHaveLength(2);
+      expect(disabledTransport.close).not.toHaveBeenCalled();
+      await expect(runtime.connect('oauth', { disableOAuth: true })).resolves.toBe(disabled);
+      await runtime.callTool('oauth', 'ping');
+      expect(mocks.streamableInstances).toHaveLength(2);
+    } finally {
+      await runtime.close();
+    }
+  });
+
+  it('restores the previous active cached variant when a new variant fails to connect', async () => {
+    const runtime = await createRuntime({
+      servers: [
+        {
+          name: 'oauth',
+          command: { kind: 'http' as const, url: new URL('https://oauth.example.com/mcp') },
+        },
+      ],
+    });
+
+    try {
+      await runtime.connect('oauth');
+      await runtime.connect('oauth', { disableOAuth: true });
+      const internals = runtime as unknown as {
+        activeClientKeys: Map;
+        clients: Map<
+          string,
+          {
+            allowCachedAuth: boolean | undefined;
+            disableOAuth: boolean;
+          }
+        >;
+      };
+      const disabledKey = [...internals.clients.entries()].find(
+        ([, cached]) => cached.disableOAuth && cached.allowCachedAuth === true
+      )?.[0];
+
+      mocks.connectMock.mockImplementationOnce(throwConnectBoom).mockImplementationOnce(throwConnectBoom);
+      await expect(runtime.connect('oauth', { disableOAuth: true, allowCachedAuth: false })).rejects.toThrow(
+        'connect boom'
+      );
+
+      expect(internals.activeClientKeys.get('oauth')).toBe(disabledKey);
+    } finally {
+      await runtime.close();
+    }
+  });
+
+  it('serializes concurrent OAuth-capable HTTP variant setup', async () => {
+    const runtime = await createRuntime({
+      servers: [
+        {
+          name: 'oauth',
+          command: { kind: 'http' as const, url: new URL('https://oauth.example.com/mcp') },
+        },
+      ],
+    });
+    let releaseFirst!: () => void;
+    mocks.connectMock.mockImplementationOnce((transport: { start?: ReturnType }) => {
+      transport.start?.mockImplementationOnce(
+        () =>
+          new Promise((resolve) => {
+            releaseFirst = resolve;
+          })
+      );
+    });
+
+    try {
+      const first = runtime.connect('oauth', { allowCachedAuth: false });
+      await vi.waitFor(() => expect(mocks.streamableInstances).toHaveLength(1));
+      const second = runtime.connect('oauth', { allowCachedAuth: true });
+      await Promise.resolve();
+
+      expect(mocks.streamableInstances).toHaveLength(1);
+      releaseFirst();
+      await first;
+      await second;
+      expect(mocks.streamableInstances).toHaveLength(2);
+    } finally {
+      await runtime.close();
+    }
+  });
+
+  it('does not create a new OAuth-capable variant after close interrupts retirement', async () => {
+    const runtime = await createRuntime({
+      servers: [
+        {
+          name: 'oauth',
+          command: { kind: 'http' as const, url: new URL('https://oauth.example.com/mcp') },
+        },
+      ],
+    });
+    await runtime.connect('oauth', { allowCachedAuth: false });
+    let releaseClose!: () => void;
+    const firstClient = mocks.clientInstances[0] as { close: () => Promise };
+    firstClient.close = vi.fn(
+      () =>
+        new Promise((resolve) => {
+          releaseClose = resolve;
+        })
+    );
+
+    const replacement = runtime.connect('oauth', { allowCachedAuth: true });
+    const replacementExpectation = expect(replacement).rejects.toThrow('superseded');
+    await vi.waitFor(() => expect(firstClient.close).toHaveBeenCalled());
+    const closing = runtime.close('oauth');
+    releaseClose();
+
+    await Promise.all([replacementExpectation, closing]);
+    expect(mocks.streamableInstances).toHaveLength(1);
+  });
+
+  it('releases serialized setup after conflicting-entry retirement fails', async () => {
+    const runtime = await createRuntime({
+      servers: [
+        {
+          name: 'oauth',
+          command: { kind: 'http' as const, url: new URL('https://oauth.example.com/mcp') },
+        },
+      ],
+    });
+    type ClientContext = Awaited>;
+    const rejected = Promise.reject(new Error('retire boom')) as Promise;
+    void rejected.catch(() => {});
+    (
+      runtime as unknown as {
+        clients: Map<
+          string,
+          {
+            server: string;
+            promise: Promise;
+            contextPromise: Promise;
+            allowCachedAuth: boolean | undefined;
+            disableOAuth: boolean;
+          }
+        >;
+      }
+    ).clients.set('oauth:conflict', {
+      server: 'oauth',
+      promise: rejected,
+      contextPromise: rejected,
+      allowCachedAuth: false,
+      disableOAuth: false,
+    });
+
+    await expect(runtime.connect('oauth', { allowCachedAuth: true })).rejects.toThrow('retire boom');
+    await expect(runtime.connect('oauth', { allowCachedAuth: true })).resolves.toBeDefined();
+    await runtime.close();
+  });
+
+  it('cancels queued OAuth-capable setup when the server closes', async () => {
+    const runtime = await createRuntime({
+      servers: [
+        {
+          name: 'oauth',
+          command: { kind: 'http' as const, url: new URL('https://oauth.example.com/mcp') },
+        },
+      ],
+    });
+    let releaseFirst!: () => void;
+    mocks.connectMock.mockImplementationOnce((transport: { start?: ReturnType }) => {
+      transport.start?.mockImplementationOnce(
+        () =>
+          new Promise((resolve) => {
+            releaseFirst = resolve;
+          })
+      );
+    });
+
+    const first = runtime.connect('oauth', { allowCachedAuth: false });
+    const firstExpectation = expect(first).rejects.toThrow('superseded');
+    await vi.waitFor(() => expect(mocks.streamableInstances).toHaveLength(1));
+    const second = runtime.connect('oauth', { allowCachedAuth: true });
+    const secondExpectation = expect(second).rejects.toThrow('superseded');
+    await Promise.resolve();
+
+    const closing = runtime.close('oauth');
+    releaseFirst();
+    await Promise.all([firstExpectation, secondExpectation, closing]);
+    expect(mocks.streamableInstances).toHaveLength(1);
+  });
+
+  it('rejects an in-flight connection when its definition is replaced', async () => {
+    const runtime = await createRuntime({
+      servers: [
+        {
+          name: 'oauth',
+          command: { kind: 'http' as const, url: new URL('https://old.example.com/mcp') },
+        },
+      ],
+    });
+    let releaseConnect!: () => void;
+    mocks.connectMock.mockImplementationOnce((transport: { start?: ReturnType }) => {
+      transport.start?.mockImplementationOnce(
+        () =>
+          new Promise((resolve) => {
+            releaseConnect = resolve;
+          })
+      );
+    });
+
+    const connecting = runtime.connect('oauth');
+    const waiting = runtime.connect('oauth');
+    const expectations = Promise.all([
+      expect(connecting).rejects.toThrow('superseded'),
+      expect(waiting).rejects.toThrow('superseded'),
+    ]);
+    await vi.waitFor(() => expect(mocks.streamableInstances).toHaveLength(1));
+    runtime.registerDefinition(
+      {
+        name: 'oauth',
+        command: { kind: 'http' as const, url: new URL('https://new.example.com/mcp') },
+      },
+      { overwrite: true }
+    );
+    releaseConnect();
+
+    await expectations;
+    const oldTransport = mocks.streamableInstances[0] as { close: ReturnType };
+    await vi.waitFor(() => expect(oldTransport.close).toHaveBeenCalled());
+  });
+
+  it('forwards disableOAuth through callOnce', async () => {
+    const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcporter-call-once-'));
+    const configPath = path.join(tempDir, 'mcporter.json');
+    await fs.writeFile(
+      configPath,
+      JSON.stringify({
+        mcpServers: {
+          oauth: {
+            url: 'https://oauth.example.com/mcp',
+            auth: 'oauth',
+          },
+        },
+      }),
+      'utf8'
+    );
+
+    try {
+      await callOnce({
+        server: 'oauth',
+        toolName: 'ping',
+        args: { ok: true },
+        configPath,
+        disableOAuth: true,
+      });
+
+      expect(mocks.callToolMock).toHaveBeenCalledWith({
+        name: 'ping',
+        arguments: { ok: true },
+      });
+      const streamableTransport = mocks.streamableInstances[0] as {
+        options?: { authProvider?: unknown };
+      };
+      expect(streamableTransport.options?.authProvider).toBeUndefined();
+    } finally {
+      await fs.rm(tempDir, { recursive: true, force: true });
+    }
+  });
+
   it('reconnects when callTool needs cached auth after an uncached connection', async () => {
     const runtime = await createRuntime({
       servers: [
@@ -284,11 +684,13 @@ describe('mcporter composability', () => {
     try {
       await runtime.listTools('oauth', { allowCachedAuth: false });
       expect(mocks.streamableInstances).toHaveLength(1);
+      const firstTransport = mocks.streamableInstances[0] as { close: ReturnType };
 
       mocks.readCachedAccessTokenMock.mockResolvedValue('cached-token');
       await runtime.callTool('oauth', 'ping');
 
       expect(mocks.streamableInstances).toHaveLength(2);
+      expect(firstTransport.close).toHaveBeenCalled();
       const streamableTransport = mocks.streamableInstances[1] as {
         options?: { requestInit?: { headers?: Record } };
       };
diff --git a/tests/runtime-error-reset.test.ts b/tests/runtime-error-reset.test.ts
index fa781135..32d413c9 100644
--- a/tests/runtime-error-reset.test.ts
+++ b/tests/runtime-error-reset.test.ts
@@ -11,11 +11,12 @@ describe('runtime connection resets', () => {
     const runtime = await createRuntime({ servers: [] });
     type ClientContext = Awaited>;
     const rejected = new McpError(ErrorCode.ConnectionClosed, 'Connection closed');
+    const transport = { close: vi.fn().mockResolvedValue(undefined) };
     const context = {
       client: {
         callTool: vi.fn().mockRejectedValue(rejected),
       },
-      transport: { close: vi.fn().mockResolvedValue(undefined) },
+      transport,
       definition: {
         name: 'temp',
         description: 'test',
@@ -25,25 +26,53 @@ describe('runtime connection resets', () => {
       oauthSession: undefined,
     } as unknown as ClientContext;
     vi.spyOn(runtime, 'connect').mockResolvedValue(context);
-    (runtime as unknown as { clients: Map> }).clients.set(
-      'temp',
-      Promise.resolve(context)
-    );
+    const promise = Promise.resolve(context);
+    (
+      runtime as unknown as {
+        clients: Map<
+          string,
+          {
+            server: string;
+            promise: Promise;
+            allowCachedAuth: boolean | undefined;
+            disableOAuth: boolean;
+          }
+        >;
+      }
+    ).clients.set('temp:test', {
+      server: 'temp',
+      promise,
+      allowCachedAuth: true,
+      disableOAuth: false,
+    });
+    (
+      runtime as unknown as {
+        contextCacheKeys: WeakMap;
+        contextCachePromises: WeakMap>;
+      }
+    ).contextCacheKeys.set(context, 'temp:test');
+    (
+      runtime as unknown as {
+        contextCachePromises: WeakMap>;
+      }
+    ).contextCachePromises.set(context, promise);
     const closeSpy = vi.spyOn(runtime, 'close').mockResolvedValue();
 
     await expect(runtime.callTool('temp', 'list_pages')).rejects.toThrow('Connection closed');
-    expect(closeSpy).toHaveBeenCalledWith('temp');
+    expect(closeSpy).not.toHaveBeenCalled();
+    expect(transport.close).toHaveBeenCalled();
   });
 
   it('keeps the connection open for user-facing InvalidParams errors', async () => {
     const runtime = await createRuntime({ servers: [] });
     type ClientContext = Awaited>;
     const rejected = new McpError(ErrorCode.InvalidParams, 'Tool help not found');
+    const transport = { close: vi.fn().mockResolvedValue(undefined) };
     const context = {
       client: {
         callTool: vi.fn().mockRejectedValue(rejected),
       },
-      transport: { close: vi.fn().mockResolvedValue(undefined) },
+      transport,
       definition: {
         name: 'temp',
         description: 'test',
@@ -53,13 +82,222 @@ describe('runtime connection resets', () => {
       oauthSession: undefined,
     } as unknown as ClientContext;
     vi.spyOn(runtime, 'connect').mockResolvedValue(context);
-    (runtime as unknown as { clients: Map> }).clients.set(
-      'temp',
-      Promise.resolve(context)
-    );
+    const promise = Promise.resolve(context);
+    (
+      runtime as unknown as {
+        clients: Map<
+          string,
+          {
+            server: string;
+            promise: Promise;
+            allowCachedAuth: boolean | undefined;
+            disableOAuth: boolean;
+          }
+        >;
+      }
+    ).clients.set('temp:test', {
+      server: 'temp',
+      promise,
+      allowCachedAuth: true,
+      disableOAuth: false,
+    });
+    (
+      runtime as unknown as {
+        contextCacheKeys: WeakMap;
+      }
+    ).contextCacheKeys.set(context, 'temp:test');
+    (
+      runtime as unknown as {
+        contextCachePromises: WeakMap>;
+      }
+    ).contextCachePromises.set(context, promise);
     const closeSpy = vi.spyOn(runtime, 'close').mockResolvedValue();
 
     await expect(runtime.callTool('temp', 'help')).rejects.toThrow('Tool help not found');
     expect(closeSpy).not.toHaveBeenCalled();
+    expect(transport.close).not.toHaveBeenCalled();
+  });
+
+  it('does not wait for unrelated cached connections when resetting a failed context', async () => {
+    const runtime = await createRuntime({ servers: [] });
+    type ClientContext = Awaited>;
+    const rejected = new McpError(ErrorCode.ConnectionClosed, 'Connection closed');
+    const transport = { close: vi.fn().mockResolvedValue(undefined) };
+    const context = {
+      client: {
+        callTool: vi.fn().mockRejectedValue(rejected),
+      },
+      transport,
+      definition: {
+        name: 'temp',
+        description: 'test',
+        command: { kind: 'stdio', command: 'node', args: [], cwd: process.cwd() },
+        source: { kind: 'local', path: '' },
+      },
+      oauthSession: undefined,
+    } as unknown as ClientContext;
+    const unresolved = new Promise(() => {});
+    const failedPromise = Promise.resolve(context);
+    const internals = runtime as unknown as {
+      clients: Map<
+        string,
+        {
+          server: string;
+          promise: Promise;
+          allowCachedAuth: boolean | undefined;
+          disableOAuth: boolean;
+        }
+      >;
+      contextCacheKeys: WeakMap;
+      contextCachePromises: WeakMap>;
+    };
+    internals.clients.set('temp:unrelated', {
+      server: 'temp',
+      promise: unresolved,
+      allowCachedAuth: true,
+      disableOAuth: false,
+    });
+    internals.clients.set('temp:failed', {
+      server: 'temp',
+      promise: failedPromise,
+      allowCachedAuth: true,
+      disableOAuth: true,
+    });
+    internals.contextCacheKeys.set(context, 'temp:failed');
+    internals.contextCachePromises.set(context, failedPromise);
+    vi.spyOn(runtime, 'connect').mockResolvedValue(context);
+
+    await expect(runtime.callTool('temp', 'list_pages')).rejects.toThrow('Connection closed');
+    expect(transport.close).toHaveBeenCalled();
+    expect(internals.clients.has('temp:failed')).toBe(false);
+    expect(internals.clients.has('temp:unrelated')).toBe(true);
+  });
+
+  it('leaves cached entries alone when an uncached list operation fails', async () => {
+    const runtime = await createRuntime({ servers: [] });
+    type ClientContext = Awaited>;
+    const rejected = new McpError(ErrorCode.ConnectionClosed, 'Connection closed');
+    const cachedTransport = { close: vi.fn().mockResolvedValue(undefined) };
+    const uncachedTransport = { close: vi.fn().mockResolvedValue(undefined) };
+    const cachedContext = {
+      client: {},
+      transport: cachedTransport,
+      definition: {
+        name: 'temp',
+        description: 'test',
+        command: { kind: 'stdio', command: 'node', args: [], cwd: process.cwd() },
+        source: { kind: 'local', path: '' },
+      },
+      oauthSession: undefined,
+    } as unknown as ClientContext;
+    const uncachedContext = {
+      client: {
+        listTools: vi.fn().mockRejectedValue(rejected),
+      },
+      transport: uncachedTransport,
+      definition: {
+        name: 'temp',
+        description: 'test',
+        command: { kind: 'stdio', command: 'node', args: [], cwd: process.cwd() },
+        source: { kind: 'local', path: '' },
+      },
+      oauthSession: undefined,
+    } as unknown as ClientContext;
+    const internals = runtime as unknown as {
+      clients: Map<
+        string,
+        {
+          server: string;
+          promise: Promise;
+          allowCachedAuth: boolean | undefined;
+          disableOAuth: boolean;
+        }
+      >;
+      contextCacheKeys: WeakMap;
+      contextCachePromises: WeakMap>;
+    };
+    internals.clients.set('temp:cached', {
+      server: 'temp',
+      promise: Promise.resolve(cachedContext),
+      allowCachedAuth: true,
+      disableOAuth: false,
+    });
+    internals.contextCacheKeys.set(cachedContext, 'temp:cached');
+    vi.spyOn(runtime, 'connect').mockResolvedValue(uncachedContext);
+
+    await expect(runtime.listTools('temp', { autoAuthorize: false })).rejects.toThrow('Connection closed');
+    expect(uncachedTransport.close).toHaveBeenCalled();
+    expect(cachedTransport.close).not.toHaveBeenCalled();
+    expect(internals.clients.has('temp:cached')).toBe(true);
+  });
+
+  it('does not evict a replacement while closing a failed stdio context', async () => {
+    const runtime = await createRuntime({ servers: [] });
+    type ClientContext = Awaited>;
+    const rejected = new McpError(ErrorCode.ConnectionClosed, 'Connection closed');
+    let releaseClose!: () => void;
+    const transport = {
+      close: vi.fn(
+        () =>
+          new Promise((resolve) => {
+            releaseClose = resolve;
+          })
+      ),
+    };
+    const context = {
+      client: {
+        close: vi.fn().mockResolvedValue(undefined),
+        callTool: vi.fn().mockRejectedValue(rejected),
+      },
+      transport,
+      definition: {
+        name: 'temp',
+        description: 'test',
+        command: { kind: 'stdio', command: 'node', args: [], cwd: process.cwd() },
+        source: { kind: 'local', path: '' },
+      },
+      oauthSession: undefined,
+    } as unknown as ClientContext;
+    const promise = Promise.resolve(context);
+    const internals = runtime as unknown as {
+      clients: Map<
+        string,
+        {
+          server: string;
+          promise: Promise;
+          allowCachedAuth: boolean | undefined;
+          disableOAuth: boolean;
+        }
+      >;
+      contextCacheKeys: WeakMap;
+      contextCachePromises: WeakMap>;
+    };
+    internals.clients.set('temp:stdio', {
+      server: 'temp',
+      promise,
+      allowCachedAuth: undefined,
+      disableOAuth: false,
+    });
+    internals.contextCacheKeys.set(context, 'temp:stdio');
+    internals.contextCachePromises.set(context, promise);
+    vi.spyOn(runtime, 'connect').mockResolvedValue(context);
+
+    const call = runtime.callTool('temp', 'list_pages');
+    const expectation = expect(call).rejects.toThrow('Connection closed');
+    await vi.waitFor(() => expect(transport.close).toHaveBeenCalled());
+    const replacement = Promise.resolve({
+      ...context,
+      transport: { close: vi.fn().mockResolvedValue(undefined) },
+    } as unknown as ClientContext);
+    internals.clients.set('temp:stdio', {
+      server: 'temp',
+      promise: replacement,
+      allowCachedAuth: true,
+      disableOAuth: true,
+    });
+    releaseClose();
+
+    await expectation;
+    expect(internals.clients.get('temp:stdio')?.promise).toBe(replacement);
   });
 });
diff --git a/tests/runtime-integration.test.ts b/tests/runtime-integration.test.ts
index 252970d6..e5490e32 100644
--- a/tests/runtime-integration.test.ts
+++ b/tests/runtime-integration.test.ts
@@ -133,4 +133,157 @@ describe('runtime integration', () => {
 
     await runtime.close('integration');
   });
+
+  it('reuses cached connection when disableOAuth: true is passed', async () => {
+    // Headless-daemon use case: the caller wants OAuth suppression
+    // (no browser launches) but still expects connection caching so
+    // every callTool doesn't spawn a fresh transport. Previously the
+    // only way to suppress OAuth was `maxOAuthAttempts: 0`, which
+    // forced `useCache = false` as a side effect — see the connect()
+    // gate. `disableOAuth: true` preserves caching.
+    const runtime = await createRuntime({
+      servers: [
+        {
+          name: 'integration',
+          description: 'Integration test server',
+          command: { kind: 'http', url: baseUrl },
+        },
+      ],
+    });
+
+    const first = await runtime.connect('integration', { disableOAuth: true });
+    const second = await runtime.connect('integration', { disableOAuth: true });
+    expect(second).toBe(first);
+
+    // close() reaps the cached client.
+    await runtime.close('integration');
+    const reopened = await runtime.connect('integration', { disableOAuth: true });
+    expect(reopened).not.toBe(first);
+
+    await runtime.close('integration');
+  });
+
+  it('treats disableOAuth: false like omitted for cache identity', async () => {
+    const runtime = await createRuntime({
+      servers: [
+        {
+          name: 'integration',
+          description: 'Integration test server',
+          command: { kind: 'http', url: baseUrl },
+        },
+      ],
+    });
+
+    const explicitFalse = await runtime.connect('integration', { disableOAuth: false });
+    const omitted = await runtime.connect('integration', {});
+    expect(omitted).toBe(explicitFalse);
+
+    await runtime.close('integration');
+  });
+
+  it('maxOAuthAttempts: 0 still bypasses the cache (existing contract preserved)', async () => {
+    // Regression guard: callers passing maxOAuthAttempts: 0 today get
+    // a fresh client per call. That contract is unchanged — only the
+    // new `disableOAuth` flag enables caching with OAuth suppression.
+    const runtime = await createRuntime({
+      servers: [
+        {
+          name: 'integration',
+          description: 'Integration test server',
+          command: { kind: 'http', url: baseUrl },
+        },
+      ],
+    });
+
+    const first = await runtime.connect('integration', { maxOAuthAttempts: 0 });
+    const second = await runtime.connect('integration', { maxOAuthAttempts: 0 });
+    expect(second).not.toBe(first);
+
+    await runtime.close('integration');
+  });
+
+  it('keeps separate cached clients when disableOAuth flag changes', async () => {
+    // Connections established with disableOAuth: true vs without are
+    // semantically different (the former cannot inherit an OAuth
+    // session that may refresh into a flow). The cache slot must not
+    // be shared across that boundary.
+    const runtime = await createRuntime({
+      servers: [
+        {
+          name: 'integration',
+          description: 'Integration test server',
+          command: { kind: 'http', url: baseUrl },
+        },
+      ],
+    });
+
+    const cached = await runtime.connect('integration', { disableOAuth: true });
+    const withFlowAllowed = await runtime.connect('integration', {});
+    expect(withFlowAllowed).not.toBe(cached);
+    const cachedAgain = await runtime.connect('integration', { disableOAuth: true });
+    expect(cachedAgain).toBe(cached);
+
+    await runtime.close('integration');
+  });
+
+  it('preserves the cached client across connect(disableOAuth:true) → callTool() (no implicit eviction)', async () => {
+    // Regression for the PR-198 review note (Codex r3366238654): the
+    // documented headless setup is `await runtime.connect(server, {
+    // disableOAuth: true })`. That call stored the cache slot with
+    // `allowCachedAuth: undefined`. The subsequent internal
+    // `callTool()` path forces `allowCachedAuth: true`, and the
+    // cache-match check (existing.allowCachedAuth === options.allowCachedAuth
+    // || options.allowCachedAuth === undefined) treated the two as
+    // structurally different — every first callTool evicted and
+    // reopened the transport. Defeats the pooling guarantee for the
+    // common pre-connect path.
+    const runtime = await createRuntime({
+      servers: [
+        {
+          name: 'integration',
+          description: 'Integration test server',
+          command: { kind: 'http', url: baseUrl },
+        },
+      ],
+    });
+
+    const initial = await runtime.connect('integration', { disableOAuth: true });
+
+    const callResult = (await runtime.callTool('integration', 'add', {
+      args: { a: 1, b: 2 },
+    })) as { structuredContent?: { result: number } };
+    expect(callResult.structuredContent?.result).toBe(3);
+
+    // After callTool, the cache slot should still hold the same
+    // ClientContext established by the prior connect() — no eviction,
+    // no extra transport spawned.
+    const afterCall = await runtime.connect('integration', { disableOAuth: true });
+    expect(afterCall).toBe(initial);
+
+    await runtime.close('integration');
+  });
+
+  it('preserves the cached client across connect(disableOAuth:true) → listTools() (no implicit eviction)', async () => {
+    // Same shape as the callTool regression: listTools also forces
+    // `allowCachedAuth: options.allowCachedAuth ?? true` internally,
+    // so the pre-connected slot was being evicted on first listTools.
+    const runtime = await createRuntime({
+      servers: [
+        {
+          name: 'integration',
+          description: 'Integration test server',
+          command: { kind: 'http', url: baseUrl },
+        },
+      ],
+    });
+
+    const initial = await runtime.connect('integration', { disableOAuth: true });
+    const tools = await runtime.listTools('integration');
+    expect(tools.some((tool) => tool.name === 'add')).toBe(true);
+
+    const afterList = await runtime.connect('integration', { disableOAuth: true });
+    expect(afterList).toBe(initial);
+
+    await runtime.close('integration');
+  });
 });
diff --git a/tests/runtime-transport.test.ts b/tests/runtime-transport.test.ts
index 965b8f6c..b2b2f738 100644
--- a/tests/runtime-transport.test.ts
+++ b/tests/runtime-transport.test.ts
@@ -352,6 +352,80 @@ describe('createClientContext (HTTP)', () => {
     await createClientContext(definition, logger, clientInfo, { maxOAuthAttempts: 0 });
   });
 
+  it('does not create OAuth sessions for OAuth HTTP servers when disableOAuth is true', async () => {
+    const definition = stubOAuthHttpDefinition('https://example.com/secure');
+
+    mocks.connectWithAuth.mockImplementationOnce(async (_client, transport, session) => {
+      expect(transport).toBeInstanceOf(StreamableHTTPClientTransport);
+      expect(session).toBeUndefined();
+      return transport;
+    });
+
+    const context = await createClientContext(definition, logger, clientInfo, {
+      disableOAuth: true,
+      allowCachedAuth: true,
+    });
+
+    expect(context.definition.auth).toBe('oauth');
+    expect(mocks.createOAuthSession).not.toHaveBeenCalled();
+    expect(mocks.connectWithAuth).toHaveBeenCalledTimes(1);
+  });
+
+  it('does not promote ad-hoc HTTP servers after Streamable 401 when disableOAuth is true', async () => {
+    const definition = stubHttpDefinition('https://example.com/secure');
+
+    mocks.connectWithAuth
+      .mockImplementationOnce(async (_client, transport, session) => {
+        expect(transport).toBeInstanceOf(StreamableHTTPClientTransport);
+        expect(session).toBeUndefined();
+        throw new Error('SSE error: Non-200 status code (401)');
+      })
+      .mockImplementationOnce(async (_client, transport, session) => {
+        expect(transport).toBeInstanceOf(SSEClientTransport);
+        expect(session).toBeUndefined();
+        return transport;
+      });
+
+    const { promotedDefinitions, onDefinitionPromoted } = createPromotionRecorder();
+    const context = await createClientContext(definition, logger, clientInfo, {
+      disableOAuth: true,
+      onDefinitionPromoted,
+    });
+
+    expect(context.definition.auth).toBeUndefined();
+    expect(mocks.createOAuthSession).not.toHaveBeenCalled();
+    expect(promotedDefinitions).toEqual([]);
+    expect(mocks.connectWithAuth).toHaveBeenCalledTimes(2);
+  });
+
+  it('does not promote ad-hoc HTTP servers after SSE 401 when disableOAuth is true', async () => {
+    const definition = stubHttpDefinition('https://example.com/sse-auth');
+
+    mocks.connectWithAuth
+      .mockImplementationOnce(async (_client, transport, session) => {
+        expect(transport).toBeInstanceOf(StreamableHTTPClientTransport);
+        expect(session).toBeUndefined();
+        throw new Error('HTTP error 405: Method Not Allowed');
+      })
+      .mockImplementationOnce(async (_client, transport, session) => {
+        expect(transport).toBeInstanceOf(SSEClientTransport);
+        expect(session).toBeUndefined();
+        throw new Error('SSE error: Non-200 status code (401)');
+      });
+
+    const { promotedDefinitions, onDefinitionPromoted } = createPromotionRecorder();
+    await expect(
+      createClientContext(definition, logger, clientInfo, {
+        disableOAuth: true,
+        onDefinitionPromoted,
+      })
+    ).rejects.toThrow('Non-200 status code (401)');
+
+    expect(mocks.createOAuthSession).not.toHaveBeenCalled();
+    expect(promotedDefinitions).toEqual([]);
+    expect(mocks.connectWithAuth).toHaveBeenCalledTimes(2);
+  });
+
   it('promotes ad-hoc HTTP servers after generic 401 errors from Streamable HTTP', async () => {
     const definition = stubHttpDefinition('https://example.com/secure');
 
diff --git a/tests/server-proxy.test.ts b/tests/server-proxy.test.ts
index ff0cef3f..6975c779 100644
--- a/tests/server-proxy.test.ts
+++ b/tests/server-proxy.test.ts
@@ -333,4 +333,273 @@ describe('createServerProxy', () => {
       tailLog: true,
     });
   });
+
+  it('threads disableOAuth through schema discovery so proxy.tool({disableOAuth:true}) cannot trigger OAuth during metadata fetch', async () => {
+    // Regression for the PR-198 reviewer note: the proxy fired
+    // `runtime.listTools(server, { includeSchema: true })` for schema
+    // discovery BEFORE parsing the caller's options. On an OAuth
+    // server with no cached schema, that pre-call could start an
+    // interactive OAuth flow even when the eventual tool call had
+    // `disableOAuth: true`. Fix: the proxy must extract disableOAuth
+    // up front and pass it to listTools so the no-OAuth contract
+    // covers the whole proxy interaction.
+    const runtime = createMockRuntime({
+      'some-tool': {
+        type: 'object',
+        properties: {
+          foo: { type: 'string' },
+          disableOAuth: { type: 'boolean' },
+        },
+        required: ['foo'],
+      },
+    });
+    const proxy = createServerProxy(runtime as unknown as Runtime, 'mock') as Record;
+    const fn = proxy.someTool as (args: unknown, options: unknown) => Promise;
+
+    await fn({ foo: 'bar' }, { disableOAuth: true });
+
+    // The schema-fetch listTools call must carry disableOAuth: true.
+    expect(runtime.listTools).toHaveBeenCalledWith('mock', {
+      includeSchema: true,
+      disableOAuth: true,
+    });
+    // And the eventual tool call must too — already covered by the
+    // existing KNOWN_OPTION_KEYS handling, asserted here so both
+    // halves of the contract are locked together.
+    expect(runtime.callTool).toHaveBeenCalledWith('mock', 'some-tool', {
+      args: { foo: 'bar' },
+      disableOAuth: true,
+    });
+  });
+
+  it('detects disableOAuth metadata options before later argument objects', async () => {
+    const runtime = createMockRuntime({
+      'some-tool': {
+        type: 'object',
+        properties: {
+          foo: { type: 'string' },
+        },
+        required: ['foo'],
+      },
+    });
+    const proxy = createServerProxy(runtime as unknown as Runtime, 'mock') as Record;
+    const fn = proxy.someTool as (options: unknown, args: unknown) => Promise;
+
+    await fn({ disableOAuth: true }, { foo: 'bar' });
+
+    expect(runtime.listTools).toHaveBeenCalledWith('mock', {
+      includeSchema: true,
+      disableOAuth: true,
+    });
+    expect(runtime.callTool).toHaveBeenCalledWith('mock', 'some-tool', {
+      args: { foo: 'bar' },
+      disableOAuth: true,
+    });
+  });
+
+  it('preserves schema-owned disableOAuth fields after metadata discovery', async () => {
+    const runtime = createMockRuntime({
+      'some-tool': {
+        type: 'object',
+        properties: {
+          disableOAuth: { type: 'boolean' },
+        },
+        required: ['disableOAuth'],
+      },
+    });
+    const proxy = createServerProxy(runtime as unknown as Runtime, 'mock') as Record;
+    const fn = proxy.someTool as (args: unknown) => Promise;
+
+    await fn({ disableOAuth: true });
+
+    expect(runtime.listTools).toHaveBeenCalledWith('mock', {
+      includeSchema: true,
+    });
+    expect(runtime.callTool).toHaveBeenCalledWith('mock', 'some-tool', {
+      args: { disableOAuth: true },
+    });
+  });
+
+  it('preserves schema-owned disableOAuth fields beside proxy options', async () => {
+    const runtime = createMockRuntime({
+      'some-tool': {
+        type: 'object',
+        properties: {
+          disableOAuth: { type: 'boolean' },
+        },
+        required: ['disableOAuth'],
+      },
+    });
+    const proxy = createServerProxy(runtime as unknown as Runtime, 'mock') as Record;
+    const fn = proxy.someTool as (args: unknown, options: unknown) => Promise;
+
+    await fn({ disableOAuth: true }, { tailLog: true });
+
+    expect(runtime.listTools).toHaveBeenCalledWith('mock', {
+      includeSchema: true,
+      autoAuthorize: false,
+    });
+    expect(runtime.callTool).toHaveBeenCalledWith('mock', 'some-tool', {
+      args: { disableOAuth: true },
+      tailLog: true,
+    });
+  });
+
+  it('preserves schema-owned disableOAuth fields beside positional arguments', async () => {
+    const runtime = createMockRuntime({
+      'some-tool': {
+        type: 'object',
+        properties: {
+          value: { type: 'string' },
+          disableOAuth: { type: 'boolean' },
+        },
+        required: ['value', 'disableOAuth'],
+      },
+    });
+    const proxy = createServerProxy(runtime as unknown as Runtime, 'mock') as Record;
+    const fn = proxy.someTool as (value: string, args: unknown) => Promise;
+
+    await fn('x', { disableOAuth: true });
+
+    expect(runtime.callTool).toHaveBeenCalledWith('mock', 'some-tool', {
+      args: {
+        value: 'x',
+        disableOAuth: true,
+      },
+    });
+  });
+
+  it('does not override active OAuth posture when schema discovery is cached', async () => {
+    const schema = {
+      type: 'object',
+      properties: {
+        value: { type: 'string' },
+        disableOAuth: { type: 'boolean' },
+      },
+      required: ['value', 'disableOAuth'],
+    };
+    const runtime = createMockRuntime({ 'some-tool': schema });
+    const proxy = createServerProxy(runtime as unknown as Runtime, 'mock', {
+      initialSchemas: { 'some-tool': schema },
+    }) as Record;
+    const fn = proxy.someTool as (value: string, args: unknown) => Promise;
+
+    await fn('x', { disableOAuth: true });
+
+    expect(runtime.listTools).not.toHaveBeenCalled();
+    expect(runtime.callTool).toHaveBeenCalledWith('mock', 'some-tool', {
+      args: {
+        value: 'x',
+        disableOAuth: true,
+      },
+    });
+  });
+
+  it('suppresses schema discovery for split proxy option bags', async () => {
+    const runtime = createMockRuntime({
+      ping: {
+        type: 'object',
+        properties: {},
+      },
+    });
+    const proxy = createServerProxy(runtime as unknown as Runtime, 'mock') as Record;
+    const fn = proxy.ping as (options: unknown, additionalOptions: unknown) => Promise;
+
+    await fn({ disableOAuth: true }, { tailLog: true });
+
+    expect(runtime.listTools).toHaveBeenCalledWith('mock', {
+      includeSchema: true,
+      autoAuthorize: false,
+    });
+    expect(runtime.callTool).toHaveBeenCalledWith('mock', 'ping', {
+      disableOAuth: true,
+      tailLog: true,
+    });
+  });
+
+  it('supports explicit args envelopes for option-only disableOAuth metadata discovery', async () => {
+    const runtime = createMockRuntime({
+      ping: {
+        type: 'object',
+        properties: {},
+      },
+    });
+    const proxy = createServerProxy(runtime as unknown as Runtime, 'mock') as Record;
+    const fn = proxy.ping as (options: unknown) => Promise;
+
+    await fn({ args: {}, disableOAuth: true });
+
+    expect(runtime.listTools).toHaveBeenCalledWith('mock', {
+      includeSchema: true,
+      disableOAuth: true,
+    });
+    expect(runtime.callTool).toHaveBeenCalledWith('mock', 'ping', {
+      args: {},
+      disableOAuth: true,
+    });
+  });
+
+  it('does not join an unsuppressed in-flight schema fetch for a disabled-OAuth call', async () => {
+    const tools: ServerToolInfo[] = [
+      {
+        name: 'ping',
+        inputSchema: {
+          type: 'object',
+          properties: {},
+        },
+      },
+    ];
+    let resolveOrdinary!: (tools: ServerToolInfo[]) => void;
+    const listTools = vi.fn((_server: string, options?: { disableOAuth?: boolean }) => {
+      if (options?.disableOAuth === true) {
+        return Promise.resolve(tools);
+      }
+      return new Promise((resolve) => {
+        resolveOrdinary = resolve;
+      });
+    });
+    const runtime = {
+      callTool: vi.fn(async (_, __, options) => options),
+      listTools,
+      getDefinition: vi.fn(() => {
+        throw new Error('no persistent schema cache');
+      }),
+    };
+    const proxy = createServerProxy(runtime as unknown as Runtime, 'mock', {
+      cacheSchemas: false,
+    }) as Record;
+    const fn = proxy.ping as (options?: unknown) => Promise;
+
+    const ordinary = fn();
+    await vi.waitFor(() => expect(listTools).toHaveBeenCalledTimes(1));
+    const suppressed = fn({ args: {}, disableOAuth: true });
+
+    await expect(suppressed).resolves.toBeDefined();
+    expect(listTools).toHaveBeenNthCalledWith(2, 'mock', {
+      includeSchema: true,
+      disableOAuth: true,
+    });
+    resolveOrdinary(tools);
+    await ordinary;
+  });
+
+  it('preserves schema-owned fields that share proxy option names', async () => {
+    const runtime = createMockRuntime({
+      wait: {
+        type: 'object',
+        properties: {
+          timeout: { type: 'number' },
+        },
+        required: ['timeout'],
+      },
+    });
+    const proxy = createServerProxy(runtime as unknown as Runtime, 'mock') as Record;
+    const fn = proxy.wait as (args: unknown) => Promise;
+
+    await fn({ timeout: 1000 });
+
+    expect(runtime.callTool).toHaveBeenCalledWith('mock', 'wait', {
+      args: { timeout: 1000 },
+    });
+  });
 });
diff --git a/tests/tool-cache.test.ts b/tests/tool-cache.test.ts
index b4c7e8a0..b40a0028 100644
--- a/tests/tool-cache.test.ts
+++ b/tests/tool-cache.test.ts
@@ -40,6 +40,14 @@ describe('loadToolMetadata', () => {
     expect(listTools).toHaveBeenCalledTimes(2);
   });
 
+  it('differentiates cache entries by disableOAuth flag', async () => {
+    const listTools = vi.fn(async () => [demoTool]);
+    const runtime = createRuntimeStub(listTools);
+    await loadToolMetadata(runtime, 'integration', { includeSchema: true });
+    await loadToolMetadata(runtime, 'integration', { includeSchema: true, disableOAuth: true });
+    expect(listTools).toHaveBeenCalledTimes(2);
+  });
+
   it('passes cached OAuth preference to the runtime', async () => {
     const listTools = vi.fn(async () => [demoTool]);
     const runtime = createRuntimeStub(listTools);
@@ -47,11 +55,13 @@ describe('loadToolMetadata', () => {
       includeSchema: true,
       autoAuthorize: false,
       allowCachedAuth: true,
+      disableOAuth: true,
     });
     expect(listTools).toHaveBeenCalledWith('integration', {
       includeSchema: true,
       autoAuthorize: false,
       allowCachedAuth: true,
+      disableOAuth: true,
     });
   });
 });