Skip to main content
Glama
kupad95

UK Parliament MCP Server

by kupad95

query_entities

Find UK MPs by cross-referencing voting records, rebellions, party, house, and declared financial interests using a single query.

Instructions

Cross-dataset query: find MPs matching multiple conditions spanning vote records AND financial interests. Examples: 'Labour MPs who voted No on division 1234', 'MPs who rebelled in division 5678 AND have defence interests', 'MPs with fossil fuel interests who voted Aye'. Specify division_id with voted='aye'/'no' or rebellion_only=true for vote filter. Specify has_interest keyword for interest filter. Results are the intersection of all conditions.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
division_idNoDivision to filter votes by.
votedNoFilter to only Aye or No voters in the division.
rebellion_onlyNoIf true, only include MPs who rebelled in the specified division.
houseNoWhich house. Defaults to Commons.
has_interestNoFilter to MPs with a declared interest matching this keyword.
partyNoFilter to a specific party.
limitNoMaximum results. Default 50.

Implementation Reference

  • The handler function handleQueryTool that executes the query_entities tool logic. It cross-references MP vote data (from division IDs) with financial interests data to find intersection matches based on filters like division_id, voted, rebellion_only, has_interest, party, and house.
    export async function handleQueryTool(
      name: string,
      args: Record<string, unknown>
    ): Promise<string> {
      try {
        if (name !== "query_entities") {
          throw new Error(`Unknown tool: ${name}`);
        }
    
        const divisionId = args.division_id as number | undefined;
        const voted = args.voted as "aye" | "no" | undefined;
        const rebellionOnly = (args.rebellion_only as boolean) ?? false;
        const house = (args.house as "Commons" | "Lords") ?? "Commons";
        const hasInterest = args.has_interest as string | undefined;
        const partyFilter = args.party as string | undefined;
        const limit = (args.limit as number) ?? 50;
    
        if (!divisionId && !hasInterest && !partyFilter) {
          return "At least one filter required: division_id, has_interest, or party.";
        }
    
        // ── Step 1: Build vote-based MP set ──────────────────────────────────────
        let mpSet: Map<number, MPRecord> | null = null;
    
        if (divisionId !== undefined) {
          const detail = await fetchDivisionDetail(house, divisionId);
          mpSet = new Map();
    
          if (house === "Commons") {
            const d = detail as CommonsDivisionDetail;
            const rebels = detectCommonsRebels(d);
            const rebelIds = new Set(rebels.map((r) => r.memberId));
    
            const allVoters: { voter: { MemberId: number; Name: string; Party: string; MemberFrom: string }; dir: string }[] = [
              ...d.Ayes.map((v) => ({ voter: v, dir: "Aye" })),
              ...d.Noes.map((v) => ({ voter: v, dir: "No" })),
            ];
    
            for (const { voter, dir } of allVoters) {
              // Apply vote direction filter
              if (voted === "aye" && dir !== "Aye") continue;
              if (voted === "no" && dir !== "No") continue;
    
              const rebelled = rebelIds.has(voter.MemberId);
    
              // Apply rebellion filter
              if (rebellionOnly && !rebelled) continue;
    
              mpSet.set(voter.MemberId, {
                memberId: voter.MemberId,
                name: voter.Name,
                party: voter.Party,
                constituency: voter.MemberFrom,
                voteDirection: dir,
                rebelled,
                interests: [],
              });
            }
          } else {
            // Lords
            const d = detail as LordsDivisionDetail;
            const rebels = detectLordsRebels(d);
            const rebelIds = new Set(rebels.map((r) => r.memberId));
    
            const allVoters: { voter: { memberId: number; name: string; party: string; memberFrom: string }; dir: string }[] = [
              ...d.contents.map((v) => ({ voter: v, dir: "Content" })),
              ...d.notContents.map((v) => ({ voter: v, dir: "NotContent" })),
            ];
    
            for (const { voter, dir } of allVoters) {
              if (voted === "aye" && dir !== "Content") continue;
              if (voted === "no" && dir !== "NotContent") continue;
    
              const rebelled = rebelIds.has(voter.memberId);
              if (rebellionOnly && !rebelled) continue;
    
              mpSet.set(voter.memberId, {
                memberId: voter.memberId,
                name: voter.name,
                party: voter.party,
                constituency: voter.memberFrom,
                voteDirection: dir,
                rebelled,
                interests: [],
              });
            }
          }
        }
    
        // ── Step 2: Apply party filter to vote-based set ──────────────────────────
        if (partyFilter && mpSet !== null) {
          const lower = partyFilter.toLowerCase();
          for (const [id, mp] of mpSet) {
            if (!mp.party.toLowerCase().includes(lower)) {
              mpSet.delete(id);
            }
          }
        }
    
        // ── Step 3: Interest filter ───────────────────────────────────────────────
        if (hasInterest) {
          const interestLower = hasInterest.toLowerCase();
          // All 12 interest categories
          const categoryIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
    
          // Map of memberId → interest summaries
          const interestMap = new Map<number, { name: string; party: string; constituency: string; interests: string[] }>();
    
          async function fetchCategoryInterests(categoryId: number): Promise<InterestItem[]> {
            const allItems: InterestItem[] = [];
            let skip = 0;
            const pageSize = 20;
    
            while (true) {
              const data = (await parliamentFetch(`${INTERESTS_API}/Interests`, {
                CategoryId: categoryId,
                Take: pageSize,
                Skip: skip,
              })) as InterestsResponse;
    
              const items = data?.items ?? [];
              allItems.push(...items);
    
              if (items.length < pageSize || skip >= 500) break;
              skip += pageSize;
            }
    
            return allItems;
          }
    
          const categoryResults = await Promise.allSettled(
            categoryIds.map((id) => fetchCategoryInterests(id))
          );
    
          for (const result of categoryResults) {
            if (result.status !== "fulfilled") continue;
            for (const item of result.value) {
              if (
                item.parentInterestId === null &&
                item.summary?.toLowerCase().includes(interestLower)
              ) {
                const existing = interestMap.get(item.member.id);
                if (existing) {
                  existing.interests.push(item.summary);
                } else {
                  interestMap.set(item.member.id, {
                    name: item.member.name,
                    party: item.member.party,
                    constituency: item.member.constituency,
                    interests: [item.summary],
                  });
                }
              }
            }
          }
    
          if (mpSet !== null) {
            // Intersection: remove MPs not in interest map, add interests to those who are
            for (const [id, mp] of mpSet) {
              const interestData = interestMap.get(id);
              if (!interestData) {
                mpSet.delete(id);
              } else {
                mp.interests = interestData.interests;
              }
            }
          } else {
            // Build mpSet from interest map
            mpSet = new Map();
            for (const [memberId, data] of interestMap) {
              mpSet.set(memberId, {
                memberId,
                name: data.name,
                party: data.party,
                constituency: data.constituency,
                voteDirection: null,
                rebelled: false,
                interests: data.interests,
              });
            }
    
            // Enrich with member details if small enough
            if (mpSet.size <= 50) {
              const memberEntries = Array.from(mpSet.entries());
              const memberDetails = await batchedFetch(
                memberEntries,
                ([memberId]) =>
                  parliamentFetch(`${MEMBERS_API}/Members/${memberId}`) as Promise<MemberResponse>
              );
              for (let i = 0; i < memberEntries.length; i++) {
                const memberData = memberDetails[i];
                if (!memberData?.value) continue;
                const [, mp] = memberEntries[i];
                const v = memberData.value;
                mp.name = v.nameDisplayAs ?? mp.name;
                mp.party = v.latestParty?.name ?? mp.party;
                mp.constituency = v.latestHouseMembership?.membershipFrom ?? mp.constituency;
              }
            }
          }
        }
    
        // ── Step 4: Apply party filter to interest-sourced set ───────────────────
        if (partyFilter && mpSet !== null) {
          const lower = partyFilter.toLowerCase();
          for (const [id, mp] of mpSet) {
            if (!mp.party.toLowerCase().includes(lower)) {
              mpSet.delete(id);
            }
          }
        }
    
        // ── Step 5: Format output ─────────────────────────────────────────────────
        if (!mpSet || mpSet.size === 0) {
          return "No MPs found matching all the specified conditions.";
        }
    
        const results = Array.from(mpSet.values()).slice(0, limit);
    
        const conditions: string[] = [];
        if (divisionId !== undefined) {
          if (rebellionOnly) conditions.push(`rebelled in division ${divisionId}`);
          else if (voted) conditions.push(`voted ${voted.toUpperCase()} in division ${divisionId}`);
          else conditions.push(`voted in division ${divisionId}`);
        }
        if (hasInterest) conditions.push(`has interest: "${hasInterest}"`);
        if (partyFilter) conditions.push(`party: ${partyFilter}`);
    
        const lines: string[] = [];
        lines.push(`MPs matching: ${conditions.join(" AND ")}`);
        lines.push(`Found: ${mpSet.size}${mpSet.size > limit ? ` (showing first ${limit})` : ""}`);
        lines.push("");
    
        for (const mp of results) {
          let row = `• ${mp.name} (${mp.party}, ${mp.constituency}) | ID: ${mp.memberId}`;
          if (mp.voteDirection) {
            row += ` | Voted: ${mp.voteDirection}`;
            if (mp.rebelled) row += " [REBEL]";
          }
          lines.push(row);
    
          for (const interest of mp.interests.slice(0, 2)) {
            lines.push(`  Interest: ${interest}`);
          }
          if (mp.interests.length > 2) {
            lines.push(`  ... and ${mp.interests.length - 2} more interests`);
          }
        }
    
        return lines.join("\n");
      } catch (error) {
        const message =
          error instanceof Error ? error.message : "An unknown error occurred.";
        throw new Error(message);
      }
    }
  • Tool definition and input schema for query_entities. Defines the name, description, and inputSchema with properties: division_id, voted (enum aye/no), rebellion_only, house (Commons/Lords), has_interest, party, and limit.
    // ─── Tool Definition ──────────────────────────────────────────────────────────
    
    export const queryTools = [
      {
        name: "query_entities",
        description:
          "Cross-dataset query: find MPs matching multiple conditions spanning vote records AND financial interests. Examples: 'Labour MPs who voted No on division 1234', 'MPs who rebelled in division 5678 AND have defence interests', 'MPs with fossil fuel interests who voted Aye'. Specify division_id with voted='aye'/'no' or rebellion_only=true for vote filter. Specify has_interest keyword for interest filter. Results are the intersection of all conditions.",
        inputSchema: {
          type: "object",
          properties: {
            division_id: {
              type: "number",
              description: "Division to filter votes by.",
            },
            voted: {
              type: "string",
              enum: ["aye", "no"],
              description: "Filter to only Aye or No voters in the division.",
            },
            rebellion_only: {
              type: "boolean",
              description:
                "If true, only include MPs who rebelled in the specified division.",
            },
            house: {
              type: "string",
              enum: ["Commons", "Lords"],
              description: "Which house. Defaults to Commons.",
            },
            has_interest: {
              type: "string",
              description:
                "Filter to MPs with a declared interest matching this keyword.",
            },
            party: {
              type: "string",
              description: "Filter to a specific party.",
            },
            limit: {
              type: "number",
              description: "Maximum results. Default 50.",
            },
          },
          required: [],
        },
      },
    ];
  • src/index.ts:30-50 (registration)
    Registration of query_entities in the MCP server's CallToolRequestSchema handler. The tool name 'query_entities' is routed to handleQueryTool in the if/else chain.
    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);
        else if (name === "get_events") result = await handleEventsTool(name, safeArgs);
        else if (name === "analyze_patterns") result = await handlePatternsTool(name, safeArgs);
        else if (name === "find_entities") result = await handleFindTool(name, safeArgs);
        else if (name === "query_entities") result = await handleQueryTool(name, safeArgs);
        else throw new Error(`Unknown tool: ${name}`);
        return { content: [{ type: "text", text: result }] };
      } catch (error) {
        const message =
          error instanceof Error ? error.message : "An unknown error occurred.";
        return {
          content: [{ type: "text", text: `Error: ${message}` }],
          isError: true,
        };
      }
    });
  • src/index.ts:20-26 (registration)
    Registration of queryTools array (containing the query_entities tool definition) into the allTools array that's exposed via ListToolsRequestSchema.
    const allTools = [
      ...rankTools,
      ...eventsTools,
      ...patternsTools,
      ...findTools,
      ...queryTools,
    ];
  • fetchDivisionDetail helper used by the query_entities handler to fetch division vote details for both Commons and Lords.
    export async function fetchDivisionDetail(
      house: "Commons" | "Lords",
      divisionId: number
    ): Promise<CommonsDivisionDetail | LordsDivisionDetail> {
      if (house === "Commons") {
        return (await parliamentFetch(
          `${COMMONS_VOTES_API}/division/${divisionId}.json`
        )) as CommonsDivisionDetail;
      } else {
        return (await parliamentFetch(
          `${LORDS_VOTES_API}/Divisions/${divisionId}`
        )) as LordsDivisionDetail;
      }
    }
