mirror of
https://github.com/mcp-use/mcp-use.git
synced 2026-09-14 18:06:01 +08:00
9e592b4071
* removed useless anyio and aiohttp dependency * version matrix in unit tests * change test names * fix weird import from langchain * new deps * bump fastembed minimal * bump uvicorn, remove unused config for pytest * add pre commit to dev deps * remove transitive deps starlette and uvicorn * test fix
213 lines
7.3 KiB
Python
213 lines
7.3 KiB
Python
"""OAuth callback server implementation."""
|
|
|
|
import asyncio
|
|
import html
|
|
from dataclasses import dataclass
|
|
|
|
import uvicorn
|
|
from starlette.applications import Starlette
|
|
from starlette.requests import Request
|
|
from starlette.responses import HTMLResponse
|
|
from starlette.routing import Route
|
|
|
|
from mcp_use.logging import logger
|
|
|
|
|
|
@dataclass
|
|
class CallbackResponse:
|
|
"""Response data from OAuth callback."""
|
|
|
|
code: str | None = None # Authorization code (success)
|
|
state: str | None = None # CSRF protection token
|
|
error: str | None = None # Errors code (if failed)
|
|
error_description: str | None = None
|
|
error_uri: str | None = None
|
|
|
|
|
|
class OAuthCallbackServer:
|
|
"""Local server to handle OAuth callback."""
|
|
|
|
def __init__(self, port: int):
|
|
"""Initialize the callback server.
|
|
|
|
Args:
|
|
port: Port to listen on.
|
|
"""
|
|
self.port = port
|
|
self.redirect_uri: str | None = None
|
|
# Thread safe way to pass callback data to the main OAuth flow
|
|
self.response_queue: asyncio.Queue[CallbackResponse] = asyncio.Queue(maxsize=1)
|
|
self.server: uvicorn.Server | None = None
|
|
|
|
async def start(self) -> str:
|
|
"""Start the callback server and return the redirect URI."""
|
|
app = self._create_app()
|
|
|
|
# Create the server
|
|
config = uvicorn.Config(
|
|
app,
|
|
host="127.0.0.1",
|
|
port=self.port,
|
|
log_level="error", # Suppress uvicorn logs
|
|
)
|
|
self.server = uvicorn.Server(config)
|
|
|
|
# Start server in background
|
|
self._server_task = asyncio.create_task(self.server.serve())
|
|
|
|
# Wait a moment for server to start
|
|
await asyncio.sleep(0.1)
|
|
|
|
self.redirect_uri = f"http://localhost:{self.port}/callback"
|
|
return self.redirect_uri
|
|
|
|
async def wait_for_code(self, timeout: float = 300) -> CallbackResponse:
|
|
"""Wait for the OAuth callback with a timeout (default 5 minutes)."""
|
|
try:
|
|
response = await asyncio.wait_for(self.response_queue.get(), timeout=timeout)
|
|
return response
|
|
except TimeoutError:
|
|
raise TimeoutError(f"OAuth callback not received within {timeout} seconds") from None
|
|
finally:
|
|
await self.shutdown()
|
|
|
|
async def shutdown(self):
|
|
"""Shutdown the callback server."""
|
|
if self.server:
|
|
self.server.should_exit = True
|
|
if hasattr(self, "_server_task"):
|
|
try:
|
|
await asyncio.wait_for(self._server_task, timeout=5.0)
|
|
except TimeoutError:
|
|
self._server_task.cancel()
|
|
|
|
def _create_app(self) -> Starlette:
|
|
"""Create the Starlette application."""
|
|
|
|
async def callback(request: Request) -> HTMLResponse:
|
|
"""Handle the OAuth callback."""
|
|
params = request.query_params
|
|
|
|
# Extract OAuth parameters
|
|
response = CallbackResponse(
|
|
code=params.get("code"),
|
|
state=params.get("state"),
|
|
error=params.get("error"),
|
|
error_description=params.get("error_description"),
|
|
error_uri=params.get("error_uri"),
|
|
)
|
|
|
|
# Log the callback response
|
|
logger.debug(
|
|
f"OAuth callback received: error={response.error}, error_description={response.error_description}"
|
|
)
|
|
if response.code:
|
|
logger.debug("OAuth callback received authorization code")
|
|
else:
|
|
logger.error(f"OAuth callback error: {response.error} - {response.error_description}")
|
|
|
|
# Put response in queue
|
|
try:
|
|
self.response_queue.put_nowait(response)
|
|
except asyncio.QueueFull:
|
|
pass # Ignore if queue is already full
|
|
|
|
# Return success page
|
|
if response.code:
|
|
html = self._success_html()
|
|
else:
|
|
html = self._error_html(response.error, response.error_description)
|
|
|
|
return HTMLResponse(content=html)
|
|
|
|
routes = [Route("/callback", callback)]
|
|
return Starlette(routes=routes)
|
|
|
|
def _success_html(self) -> str:
|
|
"""HTML response for successful authorization."""
|
|
return """
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Authorization Successful</title>
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
height: 100vh;
|
|
margin: 0;
|
|
background-color: #f5f5f5;
|
|
}
|
|
.container {
|
|
text-align: center;
|
|
padding: 2rem;
|
|
background: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
h1 { color: #22c55e; margin-bottom: 0.5rem; }
|
|
p { color: #666; margin-top: 0.5rem; }
|
|
.icon { font-size: 48px; margin-bottom: 1rem; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="icon">✅</div>
|
|
<h1>Authorization Successful!</h1>
|
|
<p>You can now close this window and return to your application.</p>
|
|
</div>
|
|
<script>
|
|
// Auto-close after 3 seconds
|
|
setTimeout(() => window.close(), 3000);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
def _error_html(self, error: str | None, description: str | None) -> str:
|
|
"""HTML response for authorization error."""
|
|
error_msg = html.escape(error or "Unknown error")
|
|
desc_msg = html.escape(description or "Authorization was not completed successfully.")
|
|
|
|
return f"""
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Authorization Error</title>
|
|
<style>
|
|
body {{
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
height: 100vh;
|
|
margin: 0;
|
|
background-color: #f5f5f5;
|
|
}}
|
|
.container {{
|
|
text-align: center;
|
|
padding: 2rem;
|
|
background: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
max-width: 500px;
|
|
}}
|
|
h1 {{ color: #ef4444; margin-bottom: 0.5rem; }}
|
|
.error {{ color: #dc2626; font-weight: 600; margin: 1rem 0; }}
|
|
.description {{ color: #666; margin-top: 0.5rem; }}
|
|
.icon {{ font-size: 48px; margin-bottom: 1rem; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="icon">❌</div>
|
|
<h1>Authorization Error</h1>
|
|
<p class="error">{error_msg}</p>
|
|
<p class="description">{desc_msg}</p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|