Skip to content

OAuth token refresh fails for MCP servers with non-root OAuth discovery endpoints (e.g., Notion) #3

@elrolio

Description

@elrolio

Description

OAuth token refresh fails for MCP servers where the OAuth authorization server metadata is hosted at the root domain rather than at the MCP server path.

Affected MCP Server

Notion MCP: https://mcp.notion.com/mcp

Root Cause

The discoverTokenEndpoint() function in oauth-utils.js only tries path-based discovery:

const discoveryUrls = [
  `${serverUrl}/.well-known/oauth-authorization-server`,
  `${serverUrl}/.well-known/openid-configuration`,
];

For Notion:

  • mcpc tries: https://mcp.notion.com/mcp/.well-known/oauth-authorization-server ❌ (returns 401)
  • Notion has: https://mcp.notion.com/.well-known/oauth-authorization-server ✅ (returns valid metadata)

Steps to Reproduce

  1. mcpc https://mcp.notion.com/mcp login - succeeds, OAuth flow works
  2. Wait for access token to expire (~1 hour for Notion)
  3. Run any mcpc command: mcpc @notion tools-list
  4. Error: Could not find OAuth token endpoint for https://mcp.notion.com/mcp. Please re-authenticate

Logs

[DEBUG] [oauth-utils] Trying OAuth discovery at: https://mcp.notion.com/mcp/.well-known/oauth-authorization-server
[DEBUG] [oauth-utils] Trying OAuth discovery at: https://mcp.notion.com/mcp/.well-known/openid-configuration
[ERROR] Could not find OAuth token endpoint for https://mcp.notion.com/mcp

Why Initial Auth Works

Initial OAuth flow uses OAuthUtils.discoverAuthorizationServerMetadata() which does try root-based discovery:

// With issuer URLs with path components, try the following well-known endpoints in order:
if (authServerUrlObj.pathname !== '/') {
  endpointsToTry.push(new URL(`/.well-known/oauth-authorization-server${authServerUrlObj.pathname}`, base).toString());
  // 2. OpenID Connect Discovery 1.0 with path insertion
  // ... more endpoints including root-based
}

But discoverTokenEndpoint() (used for refresh) doesn't have this fallback logic.

Suggested Fix

Update discoverTokenEndpoint() to also try root-based discovery as a fallback:

export async function discoverTokenEndpoint(serverUrl) {
  const serverUrlObj = new URL(serverUrl);
  const base = `${serverUrlObj.protocol}//${serverUrlObj.host}`;
  
  const discoveryUrls = [
    // Path-based (current behavior)
    `${serverUrl}/.well-known/oauth-authorization-server`,
    `${serverUrl}/.well-known/openid-configuration`,
    // Root-based fallback (needed for Notion, etc.)
    `${base}/.well-known/oauth-authorization-server`,
    `${base}/.well-known/openid-configuration`,
  ];
  // ... rest of function
}

Workaround

Use profiles.json oauthIssuer field? Currently this field exists but doesn't appear to be used by OAuthTokenManager during token refresh.

Environment

  • mcpc version: 0.1.7
  • macOS Darwin 25.2.0
  • MCP Server: Notion MCP 1.0.1

Additional Context

  • Notion's OAuth metadata at root returns valid JSON with token_endpoint: "https://mcp.notion.com/token"
  • Other MCP servers (Linear, Amplitude) work because their tokens have longer expiry (~7 days), so refresh is rarely triggered during normal use
  • The oauthIssuer field in profiles.json could potentially be used to specify the correct discovery base URL, but it's currently not utilized during token refresh

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions