Skip to main content
Glama
paulieb89

UK Legal Research MCP Server

Parliamentary Policy Vibe Check

parliament_vibe_check
Read-only

Search Hansard for debate contributions related to a policy proposal, then classify sentiment and extract supporters, opponents, and key concerns to assess parliamentary reception.

Instructions

Assess the likely parliamentary reception of a policy proposal.

Searches Hansard for relevant debate contributions, then uses LLM sampling to classify sentiment and extract supporters, opponents, and key concerns.

Degrades gracefully if sampling is unavailable — returns contributions only.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
paramsYesPolicyVibeInput with policy_text (full description) and topic (search keyword).

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYesThe policy query that was analysed
contributionsYesRaw contributions retrieved from Hansard
sentiment_summaryNoLLM-generated sentiment summary (None if sampling unavailable)
key_supportersNoMembers identified as supportive
key_opponentsNoMembers identified as opposed or critical
key_concernsNoMain concerns raised in debate

Implementation Reference

  • The main handler function for the parliament_vibe_check tool. It queries the Hansard API for contributions matching the topic, then uses LLM sampling (ctx.sample) to classify sentiment and extract supporters, opponents, and key concerns. Degrades gracefully if sampling is unavailable.
    @mcp.tool(
        name="vibe_check",
        annotations={"title": "Parliamentary Policy Vibe Check", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": False, "openWorldHint": True},
    )
    async def parliament_vibe_check(params: PolicyVibeInput, ctx: Context) -> PolicyVibeResult:
        """Assess the likely parliamentary reception of a policy proposal.
    
        Searches Hansard for relevant debate contributions, then uses LLM sampling
        to classify sentiment and extract supporters, opponents, and key concerns.
    
        Degrades gracefully if sampling is unavailable — returns contributions only.
    
        Args:
            params: PolicyVibeInput with policy_text (full description) and topic (search keyword).
        """
        client: httpx.AsyncClient = ctx.lifespan_context["http"]
        resp = await client.get(
            f"{HANSARD_API}/search.json",
            params={"searchTerm": params.topic, "take": 15},
        )
        resp.raise_for_status()
        contributions = _parse_hansard_contributions(resp.json())
    
        if not contributions:
            return PolicyVibeResult(
                query=params.topic, contributions=[],
                sentiment_summary="No Hansard contributions found for this topic.",
                key_supporters=[], key_opponents=[], key_concerns=[],
            )
    
        contributions_text = "\n\n".join(
            f"{c.member_name} ({c.party or 'Unknown'}, {c.date}):\n{c.text[:500]}"
            for c in contributions[:10]
        )
        sample_prompt = (
            f"Policy proposal: {params.policy_text}\n\n"
            f"Relevant Hansard contributions:\n{contributions_text}\n\n"
            f"Respond ONLY with a JSON object (no markdown fences):\n"
            '{"sentiment_summary": "...", "key_supporters": [...], '
            '"key_opponents": [...], "key_concerns": [...]}'
        )
    
        sentiment_summary: str | None = None
        key_supporters: list[str] = []
        key_opponents: list[str] = []
        key_concerns: list[str] = []
    
        try:
            result = await ctx.sample(sample_prompt, result_type=str)
            raw = (result.text or "").strip().lstrip("```json").lstrip("```").rstrip("```")
            parsed = json.loads(raw)
            sentiment_summary = parsed.get("sentiment_summary")
            key_supporters = parsed.get("key_supporters", [])
            key_opponents = parsed.get("key_opponents", [])
            key_concerns = parsed.get("key_concerns", [])
        except Exception:
            sentiment_summary = "Sentiment analysis unavailable (sampling not supported by this client)."
    
        return PolicyVibeResult(
            query=params.topic, contributions=contributions,
            sentiment_summary=sentiment_summary, key_supporters=key_supporters,
            key_opponents=key_opponents, key_concerns=key_concerns,
        )
  • PolicyVibeInput schema: validates policy_text (10-2000 chars) and topic (2-200 chars).
    class PolicyVibeInput(BaseModel):
        model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
    
        policy_text: str = Field(..., description="Description of the policy proposal to assess", min_length=10, max_length=2000)
        topic: str = Field(..., description=(
            "Search terms for Hansard (keyword search, not phrase-matched). "
            "Use 2-5 key terms, e.g. 'artificial intelligence financial services regulation'. "
            "Broader terms return more contributions for sentiment analysis."
        ), min_length=2, max_length=200)
  • PolicyVibeResult schema: defines the output shape with query, contributions, sentiment_summary, key_supporters, key_opponents, key_concerns.
    class PolicyVibeResult(BaseModel):
        """Result of a parliamentary sentiment analysis on a policy topic."""
    
        query: str = Field(..., description="The policy query that was analysed")
        contributions: list[HansardContribution] = Field(..., description="Raw contributions retrieved from Hansard")
        sentiment_summary: str | None = Field(None, description="LLM-generated sentiment summary (None if sampling unavailable)")
        key_supporters: list[str] = Field(default_factory=list, description="Members identified as supportive")
        key_opponents: list[str] = Field(default_factory=list, description="Members identified as opposed or critical")
        key_concerns: list[str] = Field(default_factory=list, description="Main concerns raised in debate")
  • The register_tools function that registers the tool via @mcp.tool decorator with the name 'vibe_check'. The tool is registered on the parliament_mcp FastMCP instance.
    def register_tools(mcp: FastMCP) -> None:
    
        @mcp.tool(
            name="search_hansard",
            annotations={"title": "Search Hansard Debates", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True},
        )
        async def parliament_search_hansard(params: HansardSearchInput, ctx: Context) -> HansardSearchResult:
            """Search Hansard for parliamentary debates, questions, and speeches.
    
            Returns contributions from MPs and Lords including date, party, debate title,
            and text (capped at 3000 chars per contribution). Useful for understanding
            legislative intent or political context.
    
            Args:
                params: HansardSearchInput with query, optional date range, optional member filter.
            """
            client: httpx.AsyncClient = ctx.lifespan_context["http"]
            qp: dict = {
                "searchTerm": f'"{params.query}"',
                "take": params.limit,
                "skip": params.offset,
            }
            if params.from_date:
                qp["startDate"] = params.from_date.isoformat()
            if params.to_date:
                qp["endDate"] = params.to_date.isoformat()
            if params.member:
                qp["member"] = params.member
    
            resp = await client.get(f"{HANSARD_API}/search.json", params=qp)
            resp.raise_for_status()
            contributions = _parse_hansard_contributions(resp.json())
            return HansardSearchResult(
                query=params.query,
                from_date=params.from_date,
                to_date=params.to_date,
                member=params.member,
                offset=params.offset,
                limit=params.limit,
                total=len(contributions),
                has_more=len(contributions) == params.limit,
                contributions=contributions,
            )
    
        @mcp.tool(
            name="vibe_check",
            annotations={"title": "Parliamentary Policy Vibe Check", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": False, "openWorldHint": True},
        )
  • _parse_hansard_contributions helper: parses the Hansard API response into HansardContribution objects, used by the vibe check handler.
    def _parse_hansard_contributions(data: dict) -> list[HansardContribution]:
        """Parse hansard-api.parliament.uk search.json response."""
        contributions = []
        for item in data.get("Contributions", []):
            try:
                # Extract attribution like "Lord Carlile of Berriew (CB)"
                attr = item.get("AttributedTo", "")
                name = item.get("MemberName", "Unknown")
                party = None
                if "(" in attr and ")" in attr:
                    party = attr[attr.rfind("(") + 1:attr.rfind(")")]
    
                text = _strip_html(item.get("ContributionText", ""))
    
                contributions.append(HansardContribution(
                    member_name=name,
                    party=party,
                    constituency=None,
                    date=date.fromisoformat(item.get("SittingDate", "1970-01-01")[:10]),
                    debate_title=item.get("DebateSectionName", item.get("Section", "Unknown")),
                    section=item.get("HansardSection", item.get("House", "Unknown")),
                    text=text[:3000],
                    url=item.get("Url", ""),
                ))
            except Exception:
                continue
        return contributions
Behavior4/5

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

Annotations provide readOnlyHint(true) and destructiveHint(false), and the description adds behavioral details: it uses LLM sampling and degrades gracefully by returning contributions only if sampling is unavailable. No contradictions.

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: first states purpose, second explains process, third covers fallback. All information is relevant and front-loaded; no wasted words.

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?

The description explains what the tool does and how it behaves under limitations (graceful degradation). An output schema exists to cover return format. Missing explicit mention of scope (UK Hansard) but acceptable given sibling context.

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 coverage is 100% with well-described parameters. The description adds context by explaining that topic is used as search terms for Hansard and that broader terms return more contributions, which enhances understanding beyond schema.

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 assesses parliamentary reception of a policy proposal using Hansard and LLM sampling. It is distinct from sibling tools like parliament_search_hansard which only search, and the verb 'assess' with resource 'parliamentary reception' is specific.

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 implies usage for policy reception analysis and mentions graceful degradation when sampling is unavailable. However, it does not explicitly state when to use this tool versus alternatives or provide exclusions, though the context is clear.

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