"""FastMCP client — in-process by default, HTTP when MCP_SERVER_URL is set. In-process mode (default, used by ECS Fargate one-shot tasks): Client receives the FastMCP server object directly — no HTTP, no subprocess. HTTP mode (set MCP_SERVER_URL): Client connects to a running MCP server. Used when the MCP server is deployed separately (e.g. Claude Desktop integration, local dev with 'python -m app serve'). """ from __future__ import annotations import json import logging from typing import Any from fastmcp import Client from marketing_intelligence.core.config import settings logger = logging.getLogger(__name__) def _make_client() -> Client: # type: ignore[type-arg] if settings.mcp_server_url: logger.debug("mcp.transport=http url=%s", settings.mcp_server_url) return Client(settings.mcp_server_url) from marketing_intelligence.mcp.server import mcp logger.debug("mcp.transport=in-process") return Client(mcp) class MCPToolClient: """Async context manager that wraps fastmcp.Client for agent use.""" def __init__(self) -> None: self._client: Client[Any] = _make_client() async def __aenter__(self) -> MCPToolClient: await self._client.__aenter__() # type: ignore[no-untyped-call] return self async def __aexit__(self, *args: Any) -> None: await self._client.__aexit__(*args) # type: ignore[no-untyped-call] async def list_tools_anthropic( self, allowed: set[str] | None = None ) -> list[dict[str, Any]]: tools = await self._client.list_tools() return [ { "name": t.name, "description": t.description or "", "input_schema": t.inputSchema, } for t in tools if allowed is None or t.name in allowed ] async def list_tools_bedrock( self, allowed: set[str] | None = None ) -> list[dict[str, Any]]: tools = await self._client.list_tools() return [ { "toolSpec": { "name": t.name, "description": t.description or "", "inputSchema": {"json": t.inputSchema}, } } for t in tools if allowed is None or t.name in allowed ] async def get_prompt( self, name: str, arguments: dict[str, Any] | None = None ) -> str: result = await self._client.get_prompt(name, arguments or {}) texts: list[str] = [] for msg in result.messages: content = getattr(msg, "content", None) if content is None: continue if hasattr(content, "text"): texts.append(content.text) elif isinstance(content, list): for part in content: if hasattr(part, "text"): texts.append(part.text) return "\n".join(texts) async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any: result = await self._client.call_tool(name, arguments) if result.structured_content is not None: return result.structured_content if result.content: first = result.content[0] raw = first.text if hasattr(first, "text") else str(first) try: return json.loads(raw) except json.JSONDecodeError, TypeError: return raw return {}