Skip to main content
Glama
nanabrownsnr

Twynity OAuth 2.0 MCP Template

by nanabrownsnr
README.md
# Twynity OAuth 2.0 MCP Template

A production-shaped [FastMCP](https://gofastmcp.com/) server template for building a Twynity MCP where **each user logs in to their own account at an upstream provider** via OAuth 2.0 — like ClickUp, Google, Microsoft, GitHub, etc. Tokens are exchanged through the Twynity Interconnector `oauth` category and stored encrypted in MongoDB, scoped to the exact `(user_id, persona_id)` pair.

This is the right starting point when your MCP needs the user's **own provider credentials** (access/refresh tokens) rather than a shared API key or a static configuration.

## Features

- Twynity Interconnector `oauth` category:
  - `GET /api/v1/auth/{provider}/login?redirect_uri={uri}` — mints a single-use `state` (TTL-expired Mongo `oauth_sessions` collection) and returns the provider authorization URL
  - `POST /api/v1/auth/{provider}/token-callback?redirect_uri={uri}&code={code}` — looks up `state`, exchanges the code for tokens, stores them encrypted in `oauth_tokens`, deletes the one-shot session
  - `POST /api/v1/auth/{provider}/logout` — best-effort provider-side revoke + deletes the stored tokens
- Encrypted access/refresh token storage in MongoDB (Fernet, scoped to `(user_id, persona_id)`)
- `public_metadata` returns only `has_access_token`/`has_refresh_token` — **never the raw tokens** (Interconnector §11.3, enforced by a test)
- `GET /api/v1/external-connection/me` connection-status endpoint
- MCP manifest (`/.well-known/mcp.json`) declaring `external_connections.oauth`
- Twynity JWT auth (audience-checked `JWTVerifier`) + `Persona-Id` scoping
- Tools resolve identity from FastMCP's verified access-token context (no re-verify of the stripped `authorization` header)
- Docker + Render deployment (Python 3.12 pinned)

### OAuth configuration (env vars)

Set these for your upstream provider (see `.env.example`):

- `OAUTH_PROVIDER` — the `{provider}` path segment (e.g. `clickup`)
- `OAUTH_CLIENT_ID` / `OAUTH_CLIENT_SECRET` — your OAuth app credentials
- `OAUTH_REDIRECT_URI` — **the redirect URI Twynity provides**. It must exactly match what you registered with the OAuth provider, or the flow fails silently. This is *not* your callback endpoint — Twynity calls your `token-callback` route after the provider bounces here.
- `OAUTH_AUTHORIZE_URL` / `OAUTH_TOKEN_URL` / `OAUTH_REVOKE_URL` — your provider's endpoints
- `OAUTH_SCOPES` — the scopes to request
- `OAUTH_SESSION_TTL_SECONDS` — how long an unexchanged `state` stays valid

## Setup

### Prerequisites

1. Python 3.11 or higher
2. MongoDB instance (for token + session storage)
3. An OAuth app registered with your upstream provider, configured with the Twynity redirect URI
4. A Twynity account-service URL + the MCP registered on Twynity (so user JWTs carry your hyphenated service ID in `aud`)

### Installation

1. Install the dependencies:

```bash
uv sync --locked
```

2. Copy `.env.example` to `.env` and configure the environment variables:

```bash
cp .env.example .env
```

Edit `.env` with your configuration:
- `MONGODB_URI` - MongoDB connection string for storing encrypted connections
- `ENCRYPTION_KEY` - Fernet encryption key for securing credentials (generate with `uv run python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"`)
- `ACCOUNT_SERVICE_URL` - Twynity account service URL
- `ACCOUNT_SERVICE_JWKS_ENDPOINT` - JWKS endpoint for verifying JWTs
- `ACCOUNT_SERVICE_JWKS_CACHE_TTL` - Cache TTL for JWKS
- `LICENSE_KEY` - License key for usage tracking
- `LICENSE_SERVER_BASE_URL` - License server base URL
- `LICENSE_SERVER_JWKS_ENDPOINT` - JWKS endpoint for license verification
- `LICENSE_SERVER_ACTIVATION_ENDPOINT` - License activation endpoint
- `USAGE_REPORT_ENDPOINT` - Usage reporting endpoint

### Run the Server

```bash
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000
```

## Tools

Replace the `say_hello` example tool with your own. A typical tool reads the user's stored OAuth token:

```python
from fastmcp.tools import ToolResult
from app.tools.connection import get_current_connection

async def my_tool(...) -> ToolResult:
    token = await get_current_connection()  # {"id", "access_token", "refresh_token"}
    # call your upstream API with token["access_token"]
    return ToolResult(content=[...], structured_content={...})
```

`get_current_connection()` returns the decrypted token grant for the verified user/persona, or raises an actionable auth error if the user hasn't completed the OAuth flow yet. Handle expired access tokens by refreshing with `refresh_token` against your provider's token URL.


## Configuration

The server expects a `.env` file with the following variables:

```
# MongoDB for secure connection storage
MONGODB_URI=mongodb://localhost:27017
DATABASE_NAME=twynity_mcp

# Encryption key (required)
ENCRYPTION_KEY=your_fernet_encryption_key_here

# Twynity account service details  
ACCOUNT_SERVICE_URL=https://accounts.twynity.com
ACCOUNT_SERVICE_JWKS_ENDPOINT=/oauth2/keys
ACCOUNT_SERVICE_JWKS_CACHE_TTL=300

# License service details
LICENSE_KEY=
LICENSE_SERVER_BASE_URL=https://license.twynity.com
LICENSE_SERVER_JWKS_ENDPOINT=/oauth2/keys
LICENSE_SERVER_ACTIVATION_ENDPOINT=/activate

# Usage reporting endpoint
USAGE_REPORT_ENDPOINT=https://usage.twynity.com/api/v1/report

# Public URL for the server
PUBLIC_URL=http://localhost:8000

# Environment (development, staging, production)
ENVIRONMENT=development

# Allowed origins for CORS
ALLOWED_ORIGINS=*
```

## Authentication and Security

This server uses Twynity's authentication system which requires:
1. A valid bearer JWT token
2. A `Persona-Id` header identifying the user's project/persona context

Credentials are stored securely encrypted in MongoDB, with each connection scoped to a specific user and persona pair.

## Development

### Running Tests

```bash
uv run pytest -q
```

### Code Quality

```bash
uv run ruff check app tests
```

## Deployment

### Docker

```bash
docker build -t oauth-mcp .
docker run -p 8000:8000 oauth-mcp
```

## License

MIT License - see [LICENSE](LICENSE) for details.