zerochan_search
Search Zerochan anime images by tags, with options for multi-tag queries, strict primary tag filtering, and results sorting by recent or popular content.
Instructions
Search Zerochan entries by one or more tags.
Supports single-tag, multi-tag, and strict-mode queries. Zerochan tag names
use Title Case with spaces (e.g. 'Hatsune Miku', not 'hatsune_miku').
Multi-tag example: tags=['Hatsune Miku', 'Flower'] → /Hatsune+Miku,Flower?json
Strict mode: tags=['Rem'] + strict=True → /Rem?json&strict (only entries where Rem is primary tag)
Note: Strict mode only works with a single tag. Passing multiple tags with strict=True
will ignore the strict flag.
Args:
params (SearchByTagInput): Input parameters including:
- username (str): Your Zerochan username (required)
- tags (list[str]): One or more tag names (Title Case preferred)
- strict (bool): If True and single tag, use strict mode (default: False)
- page (int): Page number (default: 1)
- limit (int): Results per page, 1–250 (default: 20)
- sort (SortOrder): 'id' for recent, 'fav' for popular (default: 'id')
- dimensions (Optional[Dimensions]): Filter by image shape
- color (Optional[str]): Filter by dominant color name
- response_format (ResponseFormat): 'markdown' or 'json' (default: 'markdown')
Returns:
str: Matching entries in the requested format.
Markdown: formatted table with ID, tags, dimensions, favorites, links.
JSON: raw API response with all available fields.Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| params | Yes |
Implementation Reference
- server.py:309-369 (handler)The main handler function for zerochan_search tool. Accepts SearchByTagInput params, builds URL path from tags, constructs query parameters, calls zerochan_get helper, and returns formatted results in either Markdown or JSON format.
async def zerochan_search(params: SearchByTagInput) -> str: """Search Zerochan entries by one or more tags. Supports single-tag, multi-tag, and strict-mode queries. Zerochan tag names use Title Case with spaces (e.g. 'Hatsune Miku', not 'hatsune_miku'). Multi-tag example: tags=['Hatsune Miku', 'Flower'] → /Hatsune+Miku,Flower?json Strict mode: tags=['Rem'] + strict=True → /Rem?json&strict (only entries where Rem is primary tag) Note: Strict mode only works with a single tag. Passing multiple tags with strict=True will ignore the strict flag. Args: params (SearchByTagInput): Input parameters including: - username (str): Your Zerochan username (required) - tags (list[str]): One or more tag names (Title Case preferred) - strict (bool): If True and single tag, use strict mode (default: False) - page (int): Page number (default: 1) - limit (int): Results per page, 1–250 (default: 20) - sort (SortOrder): 'id' for recent, 'fav' for popular (default: 'id') - dimensions (Optional[Dimensions]): Filter by image shape - color (Optional[str]): Filter by dominant color name - response_format (ResponseFormat): 'markdown' or 'json' (default: 'markdown') Returns: str: Matching entries in the requested format. Markdown: formatted table with ID, tags, dimensions, favorites, links. JSON: raw API response with all available fields. """ # Build URL path: tags are joined with commas, spaces replaced with + encoded_tags = [t.replace(" ", "+") for t in params.tags] tag_path = ",".join(encoded_tags) path = f"/{tag_path}" query: dict = { "p": params.page, "l": params.limit, "s": params.sort.value, } if params.dimensions is not None: query["d"] = params.dimensions.value if params.color: query["c"] = params.color # Strict mode only works with a single tag use_strict = params.strict and len(params.tags) == 1 if use_strict: query["strict"] = True try: data = await zerochan_get(path, query) except Exception as e: return handle_api_error(e) items = data.get("items", data) if isinstance(data, dict) else data source = " + ".join(params.tags) + (" [strict]" if use_strict else "") if params.response_format == ResponseFormat.JSON: return json.dumps(data, indent=2, ensure_ascii=False) return format_post_list_markdown(items if isinstance(items, list) else [], source) - server.py:299-308 (registration)Tool registration using @mcp.tool decorator with name='zerochan_search', title, and annotations indicating it's read-only, non-destructive, idempotent, and open-world.
@mcp.tool( name="zerochan_search", annotations={ "title": "Search Zerochan by Tag(s)", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True, } ) - server.py:197-224 (schema)SearchByTagInput Pydantic BaseModel defining the input schema for zerochan_search. Includes tags (list of strings), strict mode, pagination, limit, sort, dimensions, color filter, and response_format fields with validators.
class SearchByTagInput(BaseModel): """Input for searching Zerochan entries by one or more tags.""" model_config = ConfigDict(str_strip_whitespace=True, extra="forbid") tags: list[str] = Field( ..., description="One or more tags to filter by (e.g. ['Hatsune Miku'] or ['Hatsune Miku', 'Flower']). Tags are joined with commas in the URL.", min_length=1 ) strict: bool = Field( default=False, description="If True, use strict mode — only returns entries where the FIRST tag is the primary tag. Only works with a single tag." ) page: int = Field(default=1, description="Page number for pagination", ge=1) limit: int = Field(default=DEFAULT_LIMIT, description="Number of results per page (1–250)", ge=1, le=MAX_LIMIT) sort: SortOrder = Field(default=SortOrder.RECENT, description="Sort order: 'id' = most recent, 'fav' = most popular") dimensions: Optional[Dimensions] = Field(default=None, description="Filter by image dimensions: large, huge, landscape, portrait, square") color: Optional[str] = Field(default=None, description="Filter by dominant color, e.g. 'red', 'blue', 'green'", max_length=32) response_format: ResponseFormat = Field(default=ResponseFormat.MARKDOWN, description="Output format: 'markdown' or 'json'") @field_validator("tags") @classmethod def validate_tags(cls, v: list[str]) -> list[str]: cleaned = [t.strip() for t in v if t.strip()] if not cleaned: raise ValueError("At least one non-empty tag is required.") return cleaned - server.py:58-86 (helper)zerochan_get HTTP helper function that performs GET requests to Zerochan API. Handles User-Agent header with username, adds json=True parameter, and returns parsed JSON response or raises appropriate exceptions.
async def zerochan_get(path: str, params: dict) -> dict: """ Perform a GET request to the Zerochan API. Args: path: URL path (e.g. '/Hatsune+Miku') params: Query string parameters (json=True always included) Returns: Parsed JSON response dict Raises: ValueError: If ZEROCHAN_USERNAME env var is not set httpx.HTTPStatusError: On non-2xx responses httpx.TimeoutException: On request timeout """ if not ZEROCHAN_USERNAME: raise ValueError( "ZEROCHAN_USERNAME is not set. Add it to your MCP config: " '"env": {"ZEROCHAN_USERNAME": "YourUsername"}' ) params["json"] = True url = f"{ZEROCHAN_BASE_URL}{path}" user_agent = f"zerochan-mcp - {ZEROCHAN_USERNAME}" async with httpx.AsyncClient(timeout=15.0) as client: response = await client.get(url, params=params, headers={"User-Agent": user_agent}) response.raise_for_status() return response.json() - server.py:111-131 (helper)format_post_list_markdown helper function that formats a list of Zerochan post items into a Markdown table with columns for ID, tags, dimensions, favorites, and full image links.
def format_post_list_markdown(items: list, source: str) -> str: """Format a list of post summaries as a Markdown table.""" if not items: return "No results found." lines = [f"### Zerochan Results from `{source}`\n"] lines.append(f"| ID | Tags | Dimensions | Favorites | Full Image |") lines.append(f"|---|---|---|---|---|") for item in items: entry_id = item.get("id", "N/A") tags = ", ".join(item.get("tags", [])[:5]) if len(item.get("tags", [])) > 5: tags += f" (+{len(item['tags']) - 5} more)" width = item.get("width", "?") height = item.get("height", "?") fav = item.get("fav", "?") full_url = item.get("full", f"https://www.zerochan.net/{entry_id}") lines.append(f"| [{entry_id}]({full_url}) | {tags} | {width}×{height} | {fav} | [View]({full_url}) |") return "\n".join(lines)