Skip to main content
Glama
paulieb89

UK Legal Research MCP Server

Search UK Legislation

legislation_search
Read-onlyIdempotent

Search UK legislation by title or full text, filter by type or year, and get ranked results with metadata from legislation.gov.uk.

Instructions

Search UK legislation on legislation.gov.uk.

Returns ranked results: title, type, year, number, and legislation.gov.uk URL. Use legislation_get_toc to explore structure, then legislation_get_section for provisions.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
paramsYesLegislationSearchInput with query, optional type filter, optional year.

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultsYesMatching legislation items
totalYesTotal number of matches

Implementation Reference

  • The actual tool handler function that executes legislation_search logic. Searches UK legislation via legislation.gov.uk Atom feed, parses results into LegislationResult objects, and returns a LegislationSearchResult.
    async def legislation_search(params: LegislationSearchInput, ctx: Context) -> LegislationSearchResult:
        """Search UK legislation on legislation.gov.uk.
    
        Returns ranked results: title, type, year, number, and legislation.gov.uk URL.
        Use legislation_get_toc to explore structure, then legislation_get_section for provisions.
    
        Args:
            params: LegislationSearchInput with query, optional type filter, optional year.
        """
        client = ctx.lifespan_context["legislation_http"]
        path = f"/{params.type}" if params.type else "/search"
        # Title search by default — best ranking for "find me Act X". `fulltext`
        # opens up content search across every Act/SI, useful for concept queries.
        qp: dict = {"results-count": 20}
        qp["text" if params.fulltext else "title"] = params.query
        if params.year:
            qp["year"] = params.year
    
        resp = await client.get(f"{LEGISLATION_BASE}{path}", params=qp)
        resp.raise_for_status()
        root = etree.fromstring(resp.content)
    
        total_el = root.findtext(".//os:totalResults", namespaces=ATOM_NS)
        total = int(total_el) if total_el else 0
    
        results = []
        for entry in root.findall(".//a:entry", namespaces=ATOM_NS):
            title = entry.findtext("a:title", namespaces=ATOM_NS) or "Unknown"
            entry_id = entry.findtext("a:id", namespaces=ATOM_NS) or ""
    
            m = _ID_RE.search(entry_id)
            if m:
                leg_type, yr, num = m.group(1), int(m.group(2)), int(m.group(3))
            else:
                leg_type, yr, num = "unknown", 0, 0
    
            results.append(LegislationResult(
                title=title, type=leg_type, year=yr, number=num,
                score=None, url=f"{LEGISLATION_BASE}/{leg_type}/{yr}/{num}",
                next_steps=({
                    "toc": f"legislation://{leg_type}/{yr}/{num}/toc",
                    "section_template": f"legislation://{leg_type}/{yr}/{num}/section/{{section}}",
                    "point_in_time_hint": "Append ?date=YYYY-MM-DD to either URI for historical research",
                } if leg_type != "unknown" else {}),
            ))
    
        return LegislationSearchResult(results=results, total=total or len(results))
  • Input schema for the legislation_search tool with fields: query, type, year, fulltext.
    class LegislationSearchInput(BaseModel):
        model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
    
        query: str = Field(..., description="Search query, e.g. 'Housing Act 1988' or 'data protection personal data'", min_length=1, max_length=500)
        type: str | None = Field(None, description="Filter by type: 'ukpga' (Acts), 'uksi' (SIs), 'asp' (Scottish Acts), 'nia' (NI Acts)")
        year: int | None = Field(None, description="Filter by year of enactment", ge=1800, le=2100)
        fulltext: bool = Field(
            False,
            description=(
                "Default false → searches Act/SI titles only (best for finding a named Act, "
                "e.g. 'Housing Act 1988' returns ukpga/1988/50 first). Set true to search "
                "the full text of every Act/SI for the query (returns SIs and regulations "
                "that cite the term — e.g. 'rental deposits' would return many implementing "
                "instruments)."
            ),
        )
  • Output model for legislation_search results, containing a list of LegislationResult items and total count.
    class LegislationSearchResult(BaseModel):
        """Container for legislation search results."""
    
        results: list[LegislationResult] = Field(..., description="Matching legislation items")
        total: int = Field(..., description="Total number of matches")
  • Registration path: register_tools is called in __init__.py, which triggers the @mcp.tool decorator in tools.py. The tool is mounted on the gateway at line 185 of gateway.py with namespace 'legislation', making the full tool name 'legislation_search'.
    register_tools(legislation_mcp)
    register_prompts(legislation_mcp)
  • Lifespan factory providing the 'legislation_http' client (curl_cffi with Chrome impersonation) used by the handler to call legislation.gov.uk.
    async def http_lifespan(server: FastMCP):
        """Provide shared async HTTP clients for the lifespan of the server."""
        async with httpx.AsyncClient(
            timeout=30.0,
            headers=SHARED_HEADERS,
            follow_redirects=True,
        ) as http, httpx.AsyncClient(
            timeout=30.0,
            headers=XML_HEADERS,
            follow_redirects=True,
        ) as xml_http, CurlAsyncSession(
            impersonate="chrome",
            timeout=30.0,
            allow_redirects=True,
            # Without an explicit Accept, legislation.gov.uk's /search endpoint
            # returns the HTML search page, not the Atom feed our parser needs.
            # Real-world Codex test caught this — legislation_search returned 0 hits
            # for "Housing Act 1988". See PR comment on issue #4.
            headers={"Accept": "application/atom+xml, application/xml, text/xml"},
        ) as legislation_session:
            yield {
                "http": http,
                "xml_http": xml_http,
                "legislation_http": LegislationClient(legislation_session),
            }
Behavior4/5

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

Annotations already declare readOnlyHint and idempotentHint; description adds that results are ranked and includes fields. No contradictions; additional context about return format is useful.

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?

Three sentences, front-loaded with purpose, then results, then workflow. No filler, every sentence earns its place.

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

Completeness4/5

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

Covers return fields and workflow; missing details like pagination or error handling but output schema exists and annotations cover safety. Adequate for a search tool.

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 covers 100% of parameters with detailed descriptions; tool description adds no further parameter semantics beyond what schema provides. Baseline 3 justified.

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 searches UK legislation on legislation.gov.uk, returns ranked results with specific fields, and distinguishes from sibling tools by suggesting subsequent tools for details.

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?

Provides explicit workflow: search first, then use legislation_get_toc and legislation_get_section for further exploration. Does not compare to other sibling search tools but adequate for context.

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