Files
basecamp-mcp-server/oauth_app.py
George Antonopoulos b3a6efc5d7 refactor: Remove deprecated files and clean up codebase
This commit deletes obsolete files related to the Basecamp MCP integration, including:
- `basecamp_client.py`
- `basecamp_oauth.py`
- `composio_client_example.py`
- `composio_integration.py`
- `mcp_integration.py`
- `setup.sh`
- `start_basecamp_mcp.sh`

Additionally, a new file `mcp_server_cli.py` is introduced to streamline the MCP server functionality. The README has been updated to reflect these changes and provide clearer setup instructions.

This cleanup aims to enhance maintainability and focus on the core components of the integration.
2025-06-02 17:11:39 +01:00

378 lines
13 KiB
Python

"""
Flask application for handling the Basecamp 3 OAuth 2.0 authorization flow.
This application provides endpoints for:
1. Redirecting users to Basecamp for authorization
2. Handling the OAuth callback
3. Using the obtained token to access the Basecamp API
4. Providing a secure token endpoint for the MCP server
"""
import os
import sys
import json
import secrets
import logging
from flask import Flask, request, redirect, url_for, session, render_template_string, jsonify
from dotenv import load_dotenv
from basecamp_oauth import BasecampOAuth
from basecamp_client import BasecampClient
from search_utils import BasecampSearch
import token_storage
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("oauth_app.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
# Load environment variables
load_dotenv()
# Check for required environment variables
required_vars = ['BASECAMP_CLIENT_ID', 'BASECAMP_CLIENT_SECRET', 'BASECAMP_REDIRECT_URI', 'USER_AGENT']
missing_vars = [var for var in required_vars if not os.getenv(var)]
if missing_vars:
logger.error(f"Missing required environment variables: {', '.join(missing_vars)}")
logger.error("Please set these variables in your .env file or environment")
sys.exit(1)
# Create Flask app
app = Flask(__name__)
app.secret_key = os.getenv('FLASK_SECRET_KEY', secrets.token_hex(16))
# HTML template for displaying results
RESULTS_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<title>Basecamp 3 OAuth Demo</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1 { color: #333; }
pre { background-color: #f5f5f5; padding: 10px; border-radius: 5px; overflow-x: auto; }
.button {
display: inline-block;
background-color: #4CAF50;
color: white;
padding: 10px 20px;
text-decoration: none;
border-radius: 5px;
margin-top: 20px;
}
.container { max-width: 1000px; margin: 0 auto; }
form { margin-top: 20px; }
input[type="text"] { padding: 8px; width: 300px; }
button { padding: 8px 15px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; }
</style>
</head>
<body>
<div class="container">
<h1>{{ title }}</h1>
{% if message %}
<p>{{ message }}</p>
{% endif %}
{% if content %}
<pre>{{ content }}</pre>
{% endif %}
{% if auth_url %}
<a href="{{ auth_url }}" class="button">Log in with Basecamp</a>
{% endif %}
{% if token_info %}
<h2>OAuth Token Information</h2>
<pre>{{ token_info | tojson(indent=2) }}</pre>
{% endif %}
{% if show_logout %}
<a href="/logout" class="button">Logout</a>
{% endif %}
{% if show_home %}
<a href="/" class="button">Home</a>
{% endif %}
</div>
</body>
</html>
"""
@app.template_filter('tojson')
def to_json(value, indent=None):
return json.dumps(value, indent=indent)
def get_oauth_client():
"""Get a configured OAuth client."""
try:
client_id = os.getenv('BASECAMP_CLIENT_ID')
client_secret = os.getenv('BASECAMP_CLIENT_SECRET')
redirect_uri = os.getenv('BASECAMP_REDIRECT_URI')
user_agent = os.getenv('USER_AGENT')
logger.info("Creating OAuth client with config: %s, %s, %s", client_id, redirect_uri, user_agent)
return BasecampOAuth(
client_id=client_id,
client_secret=client_secret,
redirect_uri=redirect_uri,
user_agent=user_agent
)
except Exception as e:
logger.error("Error creating OAuth client: %s", str(e))
raise
@app.route('/')
def home():
"""Home page."""
# Check if we have a stored token
token_data = token_storage.get_token()
if token_data and token_data.get('access_token'):
# We have a token, show token information
access_token = token_data['access_token']
# Mask the token for security
masked_token = f"{access_token[:10]}...{access_token[-10:]}" if len(access_token) > 20 else "***"
token_info = {
"access_token": masked_token,
"account_id": token_data.get('account_id'),
"has_refresh_token": bool(token_data.get('refresh_token')),
"expires_at": token_data.get('expires_at'),
"updated_at": token_data.get('updated_at')
}
logger.info("Home page: User is authenticated")
return render_template_string(
RESULTS_TEMPLATE,
title="Basecamp OAuth Status",
message="You are authenticated with Basecamp!",
token_info=token_info,
show_logout=True
)
else:
# No token, show login button
try:
oauth_client = get_oauth_client()
auth_url = oauth_client.get_authorization_url()
logger.info("Home page: User not authenticated, showing login button")
return render_template_string(
RESULTS_TEMPLATE,
title="Basecamp OAuth Demo",
message="Welcome! Please log in with your Basecamp account to continue.",
auth_url=auth_url
)
except Exception as e:
logger.error("Error getting authorization URL: %s", str(e))
return render_template_string(
RESULTS_TEMPLATE,
title="Error",
message=f"Error setting up OAuth: {str(e)}",
)
@app.route('/auth/callback')
def auth_callback():
"""Handle the OAuth callback from Basecamp."""
logger.info("OAuth callback called with args: %s", request.args)
code = request.args.get('code')
error = request.args.get('error')
if error:
logger.error("OAuth callback error: %s", error)
return render_template_string(
RESULTS_TEMPLATE,
title="Authentication Error",
message=f"Basecamp returned an error: {error}",
show_home=True
)
if not code:
logger.error("OAuth callback: No code provided")
return render_template_string(
RESULTS_TEMPLATE,
title="Error",
message="No authorization code received.",
show_home=True
)
try:
# Exchange the code for an access token
oauth_client = get_oauth_client()
logger.info("Exchanging code for token")
token_data = oauth_client.exchange_code_for_token(code)
logger.info(f"Raw token data from Basecamp exchange: {token_data}")
# Store the token in our secure storage
access_token = token_data.get('access_token')
refresh_token = token_data.get('refresh_token')
expires_in = token_data.get('expires_in')
account_id = os.getenv('BASECAMP_ACCOUNT_ID')
if not access_token:
logger.error("OAuth exchange: No access token received")
return render_template_string(
RESULTS_TEMPLATE,
title="Authentication Error",
message="No access token received from Basecamp.",
show_home=True
)
# Try to get identity if account_id is not set
if not account_id:
try:
logger.info("Getting user identity to find account_id")
identity = oauth_client.get_identity(access_token)
logger.info("Identity response: %s", identity)
# Find Basecamp 3 account
if identity.get('accounts'):
for account in identity['accounts']:
if account.get('product') == 'bc3': # Basecamp 3
account_id = account['id']
logger.info("Found account_id: %s", account_id)
break
except Exception as identity_error:
logger.error("Error getting identity: %s", str(identity_error))
# Continue with the flow, but log the error
logger.info("Storing token with account_id: %s", account_id)
stored = token_storage.store_token(
access_token=access_token,
refresh_token=refresh_token,
expires_in=expires_in,
account_id=account_id
)
if not stored:
logger.error("Failed to store token")
return render_template_string(
RESULTS_TEMPLATE,
title="Error",
message="Failed to store token. Please try again.",
show_home=True
)
# Also keep the access token in session for convenience
session['access_token'] = access_token
if refresh_token:
session['refresh_token'] = refresh_token
if account_id:
session['account_id'] = account_id
logger.info("OAuth flow completed successfully")
return redirect(url_for('home'))
except Exception as e:
logger.error("Error in OAuth callback: %s", str(e), exc_info=True)
return render_template_string(
RESULTS_TEMPLATE,
title="Error",
message=f"Failed to exchange code for token: {str(e)}",
show_home=True
)
@app.route('/api/token', methods=['GET'])
def get_token_api():
"""
Secure API endpoint for the MCP server to get the token.
This should only be accessible by the MCP server.
"""
logger.info("Token API called with headers: %s", request.headers)
# In production, implement proper authentication for this endpoint
# For now, we'll use a simple API key check
api_key = request.headers.get('X-API-Key')
if not api_key or api_key != os.getenv('MCP_API_KEY', 'mcp_secret_key'):
logger.error("Token API: Invalid API key")
return jsonify({
"error": "Unauthorized",
"message": "Invalid or missing API key"
}), 401
token_data = token_storage.get_token()
if not token_data or not token_data.get('access_token'):
logger.error("Token API: No valid token available")
return jsonify({
"error": "Not authenticated",
"message": "No valid token available"
}), 404
logger.info("Token API: Successfully returned token")
return jsonify({
"access_token": token_data['access_token'],
"account_id": token_data.get('account_id')
})
@app.route('/logout')
def logout():
"""Clear the session and token storage."""
logger.info("Logout called")
session.clear()
token_storage.clear_tokens()
return redirect(url_for('home'))
@app.route('/token/info')
def token_info():
"""Display information about the stored token."""
logger.info("Token info called")
token_data = token_storage.get_token()
if not token_data:
logger.info("Token info: No token stored")
return render_template_string(
RESULTS_TEMPLATE,
title="Token Information",
message="No token stored.",
show_home=True
)
# Mask the tokens for security
access_token = token_data.get('access_token', '')
refresh_token = token_data.get('refresh_token', '')
masked_access = f"{access_token[:10]}...{access_token[-10:]}" if len(access_token) > 20 else "***"
masked_refresh = f"{refresh_token[:10]}...{refresh_token[-10:]}" if refresh_token and len(refresh_token) > 20 else "***" if refresh_token else None
display_info = {
"access_token": masked_access,
"has_refresh_token": bool(refresh_token),
"account_id": token_data.get('account_id'),
"expires_at": token_data.get('expires_at'),
"updated_at": token_data.get('updated_at')
}
logger.info("Token info: Returned token info")
return render_template_string(
RESULTS_TEMPLATE,
title="Token Information",
content=json.dumps(display_info, indent=2),
show_home=True
)
@app.route('/health')
def health_check():
"""Health check endpoint."""
logger.info("Health check called")
return jsonify({
"status": "ok",
"service": "basecamp-oauth-app"
})
if __name__ == '__main__':
try:
logger.info("Starting OAuth app on port %s", os.environ.get('PORT', 8000))
# Run the Flask app
port = int(os.environ.get('PORT', 8000))
# Disable debug and auto-reloader when running in production or background
is_debug = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true'
logger.info("Running in %s mode", "debug" if is_debug else "production")
app.run(host='0.0.0.0', port=port, debug=is_debug, use_reloader=is_debug)
except Exception as e:
logger.error("Fatal error: %s", str(e), exc_info=True)
sys.exit(1)