{"record":{"id":"d23f06cc037c79b8","repo":"D4Vinci/Scrapling","slug":"credentials-dictionary-must-contain-both-username","errorCode":null,"errorMessage":"Credentials dictionary must contain both 'username' and 'password' keys","messagePattern":"Credentials dictionary must contain both 'username' and 'password' keys","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"scrapling/core/ai.py","lineNumber":126,"sourceCode":"            page,\n            css_selector=css_selector,\n            extraction_type=extraction_type,\n            main_content_only=main_content_only,\n        )\n    ]\n    return ResponseModel(status=page.status, content=content, url=page.url)\n\n\ndef _normalize_credentials(credentials: Optional[Dict[str, str]]) -> Optional[Tuple[str, str]]:\n    \"\"\"Convert a credentials dictionary to a tuple accepted by fetchers.\"\"\"\n    if not credentials:\n        return None\n\n    username = credentials.get(\"username\")\n    password = credentials.get(\"password\")\n\n    if username is None or password is None:\n        raise ValueError(\"Credentials dictionary must contain both 'username' and 'password' keys\")\n\n    return username, password\n\n\nclass _StaticTokenVerifier(TokenVerifier):\n    \"\"\"Verifies requests against a single shared bearer token.\"\"\"\n\n    def __init__(self, token: str):\n        self._token = token.encode()\n\n    async def verify_token(self, token: str) -> Optional[AccessToken]:\n        if compare_digest(token.encode(), self._token):\n            return AccessToken(token=token, client_id=\"scrapling-mcp\", scopes=[])\n        return None\n\n\nclass ScraplingMCPServer:\n    def __init__(self, executable_path: Optional[str] = None, auth_token: Optional[str] = None):","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/D4Vinci/Scrapling/blob/5d213a2d4764002bfc4fed33c32fe09fa8b0bf7f/scrapling/core/ai.py#L108-L144","documentation":"The MCP server's proxy/credentials parameter normalizes a dict into a (username, password) tuple for the fetchers via _normalize_credentials. If the dict is non-empty but lacks either 'username' or 'password', it raises ValueError rather than sending an unauthenticated or half-configured request. This is a guard against silent auth misconfiguration.","triggerScenarios":"Calling an MCP fetch/tool call with credentials={'user': 'x'} or credentials={'username': 'x'} (missing password), or keys with different casing ('UserName'). Both keys must be present; empty/None credentials skip auth entirely.","commonSituations":"LLM-generated tool arguments using guessed key names, JSON payloads where one key was dropped, or copying credentials config from another tool that uses 'user'/'pass' naming.","solutions":["Provide both keys: credentials={'username': '...', 'password': '...'}","Check exact key spelling and lowercase (no 'user', 'login', 'pass')","Pass credentials=None (or omit) when the target needs no auth"],"exampleFix":"# before\nawait tool.fetch(url, credentials={'user': 'bob', 'password': 'hunter2'})\n\n# after\nawait tool.fetch(url, credentials={'username': 'bob', 'password': 'hunter2'})","handlingStrategy":"validation","validationCode":"creds = {'username': 'bob', 'password': 'hunter2'}\nassert set(creds) == {'username', 'password'} or not creds, 'credentials need username+password or None'","typeGuard":"from typing import Any, Dict, Optional\n\ndef is_valid_credentials(value: Any) -> bool:\n    if value is None:\n        return True\n    return (\n        isinstance(value, dict)\n        and (not value or ('username' in value and 'password' in value))\n    )","tryCatchPattern":null,"preventionTips":["Define a strict schema for tool arguments the LLM fills (required keys username, password)","Reject partial credential dicts at your boundary instead of forwarding them"],"tags":["ai","mcp","authentication","input-validation"],"backgroundTag":null,"analyzedSha":"5d213a2d4764002bfc4fed33c32fe09fa8b0bf7f","analyzedAt":"2026-08-14T22:23:09.440Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}