Skip to main content
Glama
paulieb89

UK Legal Research MCP Server

Search Committee Evidence

committees_search_evidence
Read-onlyIdempotent

Search oral and written evidence submitted to a parliamentary committee, with pagination support for large result sets.

Instructions

Search oral and written evidence submitted to a parliamentary committee.

Returns ONE PAGE of evidence (default 20). Free-text titles are capped per max_title_chars; witness lists are capped at 10 per item. For committees with many submissions, re-call with offset=offset+returned while has_more is true.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
paramsYesCommitteeEvidenceInput with committee_id, evidence_type, offset/limit pagination, and max_title_chars.

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
committee_idYesCommittee ID this page belongs to
evidence_typeYesEvidence type filter applied to this query
offsetYesNumber of evidence items skipped before this page
limitYesMax evidence items requested for this page
returnedYesNumber of evidence items actually returned in this call
has_moreYesTrue if there may be more evidence beyond this page. Re-call with offset=offset+returned to fetch the next page. Conservative: when evidence_type='both', True if either oral or written upstream page came back full.
evidenceNoEvidence items in this page. Titles are capped per max_title_chars; witness lists are capped at 10 per item.

Implementation Reference

  • The async function committees_search_evidence — the actual handler. Uses CommitteeEvidenceInput schema, fetches from committees-api.parliament.uk (OralEvidence and WrittenEvidence endpoints), caps titles per max_title_chars, and returns a CommitteeEvidencePage with has_more pagination flag.
    async def committees_search_evidence(params: CommitteeEvidenceInput, ctx: Context) -> CommitteeEvidencePage:
        """Search oral and written evidence submitted to a parliamentary committee.
    
        Returns ONE PAGE of evidence (default 20). Free-text titles are capped
        per max_title_chars; witness lists are capped at 10 per item. For
        committees with many submissions, re-call with offset=offset+returned
        while has_more is true.
    
        Args:
            params: CommitteeEvidenceInput with committee_id, evidence_type,
                offset/limit pagination, and max_title_chars.
        """
        client: httpx.AsyncClient = ctx.lifespan_context["http"]
    
        def _cap_title(t: str) -> str:
            if len(t) > params.max_title_chars:
                return t[: params.max_title_chars] + " …[truncated]"
            return t
    
        async def fetch_oral(skip: int, take: int) -> tuple[list[EvidenceItem], int]:
            resp = await client.get(
                f"{COMMITTEES_BASE}/OralEvidence",
                params={"CommitteeId": params.committee_id, "Skip": skip, "Take": take},
            )
            resp.raise_for_status()
            data = resp.json()
            items = data.get("items", data.get("results", data)) if isinstance(data, dict) else data
            if not isinstance(items, list):
                return [], 0
            results: list[EvidenceItem] = []
            for item in items:
                ev_date = item.get("evidenceDate") or item.get("date")
                witnesses: list[str] = []
                for w in item.get("witnesses", []):
                    if isinstance(w, str):
                        witnesses.append(w)
                    elif isinstance(w, dict):
                        witnesses.append(w.get("name", str(w)))
                results.append(EvidenceItem(
                    id=item.get("id", 0),
                    type="oral",
                    title=_cap_title(item.get("title", item.get("sessionTitle", "Oral evidence session"))),
                    date=date.fromisoformat(ev_date[:10]) if ev_date else None,
                    witnesses=(witnesses[:10] or None),
                    url=item.get("url"),
                ))
            return results, len(items)
    
        async def fetch_written(skip: int, take: int) -> tuple[list[EvidenceItem], int]:
            resp = await client.get(
                f"{COMMITTEES_BASE}/WrittenEvidence",
                params={"CommitteeId": params.committee_id, "Skip": skip, "Take": take},
            )
            resp.raise_for_status()
            data = resp.json()
            items = data.get("items", data.get("results", data)) if isinstance(data, dict) else data
            if not isinstance(items, list):
                return [], 0
            results: list[EvidenceItem] = []
            for item in items:
                ev_date = item.get("dateReceived") or item.get("date")
                results.append(EvidenceItem(
                    id=item.get("id", 0),
                    type="written",
                    title=_cap_title(item.get("title", "Written evidence")),
                    date=date.fromisoformat(ev_date[:10]) if ev_date else None,
                    witnesses=None,
                    url=item.get("url"),
                ))
            return results, len(items)
    
        evidence: list[EvidenceItem] = []
        has_more = False
    
        if params.evidence_type == "oral":
            evidence, raw = await fetch_oral(params.offset, params.limit)
            has_more = raw == params.limit
        elif params.evidence_type == "written":
            evidence, raw = await fetch_written(params.offset, params.limit)
            has_more = raw == params.limit
        else:
            oral_take = (params.limit + 1) // 2  # remainder to oral
            written_take = params.limit // 2
            (oral, oral_raw), (written, written_raw) = await asyncio.gather(
                fetch_oral(params.offset, oral_take),
                fetch_written(params.offset, written_take),
            )
            evidence = oral + written
            has_more = (oral_raw == oral_take) or (written_raw == written_take)
    
        return CommitteeEvidencePage(
            committee_id=params.committee_id,
            evidence_type=params.evidence_type,
            offset=params.offset,
            limit=params.limit,
            returned=len(evidence),
            has_more=has_more,
            evidence=evidence,
        )
  • CommitteeEvidenceInput Pydantic model — input schema for the tool. Fields: committee_id, evidence_type (oral/written/both), offset, limit, max_title_chars.
    class CommitteeEvidenceInput(BaseModel):
        model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
    
        committee_id: int = Field(..., description="Committee ID from committees_search_committees results.", ge=1)
        evidence_type: Literal["oral", "written", "both"] = Field("both", description="Type of evidence to search.")
        offset: int = Field(
            0,
            ge=0,
            le=2000,
            description=(
                "Number of evidence items to skip before this page. Default 0. "
                "Re-call with offset=offset+returned while has_more is true."
            ),
        )
        limit: int = Field(
            20,
            ge=1,
            le=100,
            description=(
                "Maximum evidence items to return. Default 20. When evidence_type='both' "
                "the limit is split across oral and written (roughly half each)."
            ),
        )
        max_title_chars: int = Field(
            300,
            ge=50,
            le=2000,
            description=(
                "Per-item cap on the free-text title field. Default 300 prevents "
                "context blow-up from verbose inquiry titles. Raise to 1000+ only "
                "when you need the full title text."
            ),
        )
  • EvidenceItem and CommitteeEvidencePage Pydantic models — output schema. EvidenceItem has id, type, title, date, witnesses, url. CommitteeEvidencePage has committee_id, evidence_type, offset, limit, returned, has_more, evidence.
    class EvidenceItem(BaseModel):
        model_config = ConfigDict(str_strip_whitespace=True)
    
        id: int = Field(..., description="Evidence item ID")
        type: Literal["oral", "written"] = Field(..., description="Type of evidence")
        title: str = Field(..., description="Evidence title or session description (may be truncated per max_title_chars)")
        date: Date | None = Field(None, description="Date the evidence was given or submitted")
        witnesses: list[str] | None = Field(None, description="Witness names (oral evidence only, capped at 10 per item)")
        url: str | None = Field(None, description="URL to the evidence document")
    
    
    class CommitteeEvidencePage(BaseModel):
        """A page of evidence submissions to a parliamentary committee.
    
        Returned by committees_search_evidence. Callers paginate by
        re-calling with offset=offset+returned while has_more is True.
        When evidence_type="both", oral and written evidence are
        interleaved in a single `evidence` list and the limit is split
        across both.
        """
    
        model_config = ConfigDict(str_strip_whitespace=True)
    
        committee_id: int = Field(..., description="Committee ID this page belongs to")
        evidence_type: Literal["oral", "written", "both"] = Field(
            ..., description="Evidence type filter applied to this query"
        )
        offset: int = Field(..., description="Number of evidence items skipped before this page")
        limit: int = Field(..., description="Max evidence items requested for this page")
        returned: int = Field(..., description="Number of evidence items actually returned in this call")
        has_more: bool = Field(
            ...,
            description=(
                "True if there may be more evidence beyond this page. Re-call with "
                "offset=offset+returned to fetch the next page. Conservative: when "
                "evidence_type='both', True if either oral or written upstream page "
                "came back full."
            ),
        )
        evidence: list[EvidenceItem] = Field(
            default_factory=list,
            description=(
                "Evidence items in this page. Titles are capped per max_title_chars; "
                "witness lists are capped at 10 per item."
            ),
        )
  • Registration decorator: @mcp.tool(name='search_evidence', ...) which registers the tool with FastMCP under the name 'search_evidence' (mounted under 'committees' namespace, so full name is 'committees_search_evidence').
    @mcp.tool(
        name="search_evidence",
        annotations={"title": "Search Committee Evidence", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True},
    )
  • Module-level FastMCP instance creation and tool registration. committees_mcp is created, middleware added, and register_tools(committees_mcp) is called to register all tools. This MCP is then mounted at gateway.py line 189 under namespace 'committees'.
    committees_mcp = FastMCP(
        name="committees",
        instructions=(
            "Search UK parliamentary select committees and their evidence submissions. "
            "Use committees_search_committees to find committees by name, house, or status. "
            "Use committees_get_committee to get detail including current membership. "
            "Use committees_search_evidence to find oral and written evidence for a committee. "
            "All data from committees-api.parliament.uk. No authentication required."
        ),
    )
    
    committees_mcp.add_middleware(ResponseCachingMiddleware(call_tool_settings=CallToolSettings(ttl=3600)))
    
    register_tools(committees_mcp)
    
    __all__ = ["committees_mcp"]
Behavior4/5

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

Annotations indicate readOnlyHint=true, destructiveHint=false, idempotentHint=true, openWorldHint=true. The description adds behavioral details: returns one page (default 20), truncation of free-text titles via max_title_chars, and witness lists capped at 10 per item. This complements the annotations without 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?

Two sentences front-load the purpose and essential behavioral details. No redundant information; every sentence adds value. The structure is optimal.

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?

With an output schema present, the description need not explain return values. It covers pagination, truncation, and parameter usage. For a search tool with pagination, this is complete and self-contained.

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

Parameters4/5

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

Schema description coverage is 100%, so baseline is 3. The description adds value by explaining pagination behavior (offset/limit), the splitting of limit when evidence_type='both', and the rationale for max_title_chars (prevents context blow-up).

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 'Search oral and written evidence submitted to a parliamentary committee,' specifying the verb 'search' and the resource 'evidence submitted to a parliamentary committee.' This distinguishes it from sibling tools like committees_search_committees (which searches for committees) and committees_get_committee (which gets a specific committee).

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 provides clear usage context for pagination ('re-call with offset=offset+returned while has_more is true') and mentions the scenario of committees with many submissions. It does not explicitly state when not to use or list alternatives, but the purpose is sufficiently clear from the sibling 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