Skip to main content
Glama
paulieb89

UK Legal Research MCP Server

Search Parliamentary Bills

bills_search_bills
Read-onlyIdempotent

Find 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

TableJSON Schema
NameRequiredDescriptionDefault
paramsYesBillSearchInput with query, optional session/house/stage filters, pagination.

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYesThe search term that was used
offsetYesNumber of results skipped before this page
limitYesMaximum results requested in this call
returnedYesNumber of results actually on this page
totalNoTotal results matching the query across all pages, if the upstream API reported it. None if unknown.
has_moreYesTrue if more results exist beyond this page. Re-call with offset=offset+returned to fetch the next page.
billsNoMatching bills. Use the integer `id` field from any bill to call bills_get_bill for full detail.

Implementation Reference

  • 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,
        )
  • 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."
            ),
        )
  • 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."
            ),
        )
  • 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},
        )
  • _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)}",
        )
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, destructiveHint=false, idempotentHint=true, and openWorldHint=true, indicating a safe, idempotent query. The description adds that the tool returns a paginated page of bill summaries including title, current stage, and whether it has become an Act, which adds useful behavioral context beyond annotations. No contradiction.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise with three sentences. The first sentence states the purpose and filters, the second explains the output, and the third directs to the sibling. Every sentence serves a purpose without any waste.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the presence of an output schema (documenting return values), good annotations, and a clear description that covers the search scope, filters, pagination, and sibling tool usage, the description is fully complete for an AI agent to select and invoke the tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 100% description coverage, so the schema itself already documents each parameter well. The description does not add much beyond the schema, but the schema descriptions are sufficiently detailed. Baseline 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the verb 'Search' and the resource 'UK parliamentary bills' with specific filters (keyword, session, house, stage). It distinguishes from the sibling 'bills_get_bill' by noting it returns summaries vs full detail.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly directs to use 'bills_get_bill' for full detail when a bill ID is available, providing clear guidance on when to use an alternative. The pagination context is also implied through the output description.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/paulieb89/uk-legal-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server