diff --git a/pyproject.toml b/pyproject.toml index dd1118c..e9dfd1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mcp-mailu" -version = "0.3.0" +version = "0.3.2" description = "FastMCP server for Mailu email server API integration" authors = [ {name = "Ryan Malloy", email = "ryan@supported.systems"} diff --git a/src/mcp_mailu/server.py b/src/mcp_mailu/server.py index 6f34f33..53b55c6 100644 --- a/src/mcp_mailu/server.py +++ b/src/mcp_mailu/server.py @@ -689,6 +689,17 @@ def create_mailu_client(base_url: str, api_token: str) -> httpx.AsyncClient: ) +def get_mailu_client() -> httpx.AsyncClient: + """Get a new HTTP client for Mailu API.""" + mailu_base_url = os.getenv("MAILU_BASE_URL", "https://mail.example.com") + mailu_api_token = os.getenv("MAILU_API_TOKEN") + + if not mailu_api_token: + raise ValueError("MAILU_API_TOKEN environment variable not set") + + return create_mailu_client(mailu_base_url + "/api/v1", mailu_api_token) + + def create_mcp_server() -> FastMCP: """Create the MCP server with Mailu API integration.""" @@ -699,8 +710,7 @@ def create_mcp_server() -> FastMCP: if not mailu_api_token: logger.warning("MAILU_API_TOKEN environment variable not set. Server will not work without authentication.") - # Create authenticated HTTP client - client = create_mailu_client(mailu_base_url + "/api/v1", mailu_api_token) + # Configuration validated, client will be created fresh for each request # Fetch the actual OpenAPI specification from Mailu spec_url = "https://mail.supported.systems/api/v1/swagger.json" @@ -732,7 +742,7 @@ def create_mcp_server() -> FastMCP: @mcp.tool() async def list_users() -> str: """List all users in the Mailu instance.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get("/user") return f"Users: {response.json()}" @@ -744,7 +754,7 @@ def create_mcp_server() -> FastMCP: reply_subject: str = "", reply_body: str = "", displayed_name: str = "", spam_enabled: bool = True, spam_mark_as_read: bool = False, spam_threshold: int = 80) -> str: """Create a new user in the Mailu instance.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: user_data = { "email": email, "raw_password": raw_password, @@ -772,7 +782,7 @@ def create_mcp_server() -> FastMCP: @mcp.tool() async def get_user(email: str) -> str: """Get details of a specific user.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/user/{email}") return f"User details: {response.json()}" @@ -784,7 +794,7 @@ def create_mcp_server() -> FastMCP: reply_subject: str = "", reply_body: str = "", displayed_name: str = "", spam_enabled: bool = None, spam_mark_as_read: bool = None, spam_threshold: int = None) -> str: """Update an existing user.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: user_data = {} if raw_password: user_data["raw_password"] = raw_password if comment: user_data["comment"] = comment @@ -811,7 +821,7 @@ def create_mcp_server() -> FastMCP: @mcp.tool() async def delete_user(email: str) -> str: """Delete a user from the Mailu instance.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.delete(f"/user/{email}") return f"Delete user result: {response.json()}" @@ -819,7 +829,7 @@ def create_mcp_server() -> FastMCP: @mcp.tool() async def list_domains() -> str: """List all domains in the Mailu instance.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get("/domain") return f"Domains: {response.json()}" @@ -827,7 +837,7 @@ def create_mcp_server() -> FastMCP: async def create_domain(name: str, comment: str = "", max_users: int = -1, max_aliases: int = -1, max_quota_bytes: int = 0, signup_enabled: bool = False, alternatives: str = "") -> str: """Create a new domain in the Mailu instance.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: domain_data = { "name": name, "comment": comment, @@ -845,7 +855,7 @@ def create_mcp_server() -> FastMCP: @mcp.tool() async def get_domain(domain: str) -> str: """Get details of a specific domain.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/domain/{domain}") return f"Domain details: {response.json()}" @@ -853,7 +863,7 @@ def create_mcp_server() -> FastMCP: async def update_domain(domain: str, comment: str = "", max_users: int = None, max_aliases: int = None, max_quota_bytes: int = None, signup_enabled: bool = None, alternatives: str = "") -> str: """Update an existing domain.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: domain_data = {} if comment: domain_data["comment"] = comment if max_users is not None: domain_data["max_users"] = max_users @@ -868,21 +878,21 @@ def create_mcp_server() -> FastMCP: @mcp.tool() async def delete_domain(domain: str) -> str: """Delete a domain from the Mailu instance.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.delete(f"/domain/{domain}") return f"Delete domain result: {response.json()}" @mcp.tool() async def generate_dkim_keys(domain: str) -> str: """Generate DKIM keys for a domain.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.post(f"/domain/{domain}/dkim") return f"Generate DKIM keys result: {response.json()}" @mcp.tool() async def list_domain_users(domain: str) -> str: """List all users in a specific domain.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/domain/{domain}/users") return f"Domain users: {response.json()}" @@ -890,14 +900,14 @@ def create_mcp_server() -> FastMCP: @mcp.tool() async def list_domain_managers(domain: str) -> str: """List all managers for a specific domain.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/domain/{domain}/manager") return f"Domain managers: {response.json()}" @mcp.tool() async def create_domain_manager(domain: str, user_email: str) -> str: """Create a new domain manager.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: manager_data = {"user_email": user_email} response = await mailu_client.post(f"/domain/{domain}/manager", json=manager_data) return f"Create domain manager result: {response.json()}" @@ -905,14 +915,14 @@ def create_mcp_server() -> FastMCP: @mcp.tool() async def get_domain_manager(domain: str, email: str) -> str: """Get details of a specific domain manager.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/domain/{domain}/manager/{email}") return f"Domain manager details: {response.json()}" @mcp.tool() async def delete_domain_manager(domain: str, email: str) -> str: """Delete a domain manager.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.delete(f"/domain/{domain}/manager/{email}") return f"Delete domain manager result: {response.json()}" @@ -920,14 +930,14 @@ def create_mcp_server() -> FastMCP: @mcp.tool() async def list_aliases() -> str: """List all aliases in the Mailu instance.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get("/alias") return f"Aliases: {response.json()}" @mcp.tool() async def create_alias(email: str, destination: str = "", comment: str = "", wildcard: bool = False) -> str: """Create a new alias.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: alias_data = { "email": email, "destination": destination, @@ -940,14 +950,14 @@ def create_mcp_server() -> FastMCP: @mcp.tool() async def get_alias(alias: str) -> str: """Get details of a specific alias.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/alias/{alias}") return f"Alias details: {response.json()}" @mcp.tool() async def update_alias(alias: str, destination: str = "", comment: str = "", wildcard: bool = None) -> str: """Update an existing alias.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: alias_data = {} if destination: alias_data["destination"] = destination if comment: alias_data["comment"] = comment @@ -959,14 +969,14 @@ def create_mcp_server() -> FastMCP: @mcp.tool() async def delete_alias(alias: str) -> str: """Delete an alias.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.delete(f"/alias/{alias}") return f"Delete alias result: {response.json()}" @mcp.tool() async def find_aliases_by_domain(domain: str) -> str: """Find aliases by destination domain.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/alias/destination/{domain}") return f"Aliases for domain: {response.json()}" @@ -974,14 +984,14 @@ def create_mcp_server() -> FastMCP: @mcp.tool() async def list_alternative_domains() -> str: """List all alternative domains.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get("/alternative") return f"Alternative domains: {response.json()}" @mcp.tool() async def create_alternative_domain(name: str, domain: str) -> str: """Create a new alternative domain.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: alt_data = {"name": name, "domain": domain} response = await mailu_client.post("/alternative", json=alt_data) return f"Create alternative domain result: {response.json()}" @@ -989,14 +999,14 @@ def create_mcp_server() -> FastMCP: @mcp.tool() async def get_alternative_domain(alt: str) -> str: """Get details of a specific alternative domain.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/alternative/{alt}") return f"Alternative domain details: {response.json()}" @mcp.tool() async def delete_alternative_domain(alt: str) -> str: """Delete an alternative domain.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.delete(f"/alternative/{alt}") return f"Delete alternative domain result: {response.json()}" @@ -1004,14 +1014,14 @@ def create_mcp_server() -> FastMCP: @mcp.tool() async def list_relays() -> str: """List all relays.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get("/relay") return f"Relays: {response.json()}" @mcp.tool() async def create_relay(name: str, smtp: str = "", comment: str = "") -> str: """Create a new relay.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: relay_data = {"name": name, "smtp": smtp, "comment": comment} response = await mailu_client.post("/relay", json=relay_data) return f"Create relay result: {response.json()}" @@ -1019,14 +1029,14 @@ def create_mcp_server() -> FastMCP: @mcp.tool() async def get_relay(name: str) -> str: """Get details of a specific relay.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/relay/{name}") return f"Relay details: {response.json()}" @mcp.tool() async def update_relay(name: str, smtp: str = "", comment: str = "") -> str: """Update an existing relay.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: relay_data = {} if smtp: relay_data["smtp"] = smtp if comment: relay_data["comment"] = comment @@ -1037,7 +1047,7 @@ def create_mcp_server() -> FastMCP: @mcp.tool() async def delete_relay(name: str) -> str: """Delete a relay.""" - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.delete(f"/relay/{name}") return f"Delete relay result: {response.json()}" @@ -1046,8 +1056,9 @@ def create_mcp_server() -> FastMCP: # Create MCP server from OpenAPI spec try: + openapi_client = get_mailu_client() mcp = FastMCP.from_openapi( - client=client, + client=openapi_client, openapi_spec=spec, name="Mailu MCP Server", version="1.0.0" @@ -1069,7 +1080,7 @@ def create_mcp_server() -> FastMCP: async def users_resource(ctx: Context) -> str: """List all users in the Mailu instance.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get("/user") response.raise_for_status() return json.dumps(response.json(), indent=2) @@ -1080,7 +1091,7 @@ def create_mcp_server() -> FastMCP: async def domains_resource(ctx: Context) -> str: """List all domains in the Mailu instance.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get("/domain") response.raise_for_status() return json.dumps(response.json(), indent=2) @@ -1091,7 +1102,7 @@ def create_mcp_server() -> FastMCP: async def aliases_resource(ctx: Context) -> str: """List all aliases in the Mailu instance.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get("/alias") response.raise_for_status() return json.dumps(response.json(), indent=2) @@ -1102,7 +1113,7 @@ def create_mcp_server() -> FastMCP: async def alternative_domains_resource(ctx: Context) -> str: """List all alternative domains in the Mailu instance.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get("/alternative") response.raise_for_status() return json.dumps(response.json(), indent=2) @@ -1113,7 +1124,7 @@ def create_mcp_server() -> FastMCP: async def relays_resource(ctx: Context) -> str: """List all relays in the Mailu instance.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get("/relay") response.raise_for_status() return json.dumps(response.json(), indent=2) @@ -1125,7 +1136,7 @@ def create_mcp_server() -> FastMCP: async def user_resource(email: str, ctx: Context) -> str: """Get details of a specific user.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/user/{email}") response.raise_for_status() return json.dumps(response.json(), indent=2) @@ -1136,7 +1147,7 @@ def create_mcp_server() -> FastMCP: async def domain_resource(domain: str, ctx: Context) -> str: """Get details of a specific domain.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/domain/{domain}") response.raise_for_status() return json.dumps(response.json(), indent=2) @@ -1147,7 +1158,7 @@ def create_mcp_server() -> FastMCP: async def alias_resource(alias: str, ctx: Context) -> str: """Get details of a specific alias.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/alias/{alias}") response.raise_for_status() return json.dumps(response.json(), indent=2) @@ -1158,7 +1169,7 @@ def create_mcp_server() -> FastMCP: async def alternative_domain_resource(alt: str, ctx: Context) -> str: """Get details of a specific alternative domain.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/alternative/{alt}") response.raise_for_status() return json.dumps(response.json(), indent=2) @@ -1169,7 +1180,7 @@ def create_mcp_server() -> FastMCP: async def relay_resource(name: str, ctx: Context) -> str: """Get details of a specific relay.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/relay/{name}") response.raise_for_status() return json.dumps(response.json(), indent=2) @@ -1181,7 +1192,7 @@ def create_mcp_server() -> FastMCP: async def domain_users_resource(domain: str, ctx: Context) -> str: """List all users in a specific domain.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/domain/{domain}/users") response.raise_for_status() return json.dumps(response.json(), indent=2) @@ -1192,7 +1203,7 @@ def create_mcp_server() -> FastMCP: async def domain_managers_resource(domain: str, ctx: Context) -> str: """List all managers for a specific domain.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/domain/{domain}/manager") response.raise_for_status() return json.dumps(response.json(), indent=2) @@ -1203,7 +1214,7 @@ def create_mcp_server() -> FastMCP: async def domain_manager_resource(domain: str, email: str, ctx: Context) -> str: """Get details of a specific domain manager.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/domain/{domain}/manager/{email}") response.raise_for_status() return json.dumps(response.json(), indent=2) @@ -1214,7 +1225,7 @@ def create_mcp_server() -> FastMCP: async def aliases_by_domain_resource(domain: str, ctx: Context) -> str: """Find aliases by destination domain.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.get(f"/alias/destination/{domain}") response.raise_for_status() return json.dumps(response.json(), indent=2) @@ -1234,7 +1245,7 @@ def create_mcp_server() -> FastMCP: spam_enabled: bool = True, spam_mark_as_read: bool = False, spam_threshold: int = 80) -> str: """Create a new user in the Mailu instance.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: user_data = { "email": email, "raw_password": raw_password, @@ -1272,7 +1283,7 @@ def create_mcp_server() -> FastMCP: spam_enabled: bool = None, spam_mark_as_read: bool = None, spam_threshold: int = None) -> str: """Update an existing user.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: user_data = {} if raw_password: user_data["raw_password"] = raw_password if comment: user_data["comment"] = comment @@ -1303,7 +1314,7 @@ def create_mcp_server() -> FastMCP: async def delete_user(email: str) -> str: """Delete a user from the Mailu instance.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.delete(f"/user/{email}") response.raise_for_status() return f"Delete user result: {response.json()}" @@ -1317,7 +1328,7 @@ def create_mcp_server() -> FastMCP: max_quota_bytes: int = 0, signup_enabled: bool = False, alternatives: str = "") -> str: """Create a new domain in the Mailu instance.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: domain_data = { "name": name, "comment": comment, @@ -1341,7 +1352,7 @@ def create_mcp_server() -> FastMCP: max_quota_bytes: int = None, signup_enabled: bool = None, alternatives: str = "") -> str: """Update an existing domain.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: domain_data = {} if comment: domain_data["comment"] = comment if max_users is not None: domain_data["max_users"] = max_users @@ -1360,7 +1371,7 @@ def create_mcp_server() -> FastMCP: async def delete_domain(domain: str) -> str: """Delete a domain from the Mailu instance.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.delete(f"/domain/{domain}") response.raise_for_status() return f"Delete domain result: {response.json()}" @@ -1371,7 +1382,7 @@ def create_mcp_server() -> FastMCP: async def generate_dkim_keys(domain: str) -> str: """Generate DKIM keys for a domain.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.post(f"/domain/{domain}/dkim") response.raise_for_status() return f"Generate DKIM keys result: {response.json()}" @@ -1382,7 +1393,7 @@ def create_mcp_server() -> FastMCP: async def auto_configure_domain_security(domain: str) -> str: """Auto-configure complete domain security: DKIM, SPF, DMARC with DNS records.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: # Step 1: Generate DKIM keys dkim_response = await mailu_client.post(f"/domain/{domain}/dkim") dkim_response.raise_for_status() @@ -1528,7 +1539,7 @@ def create_mcp_server() -> FastMCP: async def analyze_domain_security(domain: str) -> str: """Analyze current domain security configuration without making changes.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: # Get domain info with DNS records domain_response = await mailu_client.get(f"/domain/{domain}") domain_response.raise_for_status() @@ -1634,7 +1645,7 @@ def create_mcp_server() -> FastMCP: async def create_domain_manager(domain: str, user_email: str) -> str: """Create a new domain manager.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: manager_data = {"user_email": user_email} response = await mailu_client.post(f"/domain/{domain}/manager", json=manager_data) response.raise_for_status() @@ -1647,7 +1658,7 @@ def create_mcp_server() -> FastMCP: async def delete_domain_manager(domain: str, email: str) -> str: """Delete a domain manager.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.delete(f"/domain/{domain}/manager/{email}") response.raise_for_status() return f"Delete domain manager result: {response.json()}" @@ -1660,7 +1671,7 @@ def create_mcp_server() -> FastMCP: async def create_alias(email: str, destination: str = "", comment: str = "", wildcard: bool = False) -> str: """Create a new alias.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: alias_data = { "email": email, "destination": destination, @@ -1678,7 +1689,7 @@ def create_mcp_server() -> FastMCP: async def update_alias(alias: str, destination: str = "", comment: str = "", wildcard: bool = None) -> str: """Update an existing alias.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: alias_data = {} if destination: alias_data["destination"] = destination if comment: alias_data["comment"] = comment @@ -1694,7 +1705,7 @@ def create_mcp_server() -> FastMCP: async def delete_alias(alias: str) -> str: """Delete an alias.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.delete(f"/alias/{alias}") response.raise_for_status() return f"Delete alias result: {response.json()}" @@ -1708,7 +1719,7 @@ def create_mcp_server() -> FastMCP: async def create_alternative_domain(name: str, domain: str) -> str: """Create a new alternative domain.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: alt_data = {"name": name, "domain": domain} response = await mailu_client.post("/alternative", json=alt_data) response.raise_for_status() @@ -1721,7 +1732,7 @@ def create_mcp_server() -> FastMCP: async def delete_alternative_domain(alt: str) -> str: """Delete an alternative domain.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.delete(f"/alternative/{alt}") response.raise_for_status() return f"Delete alternative domain result: {response.json()}" @@ -1734,7 +1745,7 @@ def create_mcp_server() -> FastMCP: async def create_relay(name: str, smtp: str = "", comment: str = "") -> str: """Create a new relay.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: relay_data = {"name": name, "smtp": smtp, "comment": comment} response = await mailu_client.post("/relay", json=relay_data) response.raise_for_status() @@ -1747,7 +1758,7 @@ def create_mcp_server() -> FastMCP: async def update_relay(name: str, smtp: str = "", comment: str = "") -> str: """Update an existing relay.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: relay_data = {} if smtp: relay_data["smtp"] = smtp if comment: relay_data["comment"] = comment @@ -1762,7 +1773,7 @@ def create_mcp_server() -> FastMCP: async def delete_relay(name: str) -> str: """Delete a relay.""" try: - async with client as mailu_client: + async with get_mailu_client() as mailu_client: response = await mailu_client.delete(f"/relay/{name}") response.raise_for_status() return f"Delete relay result: {response.json()}" diff --git a/uv.lock b/uv.lock index 2ca3f7c..6dab72f 100644 --- a/uv.lock +++ b/uv.lock @@ -613,7 +613,7 @@ wheels = [ [[package]] name = "mcp-mailu" -version = "0.3.0" +version = "0.3.1" source = { editable = "." } dependencies = [ { name = "fastmcp" },