Skip to content

Commit 493ad43

Browse files
TravisHilbertCopilot
andcommitted
added mcp server for content gen
Co-authored-by: Copilot <copilot@github.com>
1 parent a093743 commit 493ad43

8 files changed

Lines changed: 195 additions & 13 deletions

File tree

data/agent_teams/content_gen.json

Lines changed: 28 additions & 12 deletions
Large diffs are not rendered by default.

docs/TroubleShootingSteps.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Use these as quick reference guides to unblock your deployments.
4848
| **Unauthorized - Operation cannot be completed without additional quota** | Insufficient quota for requested operation | <ul><li>Check your quota usage using:<br>`az vm list-usage --location "<Location>" -o table`</li><li> To request more quota refer to [VM Quota Request](https://techcommunity.microsoft.com/blog/startupsatmicrosoftblog/how-to-increase-quota-for-specific-types-of-azure-virtual-machines/3792394)</li></ul> |
4949
| **CrossTenantDeploymentNotPermitted** | Deployment across different Azure AD tenants not allowed | <ul><li> **Check tenant match:** Ensure your deployment identity (user/SP) and the target resource group are in the same tenant:<br>`az account show`<br>`az group show --name <RG_NAME>`</li><li> **Verify pipeline/service principal:** If using CI/CD, confirm the service principal belongs to the same tenant and has permissions on the resource group</li><li> **Avoid cross-tenant references:** Make sure your Bicep doesn't reference subscriptions, resource groups, or resources in another tenant</li><li> **Test minimal deployment:** Deploy a simple resource to the same resource group to confirm identity and tenant are correct</li><li> **Guest/external accounts:** Avoid using guest users from other tenants; use native accounts or SPs in the tenant</li></ul> |
5050
| **RequestDisallowedByPolicy** | Azure Policy blocking the requested operation | <ul><li> This typically indicates that an Azure Policy is preventing the requested action due to policy restrictions in your subscription</li><li> For more details and guidance on resolving this issue, refer to: [RequestDisallowedByPolicy](https://learn.microsoft.com/en-us/troubleshoot/azure/azure-kubernetes/create-upgrade-delete/error-code-requestdisallowedbypolicy) </li></ul> |
51-
| **SpecialFeatureOrQuotaIdRequired** | Subscription lacks access to specific Azure OpenAI models | This error occurs when your subscription does not have access to certain Azure OpenAI models.<br><br>**Example error message:**<br>`SpecialFeatureOrQuotaIdRequired: The current subscription does not have access to this model 'Format:OpenAI,Name:o3,Version:2025-04-16'.`<br><br>**Resolution:**<br>To gain access, submit a request using the official form:<br>👉 [Azure OpenAI Model Access Request](https://customervoice.microsoft.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR7en2Ais5pxKtso_Pz4b1_xUQ1VGQUEzRlBIMVU2UFlHSFpSNkpOR0paRSQlQCN0PWcu)<br><br>You'll need to use this form if you require access to the following restricted models:<br><ul><li> gpt-5</li><li> o3</li><li> o3-pro</li><li> deep research</li><li> reasoning summary</li><li> gpt-image-1</li></ul>Once your request is approved, redeploy your resource. |
51+
| **SpecialFeatureOrQuotaIdRequired** | Subscription lacks access to specific Azure OpenAI models | This error occurs when your subscription does not have access to certain Azure OpenAI models.<br><br>**Example error message:**<br>`SpecialFeatureOrQuotaIdRequired: The current subscription does not have access to this model 'Format:OpenAI,Name:o3,Version:2025-04-16'.`<br><br>**Resolution:**<br>To gain access, submit a request using the official form:<br>👉 [Azure OpenAI Model Access Request](https://customervoice.microsoft.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR7en2Ais5pxKtso_Pz4b1_xUQ1VGQUEzRlBIMVU2UFlHSFpSNkpOR0paRSQlQCN0PWcu)<br><br>You'll need to use this form if you require access to the following restricted models:<br><ul><li> gpt-5</li><li> o3</li><li> o3-pro</li><li> deep research</li><li> reasoning summary</li><li> gpt-5-mini</li></ul>Once your request is approved, redeploy your resource. |
5252
| **ResourceProviderError** | Resource provider not registered in subscription | <ul><li> This error occurs when the resource provider is not registered in your subscription</li><li> To register it, refer to [Register Resource Provider](https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/error-register-resource-provider?tabs=azure-cli) documentation </li></ul>|
5353

5454
--------------------------------

src/mcp_server/config/settings.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ class MCPServerConfig(BaseSettings):
3636
# Dataset path - added to handle the environment variable
3737
dataset_path: str = Field(default="./datasets")
3838

39+
# Image-generation settings (used by ImageService)
40+
azure_openai_endpoint: Optional[str] = Field(default=None)
41+
azure_openai_image_deployment: str = Field(default="gpt-5-mini")
42+
azure_storage_blob_url: Optional[str] = Field(default=None)
43+
azure_storage_images_container: str = Field(default="generated-images")
44+
azure_client_id: Optional[str] = Field(default=None)
45+
3946

4047
# Global configuration instance
4148
config = MCPServerConfig()

src/mcp_server/core/factory.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class Domain(Enum):
1919
RETAIL = "retail"
2020
GENERAL = "general"
2121
DATA = "data"
22+
IMAGE = "image"
2223

2324

2425
class MCPToolBase(ABC):

src/mcp_server/mcp_server.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from core.factory import MCPToolFactory
1111
from fastmcp.server.auth.providers.jwt import JWTVerifier
1212
from services.hr_service import HRService
13+
from services.image_service import ImageService
1314
from services.marketing_service import MarketingService
1415
from services.product_service import ProductService
1516
from services.tech_support_service import TechSupportService
@@ -26,6 +27,7 @@
2627
factory.register_service(TechSupportService())
2728
factory.register_service(MarketingService())
2829
factory.register_service(ProductService())
30+
factory.register_service(ImageService())
2931

3032

3133

src/mcp_server/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ dependencies = [
2525
"httpx==0.28.1",
2626
"werkzeug==3.1.5",
2727
"urllib3==2.6.3",
28+
"azure-storage-blob==12.25.1",
2829
]
2930

3031
[project.optional-dependencies]
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"""
2+
Image generation MCP tools service.
3+
4+
Generates images via Azure OpenAI's images/generations endpoint (gpt-5-mini),
5+
uploads the resulting PNG to Azure Blob Storage, and returns a public URL that
6+
Foundry-hosted agents can embed in their markdown responses.
7+
"""
8+
9+
import base64
10+
import logging
11+
import uuid
12+
13+
import httpx
14+
from azure.identity import DefaultAzureCredential, ManagedIdentityCredential, get_bearer_token_provider
15+
from azure.storage.blob import BlobServiceClient, ContentSettings, PublicAccess
16+
17+
from config.settings import config
18+
from core.factory import Domain, MCPToolBase
19+
20+
logger = logging.getLogger(__name__)
21+
22+
_IMAGE_API_VERSION = "2025-04-01-preview"
23+
24+
25+
def _get_credential():
26+
"""Return a credential, preferring user-assigned MI when a client id is set."""
27+
if config.azure_client_id:
28+
return ManagedIdentityCredential(client_id=config.azure_client_id)
29+
return DefaultAzureCredential()
30+
31+
32+
def _ensure_public_container(blob_service: BlobServiceClient, container_name: str) -> None:
33+
"""Create the container with blob-level public read access if missing."""
34+
container_client = blob_service.get_container_client(container_name)
35+
try:
36+
container_client.create_container(public_access=PublicAccess.BLOB)
37+
logger.info("Created public blob container '%s'", container_name)
38+
except Exception:
39+
# Container already exists — leave its access level alone.
40+
pass
41+
42+
43+
def _upload_png_and_get_url(png_bytes: bytes) -> str:
44+
"""Upload PNG bytes to blob storage, return the public URL."""
45+
if not config.azure_storage_blob_url:
46+
raise RuntimeError("AZURE_STORAGE_BLOB_URL is not configured on the MCP server")
47+
48+
account_url = config.azure_storage_blob_url.rstrip("/")
49+
container_name = config.azure_storage_images_container
50+
blob_name = f"{uuid.uuid4()}.png"
51+
52+
credential = _get_credential()
53+
blob_service = BlobServiceClient(account_url=account_url, credential=credential)
54+
_ensure_public_container(blob_service, container_name)
55+
56+
blob_client = blob_service.get_blob_client(container=container_name, blob=blob_name)
57+
blob_client.upload_blob(
58+
png_bytes,
59+
overwrite=True,
60+
content_settings=ContentSettings(content_type="image/png"),
61+
)
62+
return f"{account_url}/{container_name}/{blob_name}"
63+
64+
65+
class ImageService(MCPToolBase):
66+
"""Image-generation tools backed by Azure OpenAI gpt-5-mini."""
67+
68+
def __init__(self):
69+
super().__init__(Domain.IMAGE)
70+
71+
def register_tools(self, mcp) -> None:
72+
@mcp.tool(tags={self.domain.value})
73+
async def generate_marketing_image(prompt: str, size: str = "1024x1024") -> str:
74+
"""Generate a marketing image from a text prompt.
75+
76+
Use this tool whenever the user asks for an image, picture, photo, banner,
77+
or visual asset. Pass a detailed description of the scene, subject, style,
78+
lighting, and composition. The tool returns a public HTTPS URL to the
79+
generated PNG. Embed the URL in your response using markdown image syntax,
80+
for example: ![Generated image](<url>).
81+
82+
Args:
83+
prompt: A detailed description of the image to generate.
84+
size: One of "1024x1024", "1024x1792", or "1792x1024". Defaults to square.
85+
86+
Returns:
87+
A public HTTPS URL to the generated PNG image.
88+
"""
89+
if not config.azure_openai_endpoint:
90+
raise RuntimeError("AZURE_OPENAI_ENDPOINT is not configured on the MCP server")
91+
92+
deployment = config.azure_openai_image_deployment
93+
endpoint = config.azure_openai_endpoint.rstrip("/")
94+
url = (
95+
f"{endpoint}/openai/deployments/{deployment}"
96+
f"/images/generations?api-version={_IMAGE_API_VERSION}"
97+
)
98+
99+
token_provider = get_bearer_token_provider(
100+
_get_credential(), "https://cognitiveservices.azure.com/.default"
101+
)
102+
headers = {
103+
"Authorization": f"Bearer {token_provider()}",
104+
"Content-Type": "application/json",
105+
}
106+
body = {"prompt": prompt, "n": 1, "size": size}
107+
108+
logger.info("Generating image (deployment=%s, size=%s, prompt_len=%d)", deployment, size, len(prompt))
109+
async with httpx.AsyncClient(timeout=120.0) as client:
110+
resp = await client.post(url, json=body, headers=headers)
111+
if resp.status_code >= 400:
112+
raise RuntimeError(f"Image generation failed: {resp.status_code} {resp.text}")
113+
result_json = resp.json()
114+
115+
data = result_json.get("data") or []
116+
if not data:
117+
raise RuntimeError(f"Image generation returned no data: {result_json}")
118+
b64_data = data[0].get("b64_json") or data[0].get("b64")
119+
if not b64_data:
120+
raise RuntimeError(f"Image generation returned no b64 data: {result_json}")
121+
122+
png_bytes = base64.b64decode(b64_data)
123+
public_url = _upload_png_and_get_url(png_bytes)
124+
logger.info("Image uploaded: %s", public_url)
125+
return public_url
126+
127+
@property
128+
def tool_count(self) -> int:
129+
return 1

src/mcp_server/uv.lock

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)