Skip to main content
Glama
paulieb89

UK Legal Research MCP Server

Get Bill Detail

bills_get_bill
Read-onlyIdempotent

Retrieve full details of a UK parliamentary bill including sponsors, current stage, long title, summary, and Royal Assent date. Specify a bill ID and optionally limit summary text length.

Instructions

Get full detail for a specific parliamentary bill.

Returns sponsors, current stage, long title, summary, and Royal Assent date if enacted. Summary text is capped per max_summary_chars — check summary_truncated in the response to see if it was cut.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
paramsYesBillDetailInput with bill_id and optional max_summary_chars.

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
idYesBill ID
short_titleYesShort title of the bill
long_titleNoFull long title
summaryNoBill summary text, possibly truncated per max_summary_chars. Check summary_truncated and summary_original_length for full-text info.
summary_truncatedNoTrue if summary was cut to fit max_summary_chars
summary_original_lengthNoOriginal summary length in characters before any truncation
current_houseNoHouse where the bill currently sits
originating_houseNoHouse where the bill was introduced
current_stageNoCurrent legislative stage
sponsorsNoBill sponsors
stagesNoLegislative stages the bill has passed through
is_actNoWhether the bill has received Royal Assent
royal_assent_dateNoDate Royal Assent was given
urlYesParliament URL for this bill

