diff --git a/bunnycdn_mcp/client.py b/bunnycdn_mcp/client.py index 15d9870..f352fe4 100644 --- a/bunnycdn_mcp/client.py +++ b/bunnycdn_mcp/client.py @@ -34,6 +34,15 @@ class BunnyClient: ) return self._handle(resp) + async def put(self, path: str, json: dict | None = None) -> dict | list | str: + async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as c: + resp = await c.put( + f"{self._base}{path}", + headers=self._headers, + json=json, + ) + return self._handle(resp) + async def delete(self, path: str) -> dict | str: async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as c: resp = await c.delete( diff --git a/bunnycdn_mcp/tools/shield.py b/bunnycdn_mcp/tools/shield.py index 7c45086..cc63b8f 100644 --- a/bunnycdn_mcp/tools/shield.py +++ b/bunnycdn_mcp/tools/shield.py @@ -1,4 +1,8 @@ -"""WAF / DDoS Shield tools.""" +"""Bunny Shield tools — Shield Zone API (/shield/shield-zone, /shield/shield-zones). + +NOTE: Shield는 Pull Zone 필드(ShieldDDosProtectionEnabled 등)가 아니라 +별도 Shield Zone API로 관리됨. Pull Zone 업데이트로는 Shield를 켤 수 없음. +""" import json from typing import Annotated @@ -12,67 +16,98 @@ from ..config import logger def register_shield_tools(mcp): @mcp.tool() - async def bunny_shield_status( - pullzone_id: Annotated[int, Field(description="Pull Zone ID")], - ) -> str: - """Get the Shield/WAF/DDoS protection status for a Pull Zone.""" + async def bunny_shield_list() -> str: + """List all Shield Zones with WAF/DDoS configuration.""" try: - zone = await client.get(f"/pullzone/{pullzone_id}") - status = { - "PullZoneId": zone.get("Id"), - "Name": zone.get("Name"), - "WAFEnabled": zone.get("WAFEnabled"), - "WAFEnabledRuleGroups": zone.get("WAFEnabledRuleGroups"), - "WAFDisabledRules": zone.get("WAFDisabledRules"), - "ShieldDDosProtectionEnabled": zone.get("ShieldDDosProtectionEnabled"), - "ShieldDDosProtectionType": zone.get("ShieldDDosProtectionType"), - "OriginShieldEnabled": zone.get("OriginShieldEnabled"), - "OriginShieldZoneCode": zone.get("OriginShieldZoneCode"), - } - return json.dumps(status, indent=2) + data = await client.get("/shield/shield-zones", params={"page": 1, "perPage": 50}) + zones = data.get("data", []) if isinstance(data, dict) else data + result = [] + for z in zones: + result.append({ + "shieldZoneId": z.get("shieldZoneId"), + "pullZoneId": z.get("pullZoneId"), + "wafEnabled": z.get("wafEnabled"), + "dDoSShieldSensitivity": z.get("dDoSShieldSensitivity"), + "dDoSExecutionMode": z.get("dDoSExecutionMode"), + "learningMode": z.get("learningMode"), + "learningModeUntil": z.get("learningModeUntil"), + "planType": z.get("planType"), + }) + return json.dumps(result, indent=2) + except Exception as e: + logger.error("bunny_shield_list failed: %s", e) + return f"Error: {e}" + + @mcp.tool() + async def bunny_shield_status( + shield_zone_id: Annotated[int, Field(description="Shield Zone ID (not Pull Zone ID). Use bunny_shield_list to find it.")], + ) -> str: + """Get Shield Zone configuration by Shield Zone ID.""" + try: + data = await client.get(f"/shield/shield-zone/{shield_zone_id}") + return json.dumps(data, indent=2) except Exception as e: logger.error("bunny_shield_status failed: %s", e) return f"Error: {e}" @mcp.tool() - async def bunny_waf_rules( - pullzone_id: Annotated[int, Field(description="Pull Zone ID")], + async def bunny_shield_create( + pullzone_id: Annotated[int, Field(description="Pull Zone ID to enable Shield on")], + waf_enabled: Annotated[bool, Field(default=True, description="Enable WAF")] = True, + ddos_sensitivity: Annotated[int, Field(default=2, description="DDoS sensitivity: 0=off, 1=low, 2=medium, 3=high, 4=extreme")] = 2, ) -> str: - """List WAF rule groups and their configuration for a Pull Zone.""" + """Create a Shield Zone for a Pull Zone (enables Shield protection). Returns 409 if already exists.""" try: - data = await client.get(f"/shield/waf/{pullzone_id}") + body = { + "pullZoneId": pullzone_id, + "wafEnabled": waf_enabled, + "dDoSShieldSensitivity": ddos_sensitivity, + } + data = await client.post("/shield/shield-zone", json=body) + return json.dumps(data, indent=2) + except Exception as e: + logger.error("bunny_shield_create failed: %s", e) + return f"Error: {e}" + + @mcp.tool() + async def bunny_shield_update( + shield_zone_id: Annotated[int, Field(description="Shield Zone ID (not Pull Zone ID)")], + settings: Annotated[str, Field(description="JSON string of shield settings (e.g. '{\"wafEnabled\": true, \"dDoSShieldSensitivity\": 2}')")], + ) -> str: + """Update Shield Zone configuration by Shield Zone ID.""" + try: + parsed = json.loads(settings) + except json.JSONDecodeError as e: + return f"Error: Invalid JSON: {e}" + try: + data = await client.put(f"/shield/shield-zone/{shield_zone_id}", json=parsed) + return json.dumps(data, indent=2) if isinstance(data, dict) else f"Shield zone {shield_zone_id} updated" + except Exception as e: + logger.error("bunny_shield_update failed: %s", e) + return f"Error: {e}" + + @mcp.tool() + async def bunny_waf_rules( + shield_zone_id: Annotated[int, Field(description="Shield Zone ID")], + ) -> str: + """List WAF rules for a Shield Zone.""" + try: + data = await client.get(f"/shield/waf/{shield_zone_id}") return json.dumps(data, indent=2) except Exception as e: logger.error("bunny_waf_rules failed: %s", e) return f"Error: {e}" - @mcp.tool() - async def bunny_shield_update( - pullzone_id: Annotated[int, Field(description="Pull Zone ID")], - settings: Annotated[str, Field(description="JSON string of shield settings to update (e.g. '{\"ShieldDDosProtectionEnabled\": true, \"WAFEnabled\": true}')")], - ) -> str: - """Update DDoS/WAF/Shield settings for a Pull Zone.""" - try: - parsed = json.loads(settings) - except json.JSONDecodeError as e: - return f"Error: Invalid JSON in settings: {e}" - try: - data = await client.post(f"/pullzone/{pullzone_id}", json=parsed) - return json.dumps(data, indent=2) if isinstance(data, dict) else f"Shield settings updated for Pull Zone {pullzone_id}" - except Exception as e: - logger.error("bunny_shield_update failed: %s", e) - return f"Error: {e}" - @mcp.tool() async def bunny_waf_logs( - pullzone_id: Annotated[int, Field(description="Pull Zone ID")], + shield_zone_id: Annotated[int, Field(description="Shield Zone ID")], page: Annotated[int, Field(default=1, description="Page number")] = 1, per_page: Annotated[int, Field(default=25, description="Results per page (max 100)")] = 25, ) -> str: - """Get WAF event logs for a Pull Zone.""" + """Get WAF event logs for a Shield Zone.""" try: params = {"page": page, "perPage": min(per_page, 100)} - data = await client.get(f"/shield/waf/{pullzone_id}/logs", params=params) + data = await client.get(f"/shield/event-logs/{shield_zone_id}", params=params) return json.dumps(data, indent=2) except Exception as e: logger.error("bunny_waf_logs failed: %s", e)