Skip to main content
Glama
kupad95

UK Parliament MCP Server

by kupad95

rank_entities

Identify the most rebellious MPs by ranking them based on rebellion count, showing how many times each voted against their party whip. Filter by party or house to get a targeted leaderboard of MPs who rebelled most frequently.

Instructions

Rank MPs by rebellion count — how many times they voted against their party whip. Use this for any question about 'most rebellious MPs', 'rebel count', 'which Labour/Conservative MPs rebelled most', or rebellion frequency rankings. Scans all divisions in the date range internally and returns a sorted leaderboard in a single call. Filter by party='Labour' for Labour-specific results.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
entity_typeYesThe type of entity to rank.
metricYesThe metric to rank by.
partyNoFilter to a specific party (e.g. 'Labour').
houseNoWhich house to scan. Defaults to Commons.
date_fromNoStart date in YYYY-MM-DD format. Defaults to 2024-07-05 (current parliament).
max_divisionsNoMaximum number of divisions to scan. Default 100, max 500.
min_rebellionsNoMinimum rebellions to appear in the leaderboard. Default 1.
limitNoMaximum number of MPs to return. Default 20.

Implementation Reference

  • Main handler function for the rank_entities tool. Accepts arguments (entity_type, metric, party, house, date_from, max_divisions, min_rebellions, limit), fetches division summaries and details, detects rebels by comparing individual votes against party majority direction, builds a leaderboard sorted by rebellion count, and returns a formatted text result.
    export async function handleRankTool(
      name: string,
      args: Record<string, unknown>
    ): Promise<string> {
      try {
        if (name !== "rank_entities") {
          throw new Error(`Unknown tool: ${name}`);
        }
    
        const entityType = args.entity_type as string;
        const metric = args.metric as string;
    
        if (entityType !== "mp" || metric !== "rebellions") {
          throw new Error(
            `Unsupported entity_type/metric combination: ${entityType}/${metric}`
          );
        }
    
        const house = (args.house as "Commons" | "Lords") ?? "Commons";
        const dateFrom = (args.date_from as string) ?? "2024-07-05";
        const maxDivisions = Math.min(
          (args.max_divisions as number) ?? 100,
          500
        );
        const minRebellions = (args.min_rebellions as number) ?? 1;
        const limit = (args.limit as number) ?? 20;
        const partyFilter = args.party as string | undefined;
    
        // Fetch division summaries
        const summaries = await fetchDivisionSummaries(house, dateFrom, maxDivisions);
    
        // Fetch all division details in parallel batches
        const rebelMap = new Map<number, RebelRecord>();
    
        const details = await batchedFetch(
          summaries,
          (summary: DivisionSummary) => fetchDivisionDetail(house, summary.id)
        );
    
        for (let i = 0; i < summaries.length; i++) {
          const detail = details[i];
          if (!detail) continue;
          const summary = summaries[i];
    
          const rebels =
            house === "Commons"
              ? detectCommonsRebels(detail as CommonsDivisionDetail)
              : detectLordsRebels(detail as LordsDivisionDetail);
    
          for (const rebel of rebels) {
            const existing = rebelMap.get(rebel.memberId);
            if (existing) {
              existing.count += 1;
              if (existing.recentDivisions.length < 2) {
                existing.recentDivisions.push(summary.title);
              }
            } else {
              rebelMap.set(rebel.memberId, {
                name: rebel.name,
                party: rebel.party,
                constituency: rebel.constituency,
                count: 1,
                recentDivisions: [summary.title],
              });
            }
          }
        }
    
        // Filter and sort
        let results = Array.from(rebelMap.entries()).map(([, record]) => record);
    
        if (partyFilter) {
          const lower = partyFilter.toLowerCase();
          results = results.filter((r) => r.party.toLowerCase().includes(lower));
        }
    
        results = results.filter((r) => r.count >= minRebellions);
        results.sort((a, b) => b.count - a.count);
        results = results.slice(0, limit);
    
        if (results.length === 0) {
          return `No rebels found matching the specified criteria across ${summaries.length} divisions scanned.`;
        }
    
        const lines: string[] = [];
        lines.push(
          `Rebellion Leaderboard — ${house} (from ${dateFrom})${partyFilter ? ` — ${partyFilter} only` : ""}`
        );
        lines.push("");
    
        results.forEach((r, i) => {
          lines.push(
            `${i + 1}. ${r.name} (${r.party}, ${r.constituency}) — ${r.count} rebellion${r.count !== 1 ? "s" : ""}`
          );
          if (r.recentDivisions.length > 0) {
            lines.push(
              `   e.g. "${r.recentDivisions[0]}"${r.recentDivisions[1] ? `, "${r.recentDivisions[1]}"` : ""}`
            );
          }
        });
    
        lines.push("");
        lines.push(
          `Scanned ${summaries.length} divisions. Increase max_divisions for more complete results.`
        );
    
        return lines.join("\n");
      } catch (error) {
        const message =
          error instanceof Error ? error.message : "An unknown error occurred.";
        throw new Error(message);
      }
    }
  • Tool definition with inputSchema for rank_entities. Defines parameters: entity_type (enum: 'mp'), metric (enum: 'rebellions'), party, house (Commons/Lords), date_from, max_divisions (default 100, max 500), min_rebellions (default 1), and limit (default 20).
    export const rankTools = [
      {
        name: "rank_entities",
        description:
          "Rank MPs by rebellion count — how many times they voted against their party whip. Use this for any question about 'most rebellious MPs', 'rebel count', 'which Labour/Conservative MPs rebelled most', or rebellion frequency rankings. Scans all divisions in the date range internally and returns a sorted leaderboard in a single call. Filter by party='Labour' for Labour-specific results.",
        inputSchema: {
          type: "object",
          properties: {
            entity_type: {
              type: "string",
              enum: ["mp"],
              description: "The type of entity to rank.",
            },
            metric: {
              type: "string",
              enum: ["rebellions"],
              description: "The metric to rank by.",
            },
            party: {
              type: "string",
              description: "Filter to a specific party (e.g. 'Labour').",
            },
            house: {
              type: "string",
              enum: ["Commons", "Lords"],
              description: "Which house to scan. Defaults to Commons.",
            },
            date_from: {
              type: "string",
              description:
                "Start date in YYYY-MM-DD format. Defaults to 2024-07-05 (current parliament).",
            },
            max_divisions: {
              type: "number",
              description:
                "Maximum number of divisions to scan. Default 100, max 500.",
            },
            min_rebellions: {
              type: "number",
              description: "Minimum rebellions to appear in the leaderboard. Default 1.",
            },
            limit: {
              type: "number",
              description: "Maximum number of MPs to return. Default 20.",
            },
          },
          required: ["entity_type", "metric"],
        },
      },
  • src/index.ts:9-35 (registration)
    Import of rankTools and handleRankTool from rank.ts at line 9, and the routing logic at line 35 where the server dispatches the 'rank_entities' tool name to handleRankTool.
    import { rankTools, handleRankTool } from "./tools/rank.js";
    import { eventsTools, handleEventsTool } from "./tools/events.js";
    import { patternsTools, handlePatternsTool } from "./tools/patterns.js";
    import { findTools, handleFindTool } from "./tools/find.js";
    import { queryTools, handleQueryTool } from "./tools/query.js";
    
    const server = new Server(
      { name: "uk-parliament", version: "0.2.0" },
      { capabilities: { tools: {} } }
    );
    
    const allTools = [
      ...rankTools,
      ...eventsTools,
      ...patternsTools,
      ...findTools,
      ...queryTools,
    ];
    
    server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: allTools }));
    
    server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const { name, arguments: args } = request.params;
      const safeArgs = (args ?? {}) as Record<string, unknown>;
      try {
        let result: string;
        if (name === "rank_entities") result = await handleRankTool(name, safeArgs);
  • detectCommonsRebels - identifies MPs who voted against their party's majority direction in a Commons division by comparing Aye/No votes per party.
    export function detectCommonsRebels(div: CommonsDivisionDetail): Rebel[] {
      // Build per-party vote counts
      const partyAyes = new Map<string, number>();
      const partyNoes = new Map<string, number>();
    
      for (const v of div.Ayes) {
        partyAyes.set(v.Party, (partyAyes.get(v.Party) ?? 0) + 1);
      }
      for (const v of div.Noes) {
        partyNoes.set(v.Party, (partyNoes.get(v.Party) ?? 0) + 1);
      }
    
      // Determine majority direction for each party
      const partyDirection = new Map<string, "Aye" | "No">();
      const allParties = new Set([...partyAyes.keys(), ...partyNoes.keys()]);
      for (const party of allParties) {
        const ayes = partyAyes.get(party) ?? 0;
        const noes = partyNoes.get(party) ?? 0;
        // Need at least 2 voters to detect a rebel
        if (ayes + noes < 2) continue;
        partyDirection.set(party, ayes >= noes ? "Aye" : "No");
      }
    
      const rebels: Rebel[] = [];
    
      // Aye voters whose party majority voted No
      for (const v of div.Ayes) {
        const dir = partyDirection.get(v.Party);
        if (dir === "No") {
          rebels.push({
            memberId: v.MemberId,
            name: v.Name,
            party: v.Party,
            constituency: v.MemberFrom,
            votedDirection: "Aye",
            partyDirection: "No",
          });
        }
      }
    
      // No voters whose party majority voted Aye
      for (const v of div.Noes) {
        const dir = partyDirection.get(v.Party);
        if (dir === "Aye") {
          rebels.push({
            memberId: v.MemberId,
            name: v.Name,
            party: v.Party,
            constituency: v.MemberFrom,
            votedDirection: "No",
            partyDirection: "Aye",
          });
        }
      }
    
      return rebels;
    }
  • detectLordsRebels - identifies Lords members who voted against their party's majority direction in a Lords division.
    export function detectLordsRebels(div: LordsDivisionDetail): Rebel[] {
      const partyContents = new Map<string, number>();
      const partyNotContents = new Map<string, number>();
    
      for (const v of div.contents) {
        partyContents.set(v.party, (partyContents.get(v.party) ?? 0) + 1);
      }
      for (const v of div.notContents) {
        partyNotContents.set(v.party, (partyNotContents.get(v.party) ?? 0) + 1);
      }
    
      const partyDirection = new Map<string, "Content" | "NotContent">();
      const allParties = new Set([
        ...partyContents.keys(),
        ...partyNotContents.keys(),
      ]);
      for (const party of allParties) {
        const contents = partyContents.get(party) ?? 0;
        const notContents = partyNotContents.get(party) ?? 0;
        if (contents + notContents < 2) continue;
        partyDirection.set(
          party,
          contents >= notContents ? "Content" : "NotContent"
        );
      }
    
      const rebels: Rebel[] = [];
    
      for (const v of div.contents) {
        const dir = partyDirection.get(v.party);
        if (dir === "NotContent") {
          rebels.push({
            memberId: v.memberId,
            name: v.name,
            party: v.party,
            constituency: v.memberFrom,
            votedDirection: "Content",
            partyDirection: "NotContent",
          });
        }
      }
    
      for (const v of div.notContents) {
        const dir = partyDirection.get(v.party);
        if (dir === "Content") {
          rebels.push({
            memberId: v.memberId,
            name: v.name,
            party: v.party,
            constituency: v.memberFrom,
            votedDirection: "NotContent",
            partyDirection: "Content",
          });
        }
      }
    
      return rebels;
    }
Behavior4/5

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

No annotations are provided, so the description must carry the full burden. It discloses that the tool scans all divisions internally and returns a sorted leaderboard, and mentions default date range. It lacks details on potential side effects or permissions, but for a read-like ranking operation, this is adequate.

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 concise, with four sentences that front-load the core action and provide necessary details. Every sentence adds information without redundancy.

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?

Given the absence of an output schema, the description covers the tool's behavior well: internal scanning, defaults, and filtering. It lacks explicit mention of the return structure (e.g., 'returns list of MPs with rebellion counts'), but for a leaderboard tool, the context is largely sufficient.

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?

With 100% schema coverage, the baseline is 3. The description adds value by providing examples (e.g., 'filter by party='Labour'') and clarifying that metrics and entity types are fixed, which reinforces the schema's enum constraints. This lifts the score to 4.

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 ranks MPs by rebellion count, specifies the metric (rebellions), and gives concrete usage examples. It distinguishes itself from siblings by promising a sorted leaderboard in a single call, making the purpose unambiguous.

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 explicitly lists example queries ('most rebellious MPs', 'rebel count', etc.) and provides filter guidance (party='Labour'). However, it does not mention when to avoid this tool or suggest alternative tools, which would have made it a 5.

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/kupad95/uk-parliament-mcp-server'

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