Compare commits
3 Commits
main
...
fix/oauth-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aaac0e2618 | ||
|
|
8940d13605 | ||
|
|
ff89877666 |
@ -10,6 +10,7 @@
|
||||
- OAuth wait/redirect now share one deferred to eliminate authorization race windows and preserve stable close-path errors, including wait-before-redirect and repeated-redirect flows. (PR #70, thanks @monotykamary)
|
||||
- Added `--raw-strings` (numeric coercion off) and `--no-coerce` (all coercion off) for `mcporter call` argument parsing so IDs/codes can stay literal strings. (PR #59, thanks @nobrainer-tech)
|
||||
- Added `CallResult.images()` plus opt-in `mcporter call --save-images <dir>` so image content blocks can be persisted without changing existing stdout output contracts. (PR #61, thanks @daniella-11ways)
|
||||
- OAuth transport retries now classify HTTP 405 as HTTP (not auth) and OAuth promotion applies to configured HTTP servers too, so post-auth fallback flows no longer drop credentials on 405-only endpoints. (PR #48, thanks @caseyg)
|
||||
|
||||
### Tooling / Dependencies
|
||||
- Updated dependencies to latest releases (including MCP SDK, Rolldown RC, Zod, Biome, Oxlint, Vitest, Bun types).
|
||||
|
||||
@ -10,7 +10,7 @@ export interface ConnectionIssue {
|
||||
stdioSignal?: string;
|
||||
}
|
||||
|
||||
const AUTH_STATUSES = new Set([401, 403, 405]);
|
||||
const AUTH_STATUSES = new Set([401, 403]);
|
||||
const OFFLINE_PATTERNS = [
|
||||
'fetch failed',
|
||||
'econnrefused',
|
||||
|
||||
@ -9,10 +9,8 @@ export function maybeEnableOAuth(definition: ServerDefinition, logger: Logger):
|
||||
if (definition.command.kind !== 'http') {
|
||||
return undefined;
|
||||
}
|
||||
const isAdHocSource = definition.source && definition.source.kind === 'local' && definition.source.path === '<adhoc>';
|
||||
if (!isAdHocSource) {
|
||||
return undefined;
|
||||
}
|
||||
// Allow OAuth promotion for any HTTP server that returns 401,
|
||||
// not just ad-hoc servers (fixes issue #38)
|
||||
logger.info(`Detected OAuth requirement for '${definition.name}'. Launching browser flow...`);
|
||||
return {
|
||||
...definition,
|
||||
|
||||
@ -63,7 +63,7 @@ describe('CLI list classification and routing', () => {
|
||||
expect(
|
||||
logLines.some((line) => line.includes("vercel — Vercel MCP (auth required — run 'mcporter auth vercel'"))
|
||||
).toBe(true);
|
||||
expect(logLines.some((line) => line.includes("github (auth required — run 'mcporter auth github'"))).toBe(true);
|
||||
expect(logLines.some((line) => line.includes('github') && line.includes('HTTP 405'))).toBe(true);
|
||||
const nextDevtoolsLineFound = logLines.some(
|
||||
(line) => line.startsWith('- next-devtools') && line.includes('offline — unable to reach server')
|
||||
);
|
||||
@ -75,6 +75,7 @@ describe('CLI list classification and routing', () => {
|
||||
const summaryLine = logLines.find((line) => line.startsWith('✔ Listed'));
|
||||
expect(summaryLine).toBeDefined();
|
||||
expect(summaryLine).toContain('auth required');
|
||||
expect(summaryLine).toContain('http errors');
|
||||
expect(summaryLine).toContain('offline');
|
||||
|
||||
logSpy.mockRestore();
|
||||
|
||||
@ -26,6 +26,18 @@ describe('analyzeConnectionError', () => {
|
||||
expect(issue.statusCode).toBe(429);
|
||||
});
|
||||
|
||||
it.each([401, 403] as const)('keeps %s classified as auth', (status) => {
|
||||
const issue = analyzeConnectionError(new Error(`SSE error: Non-200 status code (${status})`));
|
||||
expect(issue.kind).toBe('auth');
|
||||
expect(issue.statusCode).toBe(status);
|
||||
});
|
||||
|
||||
it('classifies HTTP 405 as transport/http instead of auth', () => {
|
||||
const issue = analyzeConnectionError(new Error('SSE error: Non-200 status code (405)'));
|
||||
expect(issue.kind).toBe('http');
|
||||
expect(issue.statusCode).toBe(405);
|
||||
});
|
||||
|
||||
it('extracts HTTP status codes from JSON payloads', () => {
|
||||
const issue = analyzeConnectionError(new Error('{"error":{"status":503}}'));
|
||||
expect(issue.kind).toBe('http');
|
||||
|
||||
@ -25,13 +25,33 @@ describe('maybeEnableOAuth', () => {
|
||||
expect(logger.info).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not mutate non-ad-hoc servers', () => {
|
||||
it('enables OAuth for non-ad-hoc HTTP servers (issue #38)', () => {
|
||||
const def: ServerDefinition = {
|
||||
name: 'local-server',
|
||||
command: { kind: 'http', url: new URL('https://example.com') },
|
||||
source: { kind: 'local', path: '/tmp/config.json' },
|
||||
};
|
||||
const updated = maybeEnableOAuth(def, logger as never);
|
||||
expect(updated).toBeDefined();
|
||||
expect(updated?.auth).toBe('oauth');
|
||||
});
|
||||
|
||||
it('does not mutate stdio servers', () => {
|
||||
const def: ServerDefinition = {
|
||||
name: 'stdio-server',
|
||||
command: { kind: 'stdio', command: 'echo', args: [], cwd: process.cwd() },
|
||||
};
|
||||
const updated = maybeEnableOAuth(def, logger as never);
|
||||
expect(updated).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not re-promote servers already configured for oauth', () => {
|
||||
const def: ServerDefinition = {
|
||||
name: 'oauth-server',
|
||||
auth: 'oauth',
|
||||
command: { kind: 'http', url: new URL('https://example.com') },
|
||||
};
|
||||
const updated = maybeEnableOAuth(def, logger as never);
|
||||
expect(updated).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user