Search Parliamentary Bills
bills_search_billsFind UK parliamentary bills by keyword, session, house, or legislative stage. Returns paginated summaries with title and current stage.
Instructions
Search UK parliamentary bills by keyword, session, house, or legislative stage.
Returns a paginated page of bill summaries including title, current stage, and whether it has become an Act. Use bills_get_bill with the bill ID for full detail.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| params | Yes | BillSearchInput with query, optional session/house/stage filters, pagination. |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | The search term that was used | |
| offset | Yes | Number of results skipped before this page | |
| limit | Yes | Maximum results requested in this call | |
| returned | Yes | Number of results actually on this page | |
| total | No | Total results matching the query across all pages, if the upstream API reported it. None if unknown. | |
| has_more | Yes | True if more results exist beyond this page. Re-call with offset=offset+returned to fetch the next page. | |
| bills | No | Matching bills. Use the integer `id` field from any bill to call bills_get_bill for full detail. |
Implementation Reference
- src/modules/bills/tools.py:186-230 (handler)The main handler function for bills_search_bills. It builds query parameters from BillSearchInput, calls the UK Parliament Bills API (/Bills endpoint), parses results into BillSummary objects, and returns a paginated BillSearchResult.
async def bills_search_bills(params: BillSearchInput, ctx: Context) -> BillSearchResult: """Search UK parliamentary bills by keyword, session, house, or legislative stage. Returns a paginated page of bill summaries including title, current stage, and whether it has become an Act. Use bills_get_bill with the bill ID for full detail. Args: params: BillSearchInput with query, optional session/house/stage filters, pagination. """ client: httpx.AsyncClient = ctx.lifespan_context["http"] qp: dict = { "SearchTerm": params.query, "Take": params.limit, "Skip": params.offset, } if params.session is not None: qp["Session"] = params.session if params.house and params.house != "All": qp["CurrentHouse"] = HOUSE_MAP.get(params.house) if params.stage: qp["BillStage"] = STAGE_ID_MAP[params.stage] resp = await client.get(f"{BILLS_BASE}/Bills", params=qp) resp.raise_for_status() data = resp.json() bills = [_parse_bill_summary(item) for item in data.get("items", [])] total = data.get("totalResults") if not isinstance(total, int): total = None has_more = ( (params.offset + len(bills)) < total if total is not None else len(bills) == params.limit ) return BillSearchResult( query=params.query, offset=params.offset, limit=params.limit, returned=len(bills), total=total, has_more=has_more, bills=bills, ) - src/modules/bills/tools.py:32-65 (schema)BillSearchInput - input schema for bills_search_bills: query, session, house, stage, offset, limit.
class BillSearchInput(BaseModel): model_config = ConfigDict(str_strip_whitespace=True, extra="forbid") query: str = Field(..., description=( "Search term for bill titles and descriptions, " "e.g. 'online safety' or 'financial services'." ), min_length=1, max_length=500) session: int | None = Field(None, description=( "Parliamentary session ID. Omit to search all sessions. " "Session numbers change each year (e.g. 40 = 2024-25, 39 = 2023-24)." ), ge=1) house: Literal["Commons", "Lords", "All"] | None = Field(None, description="Filter by originating house. Omit for all houses.") stage: Literal["firstreading", "secondreading", "committee", "report", "thirdreading", "royalassent"] | None = Field( None, description="Filter by current legislative stage." ) offset: int = Field( 0, ge=0, le=2000, description=( "Number of results to skip before this page. Default 0 for the " "first page. Re-call with offset=offset+returned while has_more " "is true to paginate." ), ) limit: int = Field( 20, ge=1, le=100, description=( "Maximum bills to return in this call. Default 20 keeps " "responses focused; raise up to 100 for bulk exports." ), ) - src/modules/bills/models.py:37-71 (schema)BillSearchResult - output model wrapping a list of BillSummary with pagination metadata (query, offset, limit, returned, total, has_more, bills).
class BillSearchResult(BaseModel): """One page of bill search results. Wraps matching BillSummary records with search metadata so the LLM client sees a real nested object on the wire, and can page through large result sets. """ model_config = ConfigDict(str_strip_whitespace=True) query: str = Field(..., description="The search term that was used") offset: int = Field(..., description="Number of results skipped before this page") limit: int = Field(..., description="Maximum results requested in this call") returned: int = Field(..., description="Number of results actually on this page") total: int | None = Field( None, description=( "Total results matching the query across all pages, if the " "upstream API reported it. None if unknown." ), ) has_more: bool = Field( ..., description=( "True if more results exist beyond this page. Re-call with " "offset=offset+returned to fetch the next page." ), ) bills: list[BillSummary] = Field( default_factory=list, description=( "Matching bills. Use the integer `id` field from any bill to " "call bills_get_bill for full detail." ), ) - src/modules/bills/tools.py:180-185 (registration)registration via @mcp.tool decorator with name='search_bills' inside register_tools(), which is called from bills/__init__.py.
def register_tools(mcp: FastMCP) -> None: @mcp.tool( name="search_bills", annotations={"title": "Search Parliamentary Bills", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True}, ) - src/modules/bills/tools.py:93-108 (helper)_parse_bill_summary - helper that converts each raw API item dict into a BillSummary model used by the handler.
def _parse_bill_summary(item: dict) -> BillSummary: current_stage_raw = item.get("currentStage") current_stage = None if isinstance(current_stage_raw, dict): stage_name = current_stage_raw.get("stageName") or current_stage_raw.get("description") current_stage = stage_name return BillSummary( id=item.get("billId", 0), short_title=item.get("shortTitle", "Unknown"), long_title=item.get("longTitle"), current_house=_parse_house(item.get("currentHouse")), current_stage=current_stage, is_act=item.get("isAct", False), url=f"https://bills.parliament.uk/bills/{item.get('billId', 0)}", )