Tutorial

How to Deploy an MCP Agent with End-User OAuth

Deploy an MCP agent that uses end-user OAuth for per-user identity and scoped access. Configure scopes, connection types, and test with real users.

Deploy an MCP agent with end-user OAuth so each user authenticates with their own identity and the agent accesses only resources the user has authorized. Default MCP deployments use a single service account, which means all users share the same permissions and rate limits. End-user OAuth gives each user their own token, enabling per-user rate limiting, audit trails, and scoped data access. This is required for multi-tenant agents that access user-specific data like email, calendar, or private repos.

Prerequisites

  • Python 3.8+ installed
  • An MCP server implementation (FastAPI, Express, or similar)
  • An OAuth 2.0 provider (Google, GitHub, or custom)
  • A Scavio API key from scavio.dev for the search tool

Walkthrough

Step 1: Configure OAuth scopes

Define the OAuth scopes your agent needs and map them to MCP tool permissions.

Python
import os, json

# OAuth configuration
OAUTH_CONFIG = {
    'provider': 'google',  # or github, custom
    'client_id': os.environ.get('OAUTH_CLIENT_ID', ''),
    'client_secret': os.environ.get('OAUTH_CLIENT_SECRET', ''),
    'redirect_uri': 'http://localhost:3000/callback',
    'scopes': [
        'openid',
        'profile',
        'email',
    ],
}

# Map scopes to tool permissions
TOOL_PERMISSIONS = {
    'web_search': {'required_scopes': ['openid']},
    'email_search': {'required_scopes': ['openid', 'email']},
    'calendar_read': {'required_scopes': ['openid', 'https://www.googleapis.com/auth/calendar.readonly']},
}

print(f'Configured {len(TOOL_PERMISSIONS)} tools with scope requirements')

Step 2: Set connection type to end-user

Configure the MCP server to use end-user connection type instead of service account.

Python
# MCP server configuration for end-user OAuth
mcp_config = {
    'mcpServers': {
        'search-agent': {
            'url': 'http://localhost:3000/mcp',
            'connection': {
                'type': 'end_user',
                'oauth': {
                    'provider': OAUTH_CONFIG['provider'],
                    'client_id': OAUTH_CONFIG['client_id'],
                    'scopes': OAUTH_CONFIG['scopes'],
                },
            },
            'tools': list(TOOL_PERMISSIONS.keys()),
        }
    }
}

# Write config
with open('.mcp.json', 'w') as f:
    json.dump(mcp_config, f, indent=2)
print('MCP config written with end-user OAuth')

Step 3: Deploy the agent with token validation

Build the agent endpoint that validates the user's OAuth token before executing tools.

Python
import requests

SCAVIO_KEY = os.environ['SCAVIO_API_KEY']

def validate_token(token: str) -> dict:
    """Validate OAuth token and extract user info."""
    resp = requests.get('https://www.googleapis.com/oauth2/v3/userinfo',
        headers={'Authorization': f'Bearer {token}'}, timeout=10)
    if resp.status_code != 200:
        return {'valid': False, 'error': 'invalid_token'}
    user = resp.json()
    return {'valid': True, 'email': user.get('email', ''), 'sub': user.get('sub', '')}

def execute_tool(tool_name: str, params: dict, user_token: str) -> dict:
    user = validate_token(user_token)
    if not user['valid']:
        return {'error': 'unauthorized', 'message': 'Invalid or expired token'}
    if tool_name == 'web_search':
        resp = requests.post('https://api.scavio.dev/api/v1/search',
            headers={'x-api-key': SCAVIO_KEY},
            json={'platform': 'google', 'query': params.get('query', '')}, timeout=10)
        return {'results': resp.json().get('organic_results', [])[:5], 'user': user['email']}
    return {'error': f'Unknown tool: {tool_name}'}

print('Agent endpoint ready with token validation')

Step 4: Test with end-user identity

Simulate an end-user request and verify the agent correctly uses the user's identity.

