99import base64
1010import logging
1111import uuid
12- from datetime import datetime , timedelta , timezone
12+
1313
1414import httpx
1515from azure .identity import DefaultAzureCredential , ManagedIdentityCredential , get_bearer_token_provider
1616from azure .storage .blob import (
17- BlobSasPermissions ,
18- BlobServiceClient ,
19- ContentSettings ,
20- generate_blob_sas ,
17+ BlobServiceClient ,
18+ ContentSettings ,
2119)
2220
2321from config .settings import config
2624logger = logging .getLogger (__name__ )
2725
2826_IMAGE_API_VERSION = "2025-04-01-preview"
29- _SAS_VALIDITY_DAYS = 7
27+
3028
3129
3230def _get_credential ():
@@ -48,11 +46,14 @@ def _ensure_container(blob_service: BlobServiceClient, container_name: str) -> N
4846
4947
5048def _upload_png_and_get_url (png_bytes : bytes ) -> str :
51- """Upload PNG bytes to blob storage, return an accessible URL.
49+ """Upload PNG bytes to blob storage, return a relative backend-proxy path.
50+
5251
53- Prefers the backend image-proxy URL (BACKEND_URL/api/v4/images/{blob}) so
54- the browser never needs direct blob access. Falls back to a user-delegation
55- SAS URL, or bare blob URL as a last resort.
52+ Returns "/api/v4/images/{blob}" — a relative path the browser resolves
53+ against the SPA's configured API origin. The backend proxy reads the blob
54+ with its own credential, so the browser never needs direct blob access (no
55+ SAS or public blob access required), and it works in both the standard and
56+ WAF/private-networking deployments.
5657 """
5758 if not config .azure_storage_blob_url :
5859 raise RuntimeError ("AZURE_STORAGE_BLOB_URL is not configured on the MCP server" )
@@ -72,33 +73,16 @@ def _upload_png_and_get_url(png_bytes: bytes) -> str:
7273 content_settings = ContentSettings (content_type = "image/png" ),
7374 )
7475
75- # Prefer backend proxy URL — the browser fetches via the backend which has
76- # its own credential for blob access; no SAS or public access needed.
77- if config .backend_url :
78- backend_origin = config .backend_url .rstrip ("/" )
79- return f"{ backend_origin } /api/v4/images/{ blob_name } "
8076
81- # Fallback: generate a user-delegation SAS URL for direct blob access.
82- blob_url = f"{ account_url } /{ container_name } /{ blob_name } "
83- try :
84- now = datetime .now (timezone .utc )
85- delegation_key = blob_service .get_user_delegation_key (
86- key_start_time = now - timedelta (minutes = 5 ),
87- key_expiry_time = now + timedelta (days = _SAS_VALIDITY_DAYS ),
88- )
89- sas = generate_blob_sas (
90- account_name = blob_service .account_name ,
91- container_name = container_name ,
92- blob_name = blob_name ,
93- user_delegation_key = delegation_key ,
94- permission = BlobSasPermissions (read = True ),
95- expiry = now + timedelta (days = _SAS_VALIDITY_DAYS ),
96- start = now - timedelta (minutes = 5 ),
97- )
98- return f"{ blob_url } ?{ sas } "
99- except Exception as exc : # noqa: BLE001
100- logger .warning ("Failed to generate user-delegation SAS, returning bare URL: %s" , exc )
101- return blob_url
77+ # Return a relative backend proxy path. The browser resolves it against the
78+ # SPA's configured API origin, so it works in both the standard deployment
79+ # (resolves to the public backend) and the WAF/private deployment (resolves
80+ # to the frontend's same-origin reverse proxy, which forwards to the internal
81+ # backend over the VNet). The backend proxy reads the blob with its own
82+ # credential, so no SAS or public blob access is needed.
83+ return f"/api/v4/images/{ blob_name } "
84+
85+
10286
10387
10488class ImageService (MCPToolBase ):
0 commit comments