Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 50 additions & 10 deletions docs/mcp_oauth_servers.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,31 @@ This feature adds automatic OAuth 2.0 authentication support for remote MCP serv
- **🛡️ PKCE Security**: Implements RFC 7636 (Proof Key for Code Exchange) for secure public client authentication
- **🌐 Public Client Support**: Works with servers that don't require client_id (like Notion MCP)
- **🔄 Token Management**: Automatic token refresh and expiry handling
- **🚫 Extension Filtering**: Filters out browser extension interference during OAuth callbacks
- **🚫 Extension Filtering**: Filters out browser extension interference during OAuth callbacks (web)
- **🖥️ Desktop Support**: Full OAuth support for Windows, macOS, and Linux desktop applications

## Platform Support

**✅ Web Platform**: Full OAuth support with popup-based authentication flow
**❌ Mobile/Desktop**: OAuth authentication is **web-only** due to browser security requirements
**✅ Desktop Platform** (Windows, macOS, Linux): Full OAuth support with external browser and local callback server
**❌ Mobile**: OAuth authentication is not yet supported on mobile platforms

On non-web platforms:
- OAuth discovery still works (detects requirements)
- OAuth authentication throws `UnsupportedError`
- Fallback to manual configuration or other auth methods
On desktop platforms:
- OAuth discovery works automatically
- OAuth authentication opens external browser for user authorization
- Local HTTP server handles OAuth callback securely
- Same PKCE security as web implementation

## Tested OAuth Providers

- ✅ **Notion MCP** (`https://mcp.notion.com/mcp`)
- ✅ **Atlassian MCP** (specific URL varies)
- ✅ **Atlassian MCP** (`https://mcp.atlassian.com/v1/mcp`)
- 🔄 **Other RFC 8414 compliant servers** (should work automatically)

## How It Works

### Web Platform

1. **Discovery Phase**: When you enter an MCP server URL, the system:
- Checks `/.well-known/oauth-authorization-server` for OAuth metadata
- Attempts dynamic client registration if available
Expand All @@ -45,8 +50,23 @@ On non-web platforms:
- Handles token refresh when needed
- Validates token expiry

### Desktop Platform (Windows, macOS, Linux)

1. **Discovery Phase**: Same as web platform

2. **Authentication Phase**:
- Starts local HTTP callback server on available port
- Opens system default browser for OAuth authorization
- Handles PKCE code challenge/verifier generation
- Receives OAuth callback via local server with state validation
- Exchanges authorization code for access token
- Automatically closes local server after completion

3. **Usage Phase**: Same as web platform

## Architecture

### Web Platform
```
┌─────────────────┐ ┌────────────────────┐ ┌─────────────────┐
│ MCP Server │ │ OAuth Discovery │ │ OAuth Handler │
Expand All @@ -65,6 +85,25 @@ On non-web platforms:
└────────────────────┘
```

### Desktop Platform
```
┌─────────────────┐ ┌────────────────────┐ ┌─────────────────┐
│ MCP Server │ │ OAuth Discovery │ │ OAuth Handler │
│ │◄──►│ │◄──►│ │
│ /.well-known/ │ │ RFC 8414 Compliant │ │ PKCE + Browser │
│ oauth-auth... │ │ │ │ Local Server │
└─────────────────┘ └────────────────────┘ └─────────────────┘
│ │
▼ ▼
┌────────────────────┐ ┌─────────────────┐
│ MCP Client │ │ HTTP Server │
│ │ │ localhost:port │
│ Bearer Token Auth │ │ OAuth Callback │
│ StreamableClient │ │ │
│ SSEClient │ │ │
└────────────────────┘ └─────────────────┘
```

## Usage

1. Go to **Settings → MCP Servers**
Expand All @@ -78,13 +117,14 @@ On non-web platforms:

- **PKCE Protection**: Prevents authorization code interception attacks
- **State Parameter Validation**: Prevents CSRF attacks
- **Origin Validation**: Ensures callbacks come from expected sources
- **Browser Extension Filtering**: Ignores interference from development tools
- **Origin Validation**: Ensures callbacks come from expected sources (web)
- **Local Server Security**: Uses localhost-only binding for OAuth callbacks (desktop)
- **Browser Extension Filtering**: Ignores interference from development tools (web)
- **Token Expiry Handling**: Automatic refresh before expiration

## Future Enhancements