Implementation Reference

  • The async handler function for bills_get_bill. It receives a BillDetailInput, fetches bill detail from the UK Parliament Bills API, and returns a parsed BillDetail model.
    @mcp.tool(
        name="get_bill",
        annotations={"title": "Get Bill Detail", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True},
    )
    async def bills_get_bill(params: BillDetailInput, ctx: Context) -> BillDetail:
        """Get full detail for a specific parliamentary bill.
    
        Returns sponsors, current stage, long title, summary, and Royal Assent date
        if enacted. Summary text is capped per max_summary_chars — check
        summary_truncated in the response to see if it was cut.
    
        Args:
            params: BillDetailInput with bill_id and optional max_summary_chars.
        """
        client: httpx.AsyncClient = ctx.lifespan_context["http"]
        resp = await client.get(f"{BILLS_BASE}/Bills/{params.bill_id}")
        resp.raise_for_status()
        return _parse_bill_detail(resp.json(), params.max_summary_chars)
  • Input schema for bills_get_bill. Requires bill_id (int, >=1) and optional max_summary_chars (int, 500-50000, default 5000).
    class BillDetailInput(BaseModel):
        model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
    
        bill_id: int = Field(..., description="Bill ID from bills_search_bills results.", ge=1)
        max_summary_chars: int = Field(
            5000,
            ge=500,
            le=50000,
            description=(
                "Maximum characters of the bill summary text to return. Default "
                "5,000 (~1,250 tokens) covers most bills. Raise for substantive "
                "government bills (Finance Act, Levelling-up) whose summary runs "
                "longer. Check summary_truncated in the response to see if it was cut."
            ),
        )
  • Output schema (response model) for bills_get_bill. Contains all bill detail fields: id, title, summary (with truncation info), house, stage, sponsors, stages, is_act, royal_assent_date, and URL.
    class BillDetail(BaseModel):
        model_config = ConfigDict(str_strip_whitespace=True)
    
        id: int = Field(..., description="Bill ID")
        short_title: str = Field(..., description="Short title of the bill")
        long_title: str | None = Field(None, description="Full long title")
        summary: str | None = Field(None, description=(
            "Bill summary text, possibly truncated per max_summary_chars. "
            "Check summary_truncated and summary_original_length for full-text info."
        ))
        summary_truncated: bool = Field(
            False,
            description="True if summary was cut to fit max_summary_chars",
        )
        summary_original_length: int = Field(
            0,
            description="Original summary length in characters before any truncation",
        )
        current_house: str | None = Field(None, description="House where the bill currently sits")
        originating_house: str | None = Field(None, description="House where the bill was introduced")
        current_stage: str | None = Field(None, description="Current legislative stage")
        sponsors: list[BillSponsor] = Field(default_factory=list, description="Bill sponsors")
        stages: list[BillStage] = Field(default_factory=list, description="Legislative stages the bill has passed through")
        is_act: bool = Field(False, description="Whether the bill has received Royal Assent")
        royal_assent_date: Date | None = Field(None, description="Date Royal Assent was given")
        url: str = Field(..., description="Parliament URL for this bill")
  • Helper function that parses the raw API JSON response into a BillDetail model, extracting sponsors, current stage, summary (with truncation), and other metadata.
    def _parse_bill_detail(data: dict, max_summary_chars: int) -> BillDetail:
        sponsors = []
        for s in data.get("sponsors", []):
            member = s.get("member", {})
            sponsors.append(BillSponsor(
                name=member.get("name", s.get("name", "Unknown")),
                party=member.get("party"),
                house=_parse_house(member.get("house")),
            ))
    
        stages = []
        current_stage_name = None
        current_stage_data = data.get("currentStage")
        if isinstance(current_stage_data, dict):
            stage_name = current_stage_data.get("description") or current_stage_data.get("stageName", "Unknown")
            current_stage_name = stage_name
    
            sitting_date = None
            sittings = current_stage_data.get("stageSittings", [])
            if sittings and isinstance(sittings, list):
                date_str = sittings[0].get("date", "")
                if date_str:
                    try:
                        sitting_date = date.fromisoformat(date_str[:10])
                    except ValueError:
                        pass
    
            stages.append(BillStage(
                name=stage_name,
                house=_parse_house(current_stage_data.get("house")),
                date=sitting_date,
                is_current=True,
            ))
    
        royal_assent_date = None
    
        raw_summary = data.get("summary")
        summary: str | None
        if raw_summary:
            summary_original_length = len(raw_summary)
            if summary_original_length > max_summary_chars:
                summary_truncated = True
                summary = raw_summary[:max_summary_chars] + " …[truncated]"
            else:
                summary_truncated = False
                summary = raw_summary
        else:
            summary = None
            summary_truncated = False
            summary_original_length = 0
    
        return BillDetail(
            id=data.get("billId", 0),
            short_title=data.get("shortTitle", "Unknown"),
            long_title=data.get("longTitle"),
            summary=summary,
            summary_truncated=summary_truncated,
            summary_original_length=summary_original_length,
            current_house=_parse_house(data.get("currentHouse")),
            originating_house=_parse_house(data.get("originatingHouse")),
            current_stage=current_stage_name,
            sponsors=sponsors,
            stages=stages,
            is_act=data.get("isAct", False),
            royal_assent_date=royal_assent_date,
            url=f"https://bills.parliament.uk/bills/{data.get('billId', 0)}",
        )
  • Registration of the bills MCP server. register_tools(bills_mcp) is called, which is where the @mcp.tool decorator registers bills_get_bill as the tool named 'get_bill'.
    bills_mcp = FastMCP(
        name="bills",
        instructions=(
            "Search and retrieve UK parliamentary bills. "
            "Use bills_search_bills to find bills by keyword, session, house, or legislative stage. "
            "Use bills_get_bill to get full detail (sponsors, stages, progress) for a specific bill. "
            "All data from bills-api.parliament.uk. No authentication required."
        ),
    )
    
    bills_mcp.add_middleware(ResponseCachingMiddleware(call_tool_settings=CallToolSettings(ttl=3600)))
    
    register_tools(bills_mcp)
Behavior5/5

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

Annotations already indicate readOnly, idempotent, non-destructive. The description adds behavioral context: the summary is capped by max_summary_chars and the response includes a truncation flag. No contradiction with annotations.

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?

Two sentences: first states purpose and outputs, second explains truncation and the truncation flag. Concise, front-loaded, and efficient.

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 and full annotations, the description covers the essential purpose, outputs, and a key behavioral note (truncation). It is complete for agent usage.

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?

Schema description coverage is 100% with detailed parameter descriptions. The tool description only restates the truncation behavior mentioned in the schema, adding no new semantic information.

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 tool gets full detail for a specific parliamentary bill and lists the returned data (sponsors, stage, long title, summary, Royal Assent date). It distinguishes itself from sibling tools like 'bills_search_bills' which is for searching.

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

Usage Guidelines4/5

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

The description clearly indicates that the tool is for getting details of a specific bill, implying the need for a bill_id. However, it does not explicitly mention that the bill_id comes from 'bills_search_bills' or provide when-not-to-use guidance.

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