fix(oauth): three fixes for headless servers and non-standard OAuth providers
1. Remove hardcoded scope='mcp:tools' from client metadata. Providers like Granola reject this scope at the authorize endpoint (invalid_scope). Let the MCP SDK derive scope from server metadata instead, falling back to the auth server's scopes_supported. 2. Swallow xdg-open ENOENT on headless Linux servers. On VPS/CI environments without a desktop, spawning xdg-open throws an unhandled error event that crashes the process before the OAuth callback server can receive the redirect. 3. Clear stale client registration when dynamic callback port changes. With dynamic ports (the default), each run picks a different port. If a previous client registration is cached with a different redirect_uri, the auth server rejects subsequent requests with invalid_redirect_uri. Now detects the mismatch and re-registers. Fixes #67
This commit is contained in:
parent
938c09cf0f
commit
31142e03a6
31
src/oauth.ts
31
src/oauth.ts
@ -47,8 +47,13 @@ function openExternal(url: string) {
|
||||
});
|
||||
child.unref();
|
||||
} else {
|
||||
const child = spawn('xdg-open', [url], { stdio, detached: true });
|
||||
child.unref();
|
||||
try {
|
||||
const child = spawn('xdg-open', [url], { stdio, detached: true });
|
||||
child.on('error', () => {}); // swallow ENOENT on headless servers
|
||||
child.unref();
|
||||
} catch {
|
||||
// headless server — no browser available
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// best-effort: fall back to printing URL
|
||||
@ -79,7 +84,10 @@ class PersistentOAuthClientProvider implements OAuthClientProvider {
|
||||
grant_types: ['authorization_code', 'refresh_token'],
|
||||
response_types: ['code'],
|
||||
token_endpoint_auth_method: 'none',
|
||||
scope: 'mcp:tools',
|
||||
// Omit scope so the MCP SDK can derive it from the server's metadata
|
||||
// (resource metadata scopes_supported or auth server scopes_supported).
|
||||
// Hardcoding 'mcp:tools' breaks providers like Granola whose auth server
|
||||
// does not recognise that scope value.
|
||||
};
|
||||
}
|
||||
|
||||
@ -122,6 +130,23 @@ class PersistentOAuthClientProvider implements OAuthClientProvider {
|
||||
redirectUrl.pathname = callbackPath;
|
||||
}
|
||||
|
||||
// When using a dynamic port, the redirect URI changes every run. If a
|
||||
// previous client registration is cached with a different redirect URI the
|
||||
// auth server will reject the request with `invalid_redirect_uri`. Clear
|
||||
// the stale registration so the next flow re-registers with the new URI.
|
||||
if (usesDynamicPort) {
|
||||
const cachedClient = await persistence.readClientInfo();
|
||||
if (cachedClient && Array.isArray((cachedClient as Record<string, unknown>).redirect_uris)) {
|
||||
const cachedRedirect = ((cachedClient as Record<string, unknown>).redirect_uris as string[])[0];
|
||||
if (cachedRedirect && cachedRedirect !== redirectUrl.toString()) {
|
||||
logger.info(
|
||||
`Redirect URI changed (${cachedRedirect} → ${redirectUrl.toString()}); clearing stale client registration.`
|
||||
);
|
||||
await persistence.clear('client');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const provider = new PersistentOAuthClientProvider(definition, persistence, redirectUrl, logger);
|
||||
provider.attachServer(server);
|
||||
return {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user