Python
def test_end_user_flow():
    # Simulate with a mock token (in production, this comes from OAuth flow)
    mock_token = 'mock_user_token_for_testing'

    # Test the search tool
    result = execute_tool('web_search', {'query': 'python best practices 2026'}, mock_token)
    if 'error' in result and result['error'] == 'unauthorized':
        print('Token validation working (mock token rejected as expected)')
    else:
        print(f'Tool executed for user: {result.get("user", "unknown")}')

    # Test with Scavio directly (no OAuth needed)
    resp = requests.post('https://api.scavio.dev/api/v1/search',
        headers={'x-api-key': SCAVIO_KEY},
        json={'platform': 'google', 'query': 'oauth 2.0 best practices'}, timeout=10)
    results = resp.json().get('organic_results', [])
    print(f'Direct API test: {len(results)} results')

test_end_user_flow()

Step 5: Verify per-user identity isolation

Confirm that each user's requests are isolated and tracked separately.

Python
from collections import defaultdict

user_audit = defaultdict(list)

def audited_tool(tool_name: str, params: dict, user_email: str) -> dict:
    """Execute tool with per-user audit logging."""
    user_audit[user_email].append({
        'tool': tool_name,
        'params': params,
        'timestamp': __import__('time').time(),
    })
    if tool_name == 'web_search':
        resp = requests.post('https://api.scavio.dev/api/v1/search',
            headers={'x-api-key': SCAVIO_KEY},
            json={'platform': 'google', 'query': params.get('query', '')}, timeout=10)
        return {'results': resp.json().get('organic_results', [])[:3]}
    return {}

# Simulate multiple users
audited_tool('web_search', {'query': 'react tutorial'}, 'alice@example.com')
audited_tool('web_search', {'query': 'vue tutorial'}, 'bob@example.com')
audited_tool('web_search', {'query': 'angular tutorial'}, 'alice@example.com')

for user, actions in user_audit.items():
    print(f'{user}: {len(actions)} tool calls')
print('Per-user isolation verified')

Python Example

Python
import requests, os
H = {'x-api-key': os.environ['SCAVIO_API_KEY']}

def user_search(query, user_email):
    data = requests.post('https://api.scavio.dev/api/v1/search', headers=H,
        json={'platform': 'google', 'query': query}).json()
    results = data.get('organic_results', [])[:3]
    print(f'[{user_email}] {query}: {len(results)} results')
    return results

user_search('python oauth tutorial', 'user@example.com')

JavaScript Example

JavaScript
const H = {'x-api-key': process.env.SCAVIO_API_KEY, 'Content-Type': 'application/json'};
async function userSearch(query, userEmail) {
  const r = await fetch('https://api.scavio.dev/api/v1/search', {
    method: 'POST', headers: H, body: JSON.stringify({platform: 'google', query})
  });
  const results = (await r.json()).organic_results || [];
  console.log(`[${userEmail}] ${query}: ${results.length} results`);
  return results.slice(0, 3);
}
userSearch('oauth best practices', 'user@example.com');

Expected Output

JSON
An MCP agent deployed with end-user OAuth where each user authenticates independently, tool calls are scoped to user permissions, and all actions are audited per user.

Related Tutorials

Frequently Asked Questions

Most developers complete this tutorial in 15 to 30 minutes. You will need a Scavio API key (free tier works) and a working Python or JavaScript environment.

Python 3.8+ installed. An MCP server implementation (FastAPI, Express, or similar). An OAuth 2.0 provider (Google, GitHub, or custom). A Scavio API key from scavio.dev for the search tool. A Scavio API key gives you 250 free credits per month.

Yes. The free tier includes 250 credits per month, which is more than enough to complete this tutorial and prototype a working solution.

Scavio has a native LangChain package (langchain-scavio), an MCP server, and a plain REST API that works with any HTTP client. This tutorial uses the raw REST API, but you can adapt to your framework of choice.

Start Building

Deploy an MCP agent that uses end-user OAuth for per-user identity and scoped access. Configure scopes, connection types, and test with real users.