Behavior3/5

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

With no annotations, the description carries the full burden. It explains the filtering logic (intersection) and provides examples. However, it does not disclose pagination behavior, default limits (though schema notes limit), error responses, or any side effects. The description is adequate but not exhaustive.

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 a clear front-loaded statement of purpose followed by examples and usage details. Every sentence adds value, and there is no redundancy or unnecessary text.

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 tool complexity (7 parameters, no required, cross-dataset) and absence of output schema, the description covers the main functionality well. It explains the intersection logic and key filters. However, it omits mention of parameters like house, party, and limit, which are in the schema but not reinforced in the description. Additionally, no return format is specified, which could be helpful.

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%, but the description adds value by explaining how to combine parameters (e.g., division_id with voted or rebellion_only, and has_interest for interest filter). It also provides examples that illustrate parameter usage, exceeding the schema documentation.

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's purpose as a cross-dataset query to find MPs matching multiple conditions across vote records and financial interests. It provides concrete examples and distinguishes itself from sibling tools like find_entities and analyze_patterns by emphasizing the intersection of conditions.

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 explicit guidance on how to use the tool, including specifying division_id with voted or rebellion_only for vote filtering, and has_interest for interest filtering. It also explains that results are the intersection of all conditions. However, it does not explicitly contrast with sibling tools or specify when not to use this tool.

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