analyze_url
Checks a URL for phishing indicators and suspicious patterns, providing analysis to identify potential scams.
Instructions
Check a URL for phishing indicators and suspicious patterns.
Args: url: The URL to analyze for phishing indicators. api_key: Optional MEOK API key for pro tier.
Behavior: This tool is read-only and stateless — it produces analysis output without modifying any external systems, databases, or files. Safe to call repeatedly with identical inputs (idempotent). Free tier: 10/day rate limit. Pro tier: unlimited. No authentication required for basic usage.
When to use: Use this tool when you need structured analysis or classification of inputs against established frameworks or standards.
When NOT to use: Not suitable for real-time production decision-making without human review of results. Behavioral Transparency: - Side Effects: This tool is read-only and produces no side effects. It does not modify any external state, databases, or files. All output is computed in-memory and returned directly to the caller. - Authentication: No authentication required for basic usage. Pro/Enterprise tiers require a valid MEOK API key passed via the MEOK_API_KEY environment variable. - Rate Limits: Free tier: 10 calls/day. Pro tier: unlimited. Rate limit headers are included in responses (X-RateLimit-Remaining, X-RateLimit-Reset). - Error Handling: Returns structured error objects with 'error' key on failure. Never raises unhandled exceptions. Invalid inputs return descriptive validation errors. - Idempotency: Fully idempotent — calling with the same inputs always produces the same output. Safe to retry on timeout or transient failure. - Data Privacy: No input data is stored, logged, or transmitted to external services. All processing happens locally within the MCP server process.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes | ||
| api_key | No |
Implementation Reference
- server.py:397-397 (registration)Registration of the analyze_url tool via the @mcp.tool() decorator on FastMCP instance.
@mcp.tool() - server.py:398-548 (handler)The analyze_url handler function. Takes a URL and optional API key. Checks HTTPS, phishing indicators (TLD, typosquatting, suspicious structure, IP in URL, excessive subdomains), URL length, @ symbol, redirects, and suspicious file extensions. Returns a risk score, verdict, and recommended action.
def analyze_url( url: str, api_key: str = "", ) -> dict: """Check a URL for phishing indicators and suspicious patterns. Args: url: The URL to analyze for phishing indicators. api_key: Optional MEOK API key for pro tier. Behavior: This tool is read-only and stateless — it produces analysis output without modifying any external systems, databases, or files. Safe to call repeatedly with identical inputs (idempotent). Free tier: 10/day rate limit. Pro tier: unlimited. No authentication required for basic usage. When to use: Use this tool when you need structured analysis or classification of inputs against established frameworks or standards. When NOT to use: Not suitable for real-time production decision-making without human review of results. Behavioral Transparency: - Side Effects: This tool is read-only and produces no side effects. It does not modify any external state, databases, or files. All output is computed in-memory and returned directly to the caller. - Authentication: No authentication required for basic usage. Pro/Enterprise tiers require a valid MEOK API key passed via the MEOK_API_KEY environment variable. - Rate Limits: Free tier: 10 calls/day. Pro tier: unlimited. Rate limit headers are included in responses (X-RateLimit-Remaining, X-RateLimit-Reset). - Error Handling: Returns structured error objects with 'error' key on failure. Never raises unhandled exceptions. Invalid inputs return descriptive validation errors. - Idempotency: Fully idempotent — calling with the same inputs always produces the same output. Safe to retry on timeout or transient failure. - Data Privacy: No input data is stored, logged, or transmitted to external services. All processing happens locally within the MCP server process. """ allowed, msg, tier = check_access(api_key) if not allowed: return {"error": msg, "upgrade_url": "https://meok.ai/pricing"} limit_err = _check_rate_limit("analyze_url", tier) if limit_err: return {"error": "rate_limited", "message": limit_err} url_lower = url.lower().strip() findings = [] # type: List[Dict[str, str]] total_risk = 0.0 # Check for HTTPS if url_lower.startswith("http://"): findings.append({ "finding": "No HTTPS", "severity": "medium", "detail": "URL uses HTTP instead of HTTPS -- data transmitted in plain text", }) total_risk += 0.15 elif not url_lower.startswith("https://"): findings.append({ "finding": "Missing protocol", "severity": "low", "detail": "URL does not specify a protocol", }) # Check phishing indicators for indicator_name, indicator_info in PHISHING_URL_INDICATORS.items(): for pattern in indicator_info["patterns"]: if indicator_name == "ip_in_url" or indicator_name == "excessive_subdomains": if re.search(pattern, url_lower): findings.append({ "finding": indicator_info["description"], "severity": "high" if indicator_info["weight"] > 0.3 else "medium", "detail": "Matched pattern: {}".format(pattern), }) total_risk += indicator_info["weight"] break else: if pattern.lower() in url_lower: findings.append({ "finding": indicator_info["description"], "severity": "high" if indicator_info["weight"] > 0.3 else "medium", "detail": "Matched indicator: {}".format(pattern), }) total_risk += indicator_info["weight"] break # Check URL length (phishing URLs tend to be long) if len(url) > 100: findings.append({ "finding": "Unusually long URL", "severity": "low", "detail": "URL is {} characters (phishing URLs are often longer than legitimate ones)".format(len(url)), }) total_risk += 0.1 # Check for @ symbol (used to obscure real domain) if "@" in url_lower: findings.append({ "finding": "@ symbol in URL", "severity": "high", "detail": "@ symbol can be used to obscure the actual destination domain", }) total_risk += 0.4 # Check for multiple redirects (double slashes after domain) if url_lower.count("//") > 1: double_slash_count = url_lower.count("//") if double_slash_count > 2: findings.append({ "finding": "Multiple double-slashes", "severity": "medium", "detail": "May indicate redirect chains or URL obfuscation", }) total_risk += 0.15 # Check for suspicious file extensions suspicious_extensions = [".exe", ".scr", ".bat", ".cmd", ".vbs", ".js", ".hta", ".pif"] for ext in suspicious_extensions: if url_lower.endswith(ext): findings.append({ "finding": "Suspicious file extension", "severity": "critical", "detail": "URL points to executable file ({}) -- likely malware".format(ext), }) total_risk += 0.5 # Normalize score risk_score = min(1.0, total_risk) if risk_score >= 0.6: verdict = "HIGH RISK -- likely phishing" action = "Do NOT click this link. Do not enter any credentials on this site." elif risk_score >= 0.3: verdict = "MODERATE RISK -- potentially suspicious" action = "Verify the URL through official channels before proceeding." else: verdict = "LOW RISK -- no major indicators" action = "URL appears relatively safe but always verify sender context." return { "url_analyzed": url, "risk_score": round(risk_score, 2), "verdict": verdict, "recommended_action": action, "findings": findings, "url_length": len(url), "uses_https": url_lower.startswith("https://"), "next_step": "Use quick_check on the full message containing this URL for comprehensive scam analysis", "meok_labs": "https://meok.ai", } - server.py:398-548 (schema)Input parameters (url: str, api_key: str = "") and return dict schema for analyze_url tool.
def analyze_url( url: str, api_key: str = "", ) -> dict: """Check a URL for phishing indicators and suspicious patterns. Args: url: The URL to analyze for phishing indicators. api_key: Optional MEOK API key for pro tier. Behavior: This tool is read-only and stateless — it produces analysis output without modifying any external systems, databases, or files. Safe to call repeatedly with identical inputs (idempotent). Free tier: 10/day rate limit. Pro tier: unlimited. No authentication required for basic usage. When to use: Use this tool when you need structured analysis or classification of inputs against established frameworks or standards. When NOT to use: Not suitable for real-time production decision-making without human review of results. Behavioral Transparency: - Side Effects: This tool is read-only and produces no side effects. It does not modify any external state, databases, or files. All output is computed in-memory and returned directly to the caller. - Authentication: No authentication required for basic usage. Pro/Enterprise tiers require a valid MEOK API key passed via the MEOK_API_KEY environment variable. - Rate Limits: Free tier: 10 calls/day. Pro tier: unlimited. Rate limit headers are included in responses (X-RateLimit-Remaining, X-RateLimit-Reset). - Error Handling: Returns structured error objects with 'error' key on failure. Never raises unhandled exceptions. Invalid inputs return descriptive validation errors. - Idempotency: Fully idempotent — calling with the same inputs always produces the same output. Safe to retry on timeout or transient failure. - Data Privacy: No input data is stored, logged, or transmitted to external services. All processing happens locally within the MCP server process. """ allowed, msg, tier = check_access(api_key) if not allowed: return {"error": msg, "upgrade_url": "https://meok.ai/pricing"} limit_err = _check_rate_limit("analyze_url", tier) if limit_err: return {"error": "rate_limited", "message": limit_err} url_lower = url.lower().strip() findings = [] # type: List[Dict[str, str]] total_risk = 0.0 # Check for HTTPS if url_lower.startswith("http://"): findings.append({ "finding": "No HTTPS", "severity": "medium", "detail": "URL uses HTTP instead of HTTPS -- data transmitted in plain text", }) total_risk += 0.15 elif not url_lower.startswith("https://"): findings.append({ "finding": "Missing protocol", "severity": "low", "detail": "URL does not specify a protocol", }) # Check phishing indicators for indicator_name, indicator_info in PHISHING_URL_INDICATORS.items(): for pattern in indicator_info["patterns"]: if indicator_name == "ip_in_url" or indicator_name == "excessive_subdomains": if re.search(pattern, url_lower): findings.append({ "finding": indicator_info["description"], "severity": "high" if indicator_info["weight"] > 0.3 else "medium", "detail": "Matched pattern: {}".format(pattern), }) total_risk += indicator_info["weight"] break else: if pattern.lower() in url_lower: findings.append({ "finding": indicator_info["description"], "severity": "high" if indicator_info["weight"] > 0.3 else "medium", "detail": "Matched indicator: {}".format(pattern), }) total_risk += indicator_info["weight"] break # Check URL length (phishing URLs tend to be long) if len(url) > 100: findings.append({ "finding": "Unusually long URL", "severity": "low", "detail": "URL is {} characters (phishing URLs are often longer than legitimate ones)".format(len(url)), }) total_risk += 0.1 # Check for @ symbol (used to obscure real domain) if "@" in url_lower: findings.append({ "finding": "@ symbol in URL", "severity": "high", "detail": "@ symbol can be used to obscure the actual destination domain", }) total_risk += 0.4 # Check for multiple redirects (double slashes after domain) if url_lower.count("//") > 1: double_slash_count = url_lower.count("//") if double_slash_count > 2: findings.append({ "finding": "Multiple double-slashes", "severity": "medium", "detail": "May indicate redirect chains or URL obfuscation", }) total_risk += 0.15 # Check for suspicious file extensions suspicious_extensions = [".exe", ".scr", ".bat", ".cmd", ".vbs", ".js", ".hta", ".pif"] for ext in suspicious_extensions: if url_lower.endswith(ext): findings.append({ "finding": "Suspicious file extension", "severity": "critical", "detail": "URL points to executable file ({}) -- likely malware".format(ext), }) total_risk += 0.5 # Normalize score risk_score = min(1.0, total_risk) if risk_score >= 0.6: verdict = "HIGH RISK -- likely phishing" action = "Do NOT click this link. Do not enter any credentials on this site." elif risk_score >= 0.3: verdict = "MODERATE RISK -- potentially suspicious" action = "Verify the URL through official channels before proceeding." else: verdict = "LOW RISK -- no major indicators" action = "URL appears relatively safe but always verify sender context." return { "url_analyzed": url, "risk_score": round(risk_score, 2), "verdict": verdict, "recommended_action": action, "findings": findings, "url_length": len(url), "uses_https": url_lower.startswith("https://"), "next_step": "Use quick_check on the full message containing this URL for comprehensive scam analysis", "meok_labs": "https://meok.ai", } - server.py:171-203 (helper)PHISHING_URL_INDICATORS data dictionary used by analyze_url to detect phishing patterns in URLs.
PHISHING_URL_INDICATORS = { "suspicious_tld": { "patterns": [".xyz", ".top", ".club", ".work", ".click", ".loan", ".win", ".gq", ".tk", ".ml", ".cf", ".ga"], "weight": 0.3, "description": "Suspicious top-level domains commonly used in phishing", }, "typosquatting": { "patterns": [ "paypa1", "g00gle", "micros0ft", "amaz0n", "faceb00k", "app1e", "netfl1x", "1inkedin", "twitt3r", "inst4gram", ], "weight": 0.5, "description": "Intentional misspellings of legitimate brands", }, "suspicious_structure": { "patterns": [ "login-", "secure-", "verify-", "update-", "account-", "signin.", "security.", "-alert.", "-confirm.", ], "weight": 0.25, "description": "URL structures designed to look like security pages", }, "ip_in_url": { "patterns": [r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"], "weight": 0.4, "description": "Direct IP address in URL instead of domain name", }, "excessive_subdomains": { "patterns": [r"\w+\.\w+\.\w+\.\w+\.\w+"], "weight": 0.3, "description": "Excessive subdomain depth to obscure actual domain", }, } - server.py:57-72 (helper)Rate limiting helper used by analyze_url (line 440: _check_rate_limit("analyze_url", tier)).
def _check_rate_limit(caller="anonymous", tier="free"): # type: (str, str) -> Optional[str] """Returns error string if rate-limited, else None.""" if tier == "pro": return None now = datetime.now() cutoff = now - timedelta(days=1) _usage[caller] = [t for t in _usage[caller] if t > cutoff] if len(_usage[caller]) >= FREE_DAILY_LIMIT: return ( "Free tier limit reached ({}/day). " "Upgrade to MEOK AI Labs Pro for unlimited access at $29/mo: " "https://meok.ai/mcp/scam-detector/pro".format(FREE_DAILY_LIMIT) ) _usage[caller].append(now) return None