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
| Name | Required | Description | Default |
|---|---|---|---|
| entity_type | Yes | The type of entity to rank. | |
| metric | Yes | The metric to rank by. | |
| party | No | Filter to a specific party (e.g. 'Labour'). | |
| house | No | Which house to scan. Defaults to Commons. | |
| date_from | No | Start date in YYYY-MM-DD format. Defaults to 2024-07-05 (current parliament). | |
| max_divisions | No | Maximum number of divisions to scan. Default 100, max 500. | |
| min_rebellions | No | Minimum rebellions to appear in the leaderboard. Default 1. | |
| limit | No | Maximum number of MPs to return. Default 20. |
Implementation Reference
- src/tools/rank.ts:78-190 (handler)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); } } - src/tools/rank.ts:17-65 (schema)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); - src/tools/shared.ts:94-150 (helper)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; } - src/tools/shared.ts:152-209 (helper)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; }