An MCP (Model Context Protocol) server template that provides OAuth 2.0-protected tools to agentic clients. Built with FastMCP and currently integrates with Google APIs.
| Component | Location | Purpose |
|---|---|---|
| FastMCP Server | src/main.py |
Entry point, tool definitions, OAuth routes |
| OAuth Gate | src/auth/oauth_gate.py |
Token validation and OAuth flow initiation |
| OAuthToolApp | src/mcp_tools/auth_tool_app.py |
Base class for OAuth-protected tools |
| Google Provider | src/auth/providers/google_provider.py |
Google OAuth 2.0 implementation |
| Calendar Tools | src/mcp_tools/google/calendar.py |
Google Calendar API wrapper |
| Decorators | src/utils/decorators.py |
Scopes, retry, OAuth error handling |
| Step | Description |
|---|---|
| 1 | Client calls MCP tool (e.g., list_calendars) |
| 2 | @mcp_oauth_handler decorator wraps tool function |
| 3 | OAuthToolApp.run_method passes to ensure_auth |
| 4 | ensure_auth checks for valid token via provider |
| 5 | If no token, raises OAuthRequiredError with elicitation_id |
| 6 | Decorator converts to UrlElicitationRequiredError with auth URL |
| 7 | Client redirects user to /auth/connect/{elicitation_id} |
| 8 | Server redirects to Google OAuth consent screen |
| 9 | After consent, Google redirects to /auth/callback/{elicitation_id} |
| 10 | Server exchanges code for token and stores it |
| 11 | Client retries original tool call with valid token |
assistant-mcp/
├── .env # Environment configuration
├── .gitignore
├── .python-version # Python 3.13
├── pyproject.toml # Dependencies and metadata
├── uv.lock # Dependency lock file
├── CLAUDE.md # Project documentation
├── README.md
│
└── src/
├── main.py # FastMCP server entry point, tool definitions
│
├── auth/
│ ├── oauth_gate.py # OAuth flow management, token elicitation
│ ├── providers/
│ │ ├── provider.py # Abstract OAuthProvider interface
│ │ ├── google_provider.py # Google OAuth implementation
│ │ └── provider_registry.py # Provider lookup by name
│ └── tokens/
│ ├── auth_token.py # Abstract token interface
│ └── google_token.py # Google token implementation
│
├── mcp_tools/
│ ├── auth_tool_app.py # Base class for OAuth-protected tools
│ └── google/
│ └── calendar.py # Google Calendar tool implementations
│
├── utils/
│ ├── decorators.py # @tool_scope_factory, @tool_retry_factory, @mcp_oauth_handler
│ └── errors.py # Custom exceptions (OAuthRequiredError, etc.)
│
└── db/
└── db.py # Database utilities
# Clone the repository
git clone <repo-url>
cd assistant-mcp
# Install dependencies with uv
uv syncCreate a .env file with:
GOOGLE_SECRETS_PATH=./secrets/client_secret.json
GOOGLE_LOCAL_TOKEN_PATH=./secrets/token.json
SERVER_HOST=127.0.0.1
SERVER_PORT=8000| Variable | Description |
|---|---|
GOOGLE_SECRETS_PATH |
Path to Google OAuth client secrets JSON file |
GOOGLE_LOCAL_TOKEN_PATH |
Path where OAuth tokens will be stored |
SERVER_HOST |
Server host address (default: 127.0.0.1) |
SERVER_PORT |
Server port number (default: 8000) |
For local setup, you need to create a Google Cloud project and place a secrets.json file
at the root of this repo to setup the Google OAuth Provider.
uv run python src/main.pyThe server runs at http://{SERVER_HOST}:{SERVER_PORT}/mcp using streamable HTTP transport.
| Tool | Parameters | Output | OAuth Required |
|---|---|---|---|
list_calendars |
(none) | calendar_id, name, description |
Yes |
list_events |
calendar_id, start_time, duration_days |
event_id, name, start, end |
Yes |
create_event |
calendar_id, start, name, duration_minutes, location, description |
event_id, event_details |
Yes |
update_event |
calendar_id, event_id, start, name, duration_minutes, location, description |
event_id, event_details |
Yes |
| Tool | Prerequisite |
|---|---|
list_calendars |
None |
list_events |
calendar_id from list_calendars |
create_event |
calendar_id from list_calendars |
update_event |
calendar_id from list_calendars, event_id from list_events |
calendar_id:'primary'(uses primary calendar if not specified)duration_minutes:30(for create/update)duration_days:7(for list_events)
When a tool requires authentication and no valid token exists:
1. Client calls tool (e.g., list_calendars)
↓
2. OAuthToolApp.run_method invokes ensure_auth
↓
3. ensure_auth checks provider for valid token
↓
4. No token found → OAuthRequiredError with elicitation_id
↓
5. @mcp_oauth_handler converts to UrlElicitationRequiredError
↓
6. MCP client receives auth URL, displays to user
↓
7. User visits /auth/connect/{elicitation_id}
↓
8. Server redirects to Google OAuth consent
↓
9. User authorizes, Google redirects to /auth/callback/{elicitation_id}
↓
10. Server exchanges code for token, stores in JSON file
↓
11. User sees "You may close this tab"
↓
12. Client retries tool call → succeeds with valid token
Initiates OAuth flow by redirecting to Google OAuth consent screen.
Request:
curl -L http://127.0.0.1:8000/auth/connect/{elicitation_id}Response: 302 redirect to Google OAuth consent URL
OAuth callback handler. Receives authorization code and exchanges for token.
Request: (from Google redirect)
http://127.0.0.1:8000/auth/callback/{elicitation_id}?code=...&state=...
Response:
You may close this tab.
Decorators create specialized behavior for tool methods:
@tool_scope_factory(scopes=["https://www.googleapis.com/auth/calendar"])
@tool_retry_factory(error_message="Google Calendar error", retry_on=(HttpError,))
def list_calendars(self, *, token: GoogleToken, ctx: Dict[str, Any]):
...Abstract provider interface allows multiple OAuth implementations:
class OAuthProvider(ABC):
@abstractmethod
def get_access_token(self, principal_id: str, scopes: Sequence[str]) -> Optional[OAuthToken]:
...
@abstractmethod
def generate_auth_url(self, scopes: Sequence[str], elicitation_id: str, ...) -> dict:
...Tool functions compose multiple decorators:
@mcp.tool()
@mcp_oauth_handler("Authorization is required to access your Google Calendar.")
def list_calendars(ctx: Context):
return calendar_tools.run_method('list_calendars', ctx=ctx)@mcp.tool(): Registers function as MCP tool@mcp_oauth_handler: Handles OAuth errors, converts to URL elicitation
- main.py: Tool definitions and MCP routing only
- auth/: All OAuth logic (providers, tokens, flow management)
- mcp_tools/: Tool implementations with business logic
- utils/: Shared utilities (decorators, errors)
To add a new OAuth-protected tool:
# In src/main.py
@mcp.tool()
@mcp_oauth_handler("Authorization message")
def my_tool(ctx: Context, param: str):
"""Docstring becomes tool description"""
return tool_app.run_method('method_name', ctx=ctx, param=param)# In tool app class
@tool_scope_factory(scopes=[...])
@tool_retry_factory(error_message=..., retry_on=(...))
def method_name(self, *, token: GoogleToken, ctx: Dict, param: str):
# Implementation using token.present_creds()
...- msg-agent: Companion MCP client project that consumes these tools using LangGraph
- test_client: A rough test client is provided to test the MCP server