Skip to main content
Glama
MatiousCorp

Google Ad Manager MCP Server

by MatiousCorp
server.py23 kB
"""Google Ad Manager MCP Server with HTTP/SSE and stdio transport. Security: Implements Bearer token authentication following MCP security best practices. - Uses FastMCP native middleware for proper lifecycle management - Cryptographically secure token generation (secrets.token_hex) - Constant-time token comparison to prevent timing attacks - All authentication decisions logged for audit trail Reference: https://modelcontextprotocol.io/specification/draft/basic/security_best_practices Reference: https://gofastmcp.com/python-sdk/fastmcp-server-auth-auth """ import os import json import logging import secrets import hmac from typing import Optional # Use the standalone fastmcp package (has full middleware support) from fastmcp import FastMCP, Context from fastmcp.server.middleware import Middleware from fastmcp.server.dependencies import get_http_headers from fastmcp.exceptions import ToolError from .client import init_gam_client, get_gam_client from .tools import orders, line_items, creatives, advertisers, verification # Authentication token - set via environment variable or generate random AUTH_TOKEN = os.environ.get("GAM_MCP_AUTH_TOKEN", None) # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # Initialize FastMCP server mcp = FastMCP(name="Google Ad Manager") # ============================================================================= # AUTHENTICATION MIDDLEWARE (FastMCP Native) # ============================================================================= class BearerAuthMiddleware(Middleware): """FastMCP middleware for Bearer token authentication. Validates Authorization header on all tool calls. Uses constant-time comparison to prevent timing attacks. Reference: https://gelembjuk.com/blog/post/authentication-remote-mcp-server-python/ """ async def on_call_tool(self, context, call_next): """Validate bearer token before tool execution.""" global AUTH_TOKEN if not AUTH_TOKEN: # No auth configured, allow request return await call_next(context) try: headers = get_http_headers() auth_header = headers.get("authorization", "") except Exception: # If we can't get headers (e.g., stdio transport), skip auth return await call_next(context) if not auth_header: logger.warning("Auth failed: Missing Authorization header") raise ToolError("Access denied: Missing Authorization header. Use: Authorization: Bearer <token>") if not auth_header.startswith("Bearer "): logger.warning("Auth failed: Invalid Authorization format") raise ToolError("Access denied: Invalid Authorization format. Use: Authorization: Bearer <token>") token = auth_header[7:] # Remove "Bearer " prefix # Constant-time comparison to prevent timing attacks if not hmac.compare_digest(token.encode('utf-8'), AUTH_TOKEN.encode('utf-8')): logger.warning("Auth failed: Invalid token") raise ToolError("Access denied: Invalid token") logger.debug("Auth successful") return await call_next(context) # Add middleware to MCP server mcp.add_middleware(BearerAuthMiddleware()) logger.info("Bearer token authentication middleware enabled") # ============================================================================= # ORDER TOOLS # ============================================================================= @mcp.tool() def list_delivering_orders() -> str: """List all orders with line items currently delivering ads. Returns a list of orders with their delivering line items, including impression and click statistics. """ init_client() result = orders.list_delivering_orders() return json.dumps(result, indent=2) @mcp.tool() def get_order(order_id: Optional[int] = None, order_name: Optional[str] = None) -> str: """Get order details by ID or name. Args: order_id: The order ID (optional if order_name provided) order_name: The order name to search for (optional if order_id provided) Returns order details including all line items. """ init_client() result = orders.get_order(order_id=order_id, order_name=order_name) return json.dumps(result, indent=2) @mcp.tool() def create_order(order_name: str, advertiser_id: int) -> str: """Create a new order for an advertiser. Args: order_name: Name for the new order advertiser_id: ID of the advertiser company Returns the created order details. """ init_client() result = orders.create_order(order_name=order_name, advertiser_id=advertiser_id) return json.dumps(result, indent=2) @mcp.tool() def find_or_create_order(order_name: str, advertiser_id: int) -> str: """Find an existing order by name or create a new one. Args: order_name: Name of the order advertiser_id: ID of the advertiser company Returns the existing or newly created order. """ init_client() result = orders.find_or_create_order(order_name=order_name, advertiser_id=advertiser_id) return json.dumps(result, indent=2) # ============================================================================= # LINE ITEM TOOLS # ============================================================================= @mcp.tool() def get_line_item(line_item_id: int) -> str: """Get line item details by ID. Args: line_item_id: The line item ID Returns line item details including status, dates, and statistics. """ init_client() result = line_items.get_line_item(line_item_id=line_item_id) return json.dumps(result, indent=2) @mcp.tool() def create_line_item( order_id: int, name: str, end_year: int, end_month: int, end_day: int, target_ad_unit_id: str, line_item_type: str = "STANDARD", goal_impressions: int = 100000 ) -> str: """Create a new line item for an order. Args: order_id: The order ID to add line item to name: Line item name end_year: End date year (e.g., 2025) end_month: End date month (1-12) end_day: End date day (1-31) target_ad_unit_id: Ad unit ID to target (find via GAM UI or ad unit tools) line_item_type: Type of line item (STANDARD, SPONSORSHIP, etc.) goal_impressions: Impression goal (default: 100000) Returns the created line item details. """ init_client() result = line_items.create_line_item( order_id=order_id, name=name, end_year=end_year, end_month=end_month, end_day=end_day, line_item_type=line_item_type, target_ad_unit_id=target_ad_unit_id, goal_impressions=goal_impressions ) return json.dumps(result, indent=2) @mcp.tool() def duplicate_line_item( source_line_item_id: int, new_name: str, rename_source: Optional[str] = None ) -> str: """Duplicate an existing line item. Args: source_line_item_id: ID of the line item to duplicate new_name: Name for the new line item rename_source: Optional new name for the source line item Returns both the source and new line item details. """ init_client() result = line_items.duplicate_line_item( source_line_item_id=source_line_item_id, new_name=new_name, rename_source=rename_source ) return json.dumps(result, indent=2) @mcp.tool() def update_line_item_name(line_item_id: int, new_name: str) -> str: """Update a line item's name. Args: line_item_id: The line item ID new_name: New name for the line item Returns the updated line item details. """ init_client() result = line_items.update_line_item_name( line_item_id=line_item_id, new_name=new_name ) return json.dumps(result, indent=2) @mcp.tool() def list_line_items_by_order(order_id: int) -> str: """List all line items for an order. Args: order_id: The order ID Returns list of line items with their status and statistics. """ init_client() result = line_items.list_line_items_by_order(order_id=order_id) return json.dumps(result, indent=2) # ============================================================================= # CREATIVE TOOLS # ============================================================================= @mcp.tool() def upload_creative( file_path: str, advertiser_id: int, click_through_url: str, creative_name: Optional[str] = None, override_size_width: Optional[int] = None, override_size_height: Optional[int] = None ) -> str: """Upload an image creative to Ad Manager. Args: file_path: Path to the image file advertiser_id: ID of the advertiser click_through_url: Destination URL when clicked creative_name: Optional name for the creative override_size_width: Optional width to override the creative size (for serving into a different sized slot) override_size_height: Optional height to override the creative size (for serving into a different sized slot) The creative size is extracted from the filename (e.g., '300x250' in 'banner_300x250.png'). Use override_size_width and override_size_height together to serve a creative into a different sized placement (e.g., serve a 970x250 image into a 1000x250 slot). Returns the created creative details. """ init_client() result = creatives.upload_creative( file_path=file_path, advertiser_id=advertiser_id, click_through_url=click_through_url, creative_name=creative_name, override_size_width=override_size_width, override_size_height=override_size_height ) return json.dumps(result, indent=2) @mcp.tool() def associate_creative_with_line_item( creative_id: int, line_item_id: int, size_override_width: Optional[int] = None, size_override_height: Optional[int] = None ) -> str: """Associate a creative with a line item. Args: creative_id: The creative ID line_item_id: The line item ID size_override_width: Optional width for size override size_override_height: Optional height for size override Returns the association details. """ init_client() result = creatives.associate_creative_with_line_item( creative_id=creative_id, line_item_id=line_item_id, size_override_width=size_override_width, size_override_height=size_override_height ) return json.dumps(result, indent=2) @mcp.tool() def upload_and_associate_creative( file_path: str, advertiser_id: int, line_item_id: int, click_through_url: str, creative_name: Optional[str] = None ) -> str: """Upload a creative and associate it with a line item in one step. Args: file_path: Path to the image file advertiser_id: ID of the advertiser line_item_id: ID of the line item click_through_url: Destination URL when clicked creative_name: Optional name for the creative Returns the creative and association details. """ init_client() result = creatives.upload_and_associate_creative( file_path=file_path, advertiser_id=advertiser_id, line_item_id=line_item_id, click_through_url=click_through_url, creative_name=creative_name ) return json.dumps(result, indent=2) @mcp.tool() def bulk_upload_creatives( folder_path: str, advertiser_id: int, line_item_id: int, click_through_url: str, name_prefix: Optional[str] = None ) -> str: """Upload all creatives from a folder and associate with a line item. Args: folder_path: Path to folder containing image files advertiser_id: ID of the advertiser line_item_id: ID of the line item click_through_url: Destination URL when clicked name_prefix: Optional prefix for creative names Supported formats: jpg, jpeg, png, gif. Returns results for all uploads. """ init_client() result = creatives.bulk_upload_creatives( folder_path=folder_path, advertiser_id=advertiser_id, line_item_id=line_item_id, click_through_url=click_through_url, name_prefix=name_prefix ) return json.dumps(result, indent=2) @mcp.tool() def get_creative(creative_id: int) -> str: """Get creative details by ID. Args: creative_id: The creative ID Returns creative details including size and destination URL. """ init_client() result = creatives.get_creative(creative_id=creative_id) return json.dumps(result, indent=2) @mcp.tool() def list_creatives_by_advertiser(advertiser_id: int, limit: int = 100) -> str: """List creatives for an advertiser. Args: advertiser_id: The advertiser ID limit: Maximum number of creatives to return (default: 100) Returns list of creatives. """ init_client() result = creatives.list_creatives_by_advertiser( advertiser_id=advertiser_id, limit=limit ) return json.dumps(result, indent=2) # ============================================================================= # ADVERTISER TOOLS # ============================================================================= @mcp.tool() def find_advertiser(name: str) -> str: """Find an advertiser by name (partial match). Args: name: Advertiser name to search for Returns list of matching advertisers. """ init_client() result = advertisers.find_advertiser(name=name) return json.dumps(result, indent=2) @mcp.tool() def get_advertiser(advertiser_id: int) -> str: """Get advertiser details by ID. Args: advertiser_id: The advertiser/company ID Returns advertiser details. """ init_client() result = advertisers.get_advertiser(advertiser_id=advertiser_id) return json.dumps(result, indent=2) @mcp.tool() def list_advertisers(limit: int = 100) -> str: """List all advertisers. Args: limit: Maximum number of advertisers to return (default: 100) Returns list of advertisers. """ init_client() result = advertisers.list_advertisers(limit=limit) return json.dumps(result, indent=2) @mcp.tool() def create_advertiser( name: str, email: Optional[str] = None, address: Optional[str] = None ) -> str: """Create a new advertiser. Args: name: Advertiser name email: Optional email address address: Optional address Returns the created advertiser details. """ init_client() result = advertisers.create_advertiser( name=name, email=email, address=address ) return json.dumps(result, indent=2) @mcp.tool() def find_or_create_advertiser(name: str, email: Optional[str] = None) -> str: """Find an advertiser by exact name or create if not found. Args: name: Exact advertiser name email: Optional email (used if creating) Returns the existing or newly created advertiser. """ init_client() result = advertisers.find_or_create_advertiser(name=name, email=email) return json.dumps(result, indent=2) # ============================================================================= # VERIFICATION TOOLS # ============================================================================= @mcp.tool() def verify_line_item_setup(line_item_id: int) -> str: """Verify line item setup including creative placeholders and associations. Args: line_item_id: The line item ID to verify Checks: - Creative placeholders (expected sizes) - Creative associations - Size mismatches between creatives and placeholders Returns verification results with any issues found. """ init_client() result = verification.verify_line_item_setup(line_item_id=line_item_id) return json.dumps(result, indent=2) @mcp.tool() def check_line_item_delivery_status(line_item_id: int) -> str: """Check detailed delivery status for a line item. Args: line_item_id: The line item ID to check Returns delivery progress including impressions, clicks, and goal progress. """ init_client() result = verification.check_line_item_delivery_status(line_item_id=line_item_id) return json.dumps(result, indent=2) @mcp.tool() def verify_order_setup(order_id: int) -> str: """Verify complete order setup including all line items. Args: order_id: The order ID to verify Returns comprehensive verification of the order and all its line items. """ init_client() result = verification.verify_order_setup(order_id=order_id) return json.dumps(result, indent=2) # ============================================================================= # CAMPAIGN WORKFLOW TOOL # ============================================================================= @mcp.tool() def create_campaign( advertiser_name: str, order_name: str, line_item_name: str, end_year: int, end_month: int, end_day: int, creatives_folder: str, click_through_url: str, target_ad_unit_id: str, goal_impressions: int = 100000 ) -> str: """Create a complete campaign: find/create advertiser, order, line item, and upload creatives. Args: advertiser_name: Name of the advertiser order_name: Name for the order line_item_name: Name for the line item end_year: End date year end_month: End date month (1-12) end_day: End date day (1-31) creatives_folder: Path to folder containing creative images click_through_url: Destination URL for all creatives target_ad_unit_id: Ad unit ID to target (find via GAM UI or ad unit tools) goal_impressions: Impression goal (default: 100000) This is a complete workflow that: 1. Finds or creates the advertiser 2. Finds or creates the order 3. Creates the line item 4. Uploads all creatives from the folder 5. Associates creatives with the line item Returns complete campaign creation results. """ init_client() result = { "advertiser": None, "order": None, "line_item": None, "creatives": None, "errors": [] } try: # Step 1: Find or create advertiser adv_result = advertisers.find_or_create_advertiser(name=advertiser_name) if "error" in adv_result: result["errors"].append(f"Advertiser: {adv_result['error']}") return json.dumps(result, indent=2) result["advertiser"] = adv_result advertiser_id = adv_result["id"] # Step 2: Find or create order order_result = orders.find_or_create_order( order_name=order_name, advertiser_id=advertiser_id ) if "error" in order_result: result["errors"].append(f"Order: {order_result['error']}") return json.dumps(result, indent=2) result["order"] = order_result order_id = order_result["id"] # Step 3: Create line item li_result = line_items.create_line_item( order_id=order_id, name=line_item_name, end_year=end_year, end_month=end_month, end_day=end_day, target_ad_unit_id=target_ad_unit_id, goal_impressions=goal_impressions ) if "error" in li_result: result["errors"].append(f"Line Item: {li_result['error']}") return json.dumps(result, indent=2) result["line_item"] = li_result line_item_id = li_result["id"] # Step 4: Upload creatives creative_result = creatives.bulk_upload_creatives( folder_path=creatives_folder, advertiser_id=advertiser_id, line_item_id=line_item_id, click_through_url=click_through_url, name_prefix=f"{advertiser_name} - {order_name}" ) result["creatives"] = creative_result result["success"] = True result["message"] = f"Campaign '{order_name}' created successfully" except Exception as e: result["errors"].append(str(e)) result["success"] = False return json.dumps(result, indent=2) def init_client(): """Initialize the GAM client from environment variables. This is called lazily when the first tool is executed, not at server startup. This allows the server to start and list tools even without credentials. """ # Check if already initialized if get_gam_client() is not None: return credentials_path = os.environ.get("GAM_CREDENTIALS_PATH") if not credentials_path: raise ValueError( "GAM_CREDENTIALS_PATH environment variable is required. " "Set it to the path of your Google Ad Manager service account JSON file." ) network_code = os.environ.get("GAM_NETWORK_CODE") if not network_code: raise ValueError( "GAM_NETWORK_CODE environment variable is required. " "Set it to your Google Ad Manager network code." ) logger.info(f"Initializing GAM client for network {network_code}") init_gam_client( credentials_path=credentials_path, network_code=network_code, application_name="GAM MCP Server" ) def main(): """Main entry point for the MCP server.""" global AUTH_TOKEN # Get transport mode from environment (default: stdio for CLI usage) transport = os.environ.get("GAM_MCP_TRANSPORT", "stdio").lower() host = os.environ.get("GAM_MCP_HOST", "0.0.0.0") port = int(os.environ.get("GAM_MCP_PORT", "8000")) if transport == "stdio": # Stdio transport - no auth token needed (local process) logger.info("Starting GAM MCP Server with stdio transport") mcp.run(transport="stdio") else: # HTTP transport - set up auth token if AUTH_TOKEN is None: AUTH_TOKEN = secrets.token_hex(32) logger.info("Generated auth token (set GAM_MCP_AUTH_TOKEN env var to use a fixed token)") logger.info("") logger.info("=" * 60) logger.info("GAM MCP Server Authentication Token:") logger.info(AUTH_TOKEN) logger.info("=" * 60) logger.info("") logger.info("Use this token in the Authorization header:") logger.info(f"Authorization: Bearer {AUTH_TOKEN}") logger.info("") # Run using FastMCP's native runner (proper lifecycle management) logger.info(f"Starting GAM MCP Server on http://{host}:{port}/mcp") mcp.run(transport="http", host=host, port=port, path="/mcp") if __name__ == "__main__": main()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/MatiousCorp/google-ad-manager-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server