From 45dcb6561ef7e77869fc99e825076ac954f8444d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 4 May 2026 05:44:54 +0100 Subject: [PATCH] fix: show oauth manual URL by default --- CHANGELOG.md | 1 + src/oauth.ts | 2 +- tests/oauth-session.test.ts | 30 ++++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7804574..c4b22ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Restore `mcporter call --key value` / `--key=value` tool arguments, including JSON array/object coercion, `--json -` stdin payloads, schema-aware bare string-to-array wrapping, and kebab-case to camelCase field mapping. (Issues #119 and #126) - Quote generated `emit-ts` members for tool names that are not valid TypeScript identifiers. (PR #149 / issue #30, thanks @solomonneas) - Resolve relative stdio args in generated CLI bundles against the generated script location instead of the caller's current directory. (PR #148 / issue #56, thanks @solomonneas) +- Print OAuth manual-completion URLs at the default warning log level so headless users can copy them. (PR #143 / issue #139, thanks @stainlu) ### Config diff --git a/src/oauth.ts b/src/oauth.ts index 3da5a6e..62114fd 100644 --- a/src/oauth.ts +++ b/src/oauth.ts @@ -256,7 +256,7 @@ class PersistentOAuthClientProvider implements OAuthClientProvider { this.logger.info(`Authorization required for ${this.definition.name}. Opening browser...`); this.ensureAuthorizationDeferred(); __oauthInternals.openExternal(authorizationUrl.toString()); - this.logger.info(`If the browser did not open, visit ${authorizationUrl.toString()} manually.`); + this.logger.warn(`If the browser did not open, visit ${authorizationUrl.toString()} manually.`); } async saveCodeVerifier(codeVerifier: string): Promise { diff --git a/tests/oauth-session.test.ts b/tests/oauth-session.test.ts index cd07e53..e8e3418 100644 --- a/tests/oauth-session.test.ts +++ b/tests/oauth-session.test.ts @@ -213,4 +213,34 @@ describe('FileOAuthClientProvider session lifecycle', () => { await expect(waitPromise).resolves.toBe('stable-deferred-code'); await session.close(); }); + + it('logs the manual OAuth URL at warn level for headless terminals (#139)', async () => { + const tokenCacheDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcporter-oauth-test-')); + tempDirs.push(tokenCacheDir); + const definition: ServerDefinition = { + name: 'test-oauth-headless-url', + description: 'Test OAuth server', + command: { kind: 'http', url: new URL('https://example.com/mcp') }, + auth: 'oauth', + tokenCacheDir, + }; + const logger = { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + + const session = await createOAuthSession(definition, logger); + const provider = session.provider as StatefulProvider; + vi.spyOn(__oauthInternals, 'openExternal').mockImplementation(() => {}); + const authorizationUrl = new URL('https://example.com/auth?code=xyz'); + const waitPromise = session.waitForAuthorizationCode().catch(() => undefined); + + await provider.redirectToAuthorization(authorizationUrl); + + expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining(`visit ${authorizationUrl.toString()} manually`)); + + await session.close(); + await waitPromise; + }); });