Add HTTP URL-based attachment serving for Gmail attachments
This commit implements a new feature that allows Gmail attachments to be
served via HTTP URLs instead of returning base64-encoded data in the tool
response. This avoids consuming LLM context window space and token budgets
for large attachments.
Architecture:
-------------
The implementation works in both stdio and streamable-http transport modes:
1. Temp File Storage (core/attachment_storage.py):
- New AttachmentStorage class manages temporary file storage in ./tmp/attachments/
- Uses UUID-based file IDs to prevent guessing/unauthorized access
- Tracks metadata: filename, mime type, size, creation/expiration times
- Files expire after 1 hour (configurable) with automatic cleanup support
- Handles base64 decoding and file writing
2. HTTP Route Handlers:
- Added /attachments/{file_id} route to main FastMCP server (streamable-http mode)
- Added same route to MinimalOAuthServer (stdio mode)
- Both routes serve files with proper Content-Type headers via FileResponse
- Returns 404 for expired or missing attachments
3. Modified get_gmail_attachment_content():
- Now saves attachments to temp storage and returns HTTP URL
- Attempts to fetch filename/mimeType from message metadata (best effort)
- Handles stateless mode gracefully (skips file saving, shows preview)
- Falls back to base64 preview if file saving fails
- URL generation respects WORKSPACE_EXTERNAL_URL for reverse proxy setups
Key Features:
-------------
- Works in both stdio and streamable-http modes (uses existing HTTP servers)
- Respects stateless mode (no file writes when WORKSPACE_MCP_STATELESS_MODE=true)
- Secure: UUID-based file IDs prevent unauthorized access
- Automatic expiration: Files cleaned up after 1 hour
- Reverse proxy support: Uses WORKSPACE_EXTERNAL_URL if configured
- Graceful degradation: Falls back to preview if storage fails
Benefits:
---------
- Avoids context window bloat: Large attachments don't consume LLM tokens
- Better performance: Clients can stream/download files directly
- More efficient: No need to decode base64 in client applications
- Works across network boundaries: URLs accessible from any client
The feature maintains backward compatibility - if file saving fails or stateless
mode is enabled, the function falls back to showing a base64 preview.
This commit is contained in:
@@ -13,6 +13,7 @@ import socket
|
||||
import uvicorn
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -39,6 +40,8 @@ class MinimalOAuthServer:
|
||||
|
||||
# Setup the callback route
|
||||
self._setup_callback_route()
|
||||
# Setup attachment serving route
|
||||
self._setup_attachment_route()
|
||||
|
||||
def _setup_callback_route(self):
|
||||
"""Setup the OAuth callback route."""
|
||||
@@ -89,6 +92,35 @@ class MinimalOAuthServer:
|
||||
logger.error(error_message_detail, exc_info=True)
|
||||
return create_server_error_response(str(e))
|
||||
|
||||
def _setup_attachment_route(self):
|
||||
"""Setup the attachment serving route."""
|
||||
from core.attachment_storage import get_attachment_storage
|
||||
|
||||
@self.app.get("/attachments/{file_id}")
|
||||
async def serve_attachment(file_id: str, request: Request):
|
||||
"""Serve a stored attachment file."""
|
||||
storage = get_attachment_storage()
|
||||
metadata = storage.get_attachment_metadata(file_id)
|
||||
|
||||
if not metadata:
|
||||
return JSONResponse(
|
||||
{"error": "Attachment not found or expired"},
|
||||
status_code=404
|
||||
)
|
||||
|
||||
file_path = storage.get_attachment_path(file_id)
|
||||
if not file_path:
|
||||
return JSONResponse(
|
||||
{"error": "Attachment file not found"},
|
||||
status_code=404
|
||||
)
|
||||
|
||||
return FileResponse(
|
||||
path=str(file_path),
|
||||
filename=metadata["filename"],
|
||||
media_type=metadata["mime_type"]
|
||||
)
|
||||
|
||||
def start(self) -> tuple[bool, str]:
|
||||
"""
|
||||
Start the minimal OAuth server.
|
||||
|
||||
Reference in New Issue
Block a user