Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 7 additions & 17 deletions src/core/tools/creative_formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from fastmcp.exceptions import ToolError
from fastmcp.server.context import Context
from fastmcp.tools.tool import ToolResult
from pydantic import ValidationError

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -120,20 +121,8 @@ def _list_creative_formats_impl(
# Create response (no message/specification_version - not in adapter schema)
response = ListCreativeFormatsResponse(formats=formats)

# Add schema validation metadata for client validation
from src.core.schema_validation import INCLUDE_SCHEMAS_IN_RESPONSES, enhance_mcp_response_with_schema

if INCLUDE_SCHEMAS_IN_RESPONSES:
# Convert to dict, enhance with schema, return enhanced dict
response_dict = response.model_dump()
enhanced_response = enhance_mcp_response_with_schema(
response_data=response_dict,
model_class=ListCreativeFormatsResponse,
include_full_schema=False, # Set to True for development debugging
)
# Return the enhanced response (FastMCP handles dict returns)
return enhanced_response

# Always return Pydantic model - MCP wrapper will handle serialization
# Schema enhancement (if needed) should happen in the MCP wrapper, not here
return response


Expand All @@ -144,7 +133,7 @@ def list_creative_formats(
format_ids: list[str] | None = None,
webhook_url: str | None = None,
context: Context = None,
) -> ListCreativeFormatsResponse:
):
"""List all available creative formats (AdCP spec endpoint).

MCP tool wrapper that delegates to the shared implementation.
Expand All @@ -158,7 +147,7 @@ def list_creative_formats(
context: FastMCP context (automatically provided)

Returns:
ListCreativeFormatsResponse with all available formats
ToolResult with ListCreativeFormatsResponse data
"""
try:
req = ListCreativeFormatsRequest(
Expand All @@ -170,7 +159,8 @@ def list_creative_formats(
except ValidationError as e:
raise ToolError(format_validation_error(e, context="list_creative_formats request")) from e

return _list_creative_formats_impl(req, context)
response = _list_creative_formats_impl(req, context)
return ToolResult(content=str(response), structured_content=response.model_dump())


def list_creative_formats_raw(
Expand Down
16 changes: 11 additions & 5 deletions src/core/tools/creatives.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from fastmcp.exceptions import ToolError
from fastmcp.server.context import Context
from fastmcp.tools.tool import ToolResult
from pydantic import ValidationError
from rich.console import Console
from sqlalchemy import select
Expand Down Expand Up @@ -1424,7 +1425,7 @@ def sync_creatives(
validation_mode: str = "strict",
push_notification_config: dict | None = None,
context: Context | None = None,
) -> SyncCreativesResponse:
):
"""Sync creative assets to centralized library (AdCP v2.4 spec compliant endpoint).

MCP tool wrapper that delegates to the shared implementation.
Expand All @@ -1440,9 +1441,9 @@ def sync_creatives(
context: FastMCP context (automatically provided)

Returns:
SyncCreativesResponse with sync results
ToolResult with SyncCreativesResponse data
"""
return _sync_creatives_impl(
response = _sync_creatives_impl(
creatives=creatives,
patch=patch,
assignments=assignments,
Expand All @@ -1452,6 +1453,7 @@ def sync_creatives(
push_notification_config=push_notification_config,
context=context,
)
return ToolResult(content=str(response), structured_content=response.model_dump())


def _list_creatives_impl(
Expand Down Expand Up @@ -1777,14 +1779,17 @@ def list_creatives(
sort_order: str = "desc",
webhook_url: str | None = None,
context: Context = None,
) -> ListCreativesResponse:
):
"""List and filter creative assets from the centralized library.

MCP tool wrapper that delegates to the shared implementation.
Supports both flat parameters (status, format, etc.) and nested objects (filters, sort, pagination)
for maximum flexibility.

Returns:
ToolResult with ListCreativesResponse data
"""
return _list_creatives_impl(
response = _list_creatives_impl(
media_buy_id,
buyer_ref,
status,
Expand All @@ -1806,6 +1811,7 @@ def list_creatives(
sort_order,
context,
)
return ToolResult(content=str(response), structured_content=response.model_dump())


def sync_creatives_raw(
Expand Down
8 changes: 5 additions & 3 deletions src/core/tools/media_buy_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from fastmcp.exceptions import ToolError
from fastmcp.server.context import Context
from fastmcp.tools.tool import ToolResult
from pydantic import ValidationError
from rich.console import Console

Expand Down Expand Up @@ -1889,7 +1890,7 @@ async def create_media_buy(
push_notification_config: dict[str, Any] | None = None,
webhook_url: str | None = None,
context: Context | None = None,
) -> CreateMediaBuyResponse:
):
"""Create a media buy with the specified parameters.

MCP tool wrapper that delegates to the shared implementation.
Expand Down Expand Up @@ -1918,9 +1919,9 @@ async def create_media_buy(
context: FastMCP context (automatically provided)

Returns:
CreateMediaBuyResponse with media buy details
ToolResult with CreateMediaBuyResponse data
"""
return await _create_media_buy_impl(
response = await _create_media_buy_impl(
buyer_ref=buyer_ref,
brand_manifest=brand_manifest,
po_number=po_number,
Expand All @@ -1943,6 +1944,7 @@ async def create_media_buy(
push_notification_config=push_notification_config,
context=context,
)
return ToolResult(content=str(response), structured_content=response.model_dump())


async def create_media_buy_raw(
Expand Down
8 changes: 5 additions & 3 deletions src/core/tools/media_buy_delivery.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from fastmcp.exceptions import ToolError
from fastmcp.server.context import Context
from fastmcp.tools.tool import ToolResult
from pydantic import ValidationError
from rich.console import Console

Expand Down Expand Up @@ -310,7 +311,7 @@ def get_media_buy_delivery(
end_date: str = None,
webhook_url: str | None = None,
context: Context = None,
) -> GetMediaBuyDeliveryResponse:
):
"""Get delivery data for media buys.

AdCP-compliant implementation of get_media_buy_delivery tool.
Expand All @@ -325,7 +326,7 @@ def get_media_buy_delivery(
context: FastMCP context (automatically provided)

Returns:
GetMediaBuyDeliveryResponse with AdCP-compliant delivery data for the requested media buys
ToolResult with GetMediaBuyDeliveryResponse data
"""
# Create AdCP-compliant request object
try:
Expand All @@ -339,7 +340,8 @@ def get_media_buy_delivery(
except ValidationError as e:
raise ToolError(format_validation_error(e, context="get_media_buy_delivery request")) from e

return _get_media_buy_delivery_impl(req, context)
response = _get_media_buy_delivery_impl(req, context)
return ToolResult(content=str(response), structured_content=response.model_dump())


def get_media_buy_delivery_raw(
Expand Down
8 changes: 5 additions & 3 deletions src/core/tools/media_buy_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from fastmcp.exceptions import ToolError
from fastmcp.server.context import Context
from fastmcp.tools.tool import ToolResult
from pydantic import ValidationError
from sqlalchemy import select

Expand Down Expand Up @@ -663,7 +664,7 @@ def update_media_buy(
creatives: list = None,
push_notification_config: dict | None = None,
context: Context = None,
) -> UpdateMediaBuyResponse:
):
"""Update a media buy with campaign-level and/or package-level changes.

MCP tool wrapper that delegates to the shared implementation.
Expand All @@ -687,9 +688,9 @@ def update_media_buy(
context: FastMCP context (automatically provided)

Returns:
UpdateMediaBuyResponse with updated media buy details
ToolResult with UpdateMediaBuyResponse data
"""
return _update_media_buy_impl(
response = _update_media_buy_impl(
media_buy_id=media_buy_id,
buyer_ref=buyer_ref,
active=active,
Expand All @@ -707,6 +708,7 @@ def update_media_buy(
push_notification_config=push_notification_config,
context=context,
)
return ToolResult(content=str(response), structured_content=response.model_dump())


def update_media_buy_raw(
Expand Down
32 changes: 26 additions & 6 deletions src/core/tools/performance.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from fastmcp.exceptions import ToolError
from fastmcp.server.context import Context
from fastmcp.tools.tool import ToolResult
from pydantic import ValidationError
from rich.console import Console

Expand All @@ -23,19 +24,18 @@
from src.core.validation_helpers import format_validation_error


def update_performance_index(
media_buy_id: str, performance_data: list[dict[str, Any]], webhook_url: str | None = None, context: Context = None
def _update_performance_index_impl(
media_buy_id: str, performance_data: list[dict[str, Any]], context: Context = None
) -> UpdatePerformanceIndexResponse:
"""Update performance index data for a media buy.
"""Shared implementation for update_performance_index (used by both MCP and A2A).

Args:
media_buy_id: ID of the media buy to update
performance_data: List of performance data objects
webhook_url: URL for async task completion notifications (AdCP spec, optional)
context: FastMCP context (automatically provided)

Returns:
UpdatePerformanceIndexResponse with operation status
UpdatePerformanceIndexResponse with update status
"""
# Create request object from individual parameters (MCP-compliant)
# Convert dict performance_data to ProductPerformance objects
Expand Down Expand Up @@ -92,6 +92,26 @@ def update_performance_index(
)


def update_performance_index(
media_buy_id: str, performance_data: list[dict[str, Any]], webhook_url: str | None = None, context: Context = None
):
"""Update performance index data for a media buy.

MCP tool wrapper that delegates to the shared implementation.

Args:
media_buy_id: ID of the media buy to update
performance_data: List of performance data objects
webhook_url: URL for async task completion notifications (AdCP spec, optional)
context: FastMCP context (automatically provided)

Returns:
ToolResult with UpdatePerformanceIndexResponse data
"""
response = _update_performance_index_impl(media_buy_id, performance_data, context)
return ToolResult(content=str(response), structured_content=response.model_dump())


def update_performance_index_raw(media_buy_id: str, performance_data: list[dict[str, Any]], context: Context = None):
"""Update performance data for a media buy (raw function for A2A server use).

Expand All @@ -105,7 +125,7 @@ def update_performance_index_raw(media_buy_id: str, performance_data: list[dict[
Returns:
UpdatePerformanceIndexResponse
"""
return update_performance_index(media_buy_id, performance_data, webhook_url=None, context=context)
return _update_performance_index_impl(media_buy_id, performance_data, context)


# --- Human-in-the-Loop Task Queue Tools ---
Expand Down
10 changes: 7 additions & 3 deletions src/core/tools/products.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from fastmcp.exceptions import ToolError
from fastmcp.server.context import Context
from fastmcp.tools.tool import ToolResult
from pydantic import ValidationError

# Imports for implementation
Expand Down Expand Up @@ -505,7 +506,7 @@ async def get_products(
brief: str = "",
filters: dict | None = None,
context: Context = None,
) -> GetProductsResponse:
):
"""Get available products matching the brief.

MCP tool wrapper that delegates to the shared implementation.
Expand All @@ -517,7 +518,7 @@ async def get_products(
context: FastMCP context (automatically provided)

Returns:
GetProductsResponse containing matching products
ToolResult with human-readable text and structured data

Note:
promoted_offering is deprecated - use brand_manifest instead.
Expand All @@ -539,7 +540,10 @@ async def get_products(

# Call shared implementation
# Note: GetProductsRequest is now a flat class (not RootModel), so pass req directly
return await _get_products_impl(req, context)
response = await _get_products_impl(req, context)

# Return ToolResult with human-readable text and structured data
return ToolResult(content=str(response), structured_content=response.model_dump())


async def get_products_raw(
Expand Down
12 changes: 9 additions & 3 deletions src/core/tools/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import sqlalchemy as sa
from fastmcp.exceptions import ToolError
from fastmcp.server.context import Context
from fastmcp.tools.tool import ToolResult
from sqlalchemy import select

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -243,7 +244,7 @@ def _list_authorized_properties_impl(

def list_authorized_properties(
req: ListAuthorizedPropertiesRequest | None = None, webhook_url: str | None = None, context: Context | None = None
) -> ListAuthorizedPropertiesResponse:
):
"""List all properties this agent is authorized to represent (AdCP spec endpoint).

MCP tool wrapper that delegates to the shared implementation.
Expand All @@ -254,7 +255,7 @@ def list_authorized_properties(
context: FastMCP context for authentication

Returns:
ListAuthorizedPropertiesResponse with properties and tag metadata
ToolResult with human-readable text and structured data
"""
# FIX: Create MinimalContext with headers from FastMCP request (like A2A does)
# This ensures tenant detection works the same way for both MCP and A2A
Expand Down Expand Up @@ -309,7 +310,12 @@ def __init__(self, headers):
logger.info("MCP list_authorized_properties: No context provided")
tool_context = context

return _list_authorized_properties_impl(req, tool_context)
response = _list_authorized_properties_impl(req, tool_context)

# Return ToolResult with human-readable text and structured data
# The __str__() method provides the human-readable message
# The model_dump() provides the structured JSON data
return ToolResult(content=str(response), structured_content=response.model_dump())


def list_authorized_properties_raw(
Expand Down
Loading