- Mobile/Desktop OAuth support via external browser
- Mobile OAuth support via external browser
- Additional OAuth flows (device code, etc.)
- OAuth provider-specific optimizations
- Enhanced error handling and user feedback
Expand Down
30 changes: 13 additions & 17 deletions lib/provider/mcp_server_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import 'package:chatmcp/utils/storage_manager.dart';
import '../mcp/client/mcp_client_interface.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../mcp/models/server.dart';
import '../utils/oauth_web.dart' if (dart.library.io) '../utils/oauth_stub.dart';
import '../utils/oauth_web.dart' if (dart.library.io) '../utils/oauth_desktop.dart';
import '../utils/oauth_discovery.dart';

var defaultInMemoryServers = [
Expand Down Expand Up @@ -437,10 +437,7 @@ class McpServerProvider extends ChangeNotifier {

/// Discovers OAuth configuration for a server URL automatically
Future<OAuthDiscoveryResult> discoverOAuthForServer(String serverUrl) async {
if (!kIsWeb) {
return OAuthDiscoveryResult(requiresOAuth: false);
}

// OAuth discovery works on all platforms now (web and desktop)
try {
Logger.root.info('Discovering OAuth for server: $serverUrl');
final result = await OAuthDiscoveryService.discoverOAuth(serverUrl);
Expand All @@ -463,7 +460,7 @@ class McpServerProvider extends ChangeNotifier {

/// Automatically configures and authenticates a server with discovered OAuth
Future<bool> autoAuthenticateServer(String serverName, OAuthDiscoveryResult oauthConfig) async {
if (!kIsWeb || !oauthConfig.requiresOAuth) {
if (!oauthConfig.requiresOAuth) {
return false;
}

Expand All @@ -473,7 +470,7 @@ class McpServerProvider extends ChangeNotifier {
// Use discovered client ID or null for public clients (like Notion MCP)
String? clientId = oauthConfig.clientId;
String scope = oauthConfig.scope ?? 'read write';
String redirectUri = oauthConfig.redirectUri ?? '${Uri.base.origin}/oauth_callback.html';
String redirectUri = oauthConfig.redirectUri ?? 'http://localhost:0';

Logger.root.info('Using clientId: $clientId, scope: $scope, redirectUri: $redirectUri');

Expand All @@ -485,14 +482,18 @@ class McpServerProvider extends ChangeNotifier {
scope: scope,
);

// Use the actual redirect URI from the auth result if available (desktop)
// or fall back to the configured redirect URI (web)
final actualRedirectUri = authResult['redirect_uri'] as String? ?? redirectUri;

// Exchange code for token
final tokenResult = await WebOAuthHandler.exchangeCodeForToken(
tokenUrl: oauthConfig.tokenUrl!,
clientId: clientId,
clientSecret: null, // Public clients don't require client secret
code: authResult['code'] as String,
codeVerifier: authResult['code_verifier'] as String,
redirectUri: redirectUri,
redirectUri: actualRedirectUri,
);

// Update server configuration with OAuth info and tokens
Expand Down Expand Up @@ -564,10 +565,6 @@ class McpServerProvider extends ChangeNotifier {
throw Exception('OAuth not enabled for server: $serverName');
}

if (!kIsWeb) {
throw Exception('OAuth is only supported on web platform');
}

// Extract OAuth parameters with debugging
final authorizationUrl = oauth['authorization_url'] as String? ?? '';
final clientId = oauth['client_id'] as String? ?? '';
Expand All @@ -588,14 +585,17 @@ class McpServerProvider extends ChangeNotifier {
scope: scope,
);

// Use the actual redirect URI from the auth result if available (desktop)
final actualRedirectUri = authResult['redirect_uri'] as String? ?? redirectUri;

// Exchange code for token
final tokenResult = await WebOAuthHandler.exchangeCodeForToken(
tokenUrl: oauth['token_url'] as String,
clientId: oauth['client_id'] as String,
clientSecret: oauth['client_secret'] as String?,
code: authResult['code'] as String,
codeVerifier: authResult['code_verifier'] as String,
redirectUri: oauth['redirect_uri'] as String,
redirectUri: actualRedirectUri,
);

// Update server config with tokens
Expand Down Expand Up @@ -640,10 +640,6 @@ class McpServerProvider extends ChangeNotifier {
throw Exception('No refresh token available for server: $serverName');
}

if (!kIsWeb) {
throw Exception('OAuth is only supported on web platform');
}

// Refresh token
final tokenResult = await WebOAuthHandler.refreshToken(
tokenUrl: oauth['token_url'] as String,
Expand Down
Loading