-
Notifications
You must be signed in to change notification settings - Fork 2
feat(mcp): OAuth 2.1 support for remote MCP servers (e.g. Todoist) #1930
Description
Problem
Remote MCP servers that require OAuth 2.1 authorization (e.g. https://ai.todoist.net/mcp) cannot be used with Zeph. The current McpClient::connect_url() in crates/zeph-mcp/src/client.rs uses StreamableHttpClientTransport::from_uri() without any auth headers or OAuth flow.
Library Support
rmcp already supports OAuth 2.1 via the auth feature flag (verified in rmcp v1.2.0 docs):
- Full OAuth 2.1 with PKCE (S256)
- Protected Resource Metadata discovery (RFC 9728)
- Authorization Server Metadata discovery (RFC 8414)
- Dynamic client registration (RFC 7591)
- Automatic token refresh
OAuthStatestate machine +AuthClienttransport
Currently Zeph uses rmcp with these features in crates/zeph-mcp/Cargo.toml:
rmcp = { workspace = true, features = ["client", "transport-child-process", "transport-streamable-http-client-reqwest"] }Need to add auth feature and wire the OAuth flow.
Proposed Solution
1. Add auth feature to rmcp dependency
rmcp = { workspace = true, features = ["client", "transport-child-process", "transport-streamable-http-client-reqwest", "auth"] }2. Extend config with two auth modes
Mode A — Static Bearer token (simple, for APIs with API keys like Todoist REST):
[[mcp.servers]]
id = "todoist"
url = "https://api.todoist.com/mcp/v1"
timeout = 30
[mcp.servers.headers]
Authorization = "Bearer ${TODOIST_API_TOKEN}"Mode B — OAuth 2.1 (for servers like ai.todoist.net requiring browser auth):
[[mcp.servers]]
id = "todoist-oauth"
url = "https://ai.todoist.net/mcp"
timeout = 30
[mcp.servers.oauth]
enabled = true
token_storage = "vault" # persist tokens in zeph vault across restarts3. OAuth flow for non-interactive contexts (CLI/Telegram)
Since Zeph runs headless (CLI, Telegram bot), the OAuth flow needs to handle the browser redirect:
- On first connect: print authorization URL to console / send via Telegram channel
- Start local callback server on
localhost:PORTto receive the code - Exchange code → tokens → store in vault
- On subsequent starts: load tokens from vault, refresh automatically
4. Changes required
| File | Change |
|---|---|
crates/zeph-mcp/Cargo.toml |
Add auth to rmcp features |
crates/zeph-mcp/src/client.rs |
Add connect_url_with_headers() and connect_url_oauth() |
crates/zeph-mcp/src/manager.rs |
Extend McpTransport::Http with optional headers and oauth fields |
crates/zeph-core/src/config/ |
Add headers and oauth fields to McpServerConfig |
crates/zeph-core/src/config/default.toml |
Document new fields |
src/agent_setup.rs |
Handle OAuth token acquisition on startup |
Acceptance Criteria
- Static
headersconfig works: token read from vault, passed as Bearer header - OAuth 2.1 flow works: authorization URL printed/sent, callback handled, tokens stored in vault
- Token auto-refresh works across sessions
- Todoist remote MCP (
https://ai.todoist.net/mcp) connects successfully - CLI: authorization URL printed to stdout with instructions
- Telegram: authorization URL sent as message to the user
- TUI: authorization URL shown in status panel with spinner `Waiting for OAuth..."
References
- rmcp OAuth docs: https://github.com/modelcontextprotocol/rust-sdk/blob/main/docs/OAUTH_SUPPORT.md
- rmcp OAuth example:
examples/clients/src/auth/oauth_client.rs - MCP Authorization spec: https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization/