Skip to main content
Glama

FedMCP - Federal Parliamentary Information

QUICK_WINS_IMPLEMENTATION.md17.8 kB
# Quick Wins Implementation Guide: House of Commons Open Data ## Executive Summary These quick wins can be implemented in 1-2 days and will add immediate value to CanadaGPT/FedMCP by filling critical data gaps in government structure and accountability tracking. ## Priority 1: Cabinet Ministers & Government Roles (2 hours) ### New Client Implementation Create `/Users/matthewdufresne/FedMCP/src/fedmcp/clients/government_roles.py`: ```python """Client for fetching government roles from House of Commons Open Data.""" import logging from typing import List, Dict, Optional from xml.etree import ElementTree as ET from dataclasses import dataclass from datetime import datetime from .http import RateLimitedSession logger = logging.getLogger(__name__) @dataclass class CabinetMinister: """Cabinet minister with portfolio.""" name: str title: str department: Optional[str] precedence: int start_date: Optional[datetime] mp_url: Optional[str] @dataclass class ParliamentarySecretary: """Parliamentary secretary appointment.""" name: str title: str minister_supported: str constituency: str start_date: Optional[datetime] @dataclass class PartyStanding: """Party seat distribution by province.""" party: str total_seats: int provincial_seats: Dict[str, int] official_status: Optional[str] # "Government", "Official Opposition", etc. class GovernmentRolesClient: """Client for House of Commons government structure data.""" BASE_URL = "https://www.ourcommons.ca" def __init__(self, session: Optional[RateLimitedSession] = None): """Initialize with optional shared session.""" self.session = session or RateLimitedSession(min_request_interval=0) def get_cabinet_ministers(self) -> List[CabinetMinister]: """Fetch current Cabinet ministers with portfolios. Returns: List of CabinetMinister objects ordered by precedence """ url = f"{self.BASE_URL}/Members/en/ministries/XML" logger.info("Fetching Cabinet ministers from %s", url) response = self.session.get(url) root = ET.fromstring(response.content) ministers = [] for minister_elem in root.findall('.//Minister'): try: ministers.append(CabinetMinister( name=minister_elem.findtext('PersonName', ''), title=minister_elem.findtext('Title', ''), department=minister_elem.findtext('Department'), precedence=int(minister_elem.findtext('Precedence', '0')), start_date=self._parse_date(minister_elem.findtext('FromDate')), mp_url=minister_elem.findtext('PersonWebSiteUrl') )) except Exception as e: logger.warning(f"Error parsing minister: {e}") continue # Sort by precedence (Prime Minister = 1) ministers.sort(key=lambda m: m.precedence) logger.info(f"Retrieved {len(ministers)} Cabinet ministers") return ministers def get_parliamentary_secretaries(self) -> List[ParliamentarySecretary]: """Fetch current Parliamentary Secretaries. Returns: List of ParliamentarySecretary appointments """ url = f"{self.BASE_URL}/Members/en/parliamentary-secretaries/XML" logger.info("Fetching Parliamentary Secretaries from %s", url) response = self.session.get(url) root = ET.fromstring(response.content) secretaries = [] for ps_elem in root.findall('.//ParliamentarySecretary'): try: secretaries.append(ParliamentarySecretary( name=ps_elem.findtext('PersonName', ''), title=ps_elem.findtext('Title', ''), minister_supported=ps_elem.findtext('MinisterSupported', ''), constituency=ps_elem.findtext('ConstituencyName', ''), start_date=self._parse_date(ps_elem.findtext('FromDate')) )) except Exception as e: logger.warning(f"Error parsing parliamentary secretary: {e}") continue logger.info(f"Retrieved {len(secretaries)} Parliamentary Secretaries") return secretaries def get_party_standings(self) -> List[PartyStanding]: """Fetch current party standings by province. Returns: List of PartyStanding objects with provincial seat distribution """ url = f"{self.BASE_URL}/Members/en/party-standings/XML" logger.info("Fetching party standings from %s", url) response = self.session.get(url) root = ET.fromstring(response.content) standings = [] for party_elem in root.findall('.//Party'): party_name = party_elem.findtext('Name', '') total = int(party_elem.findtext('TotalSeats', '0')) # Get provincial breakdown provincial = {} for prov_elem in party_elem.findall('.//Province'): prov_name = prov_elem.findtext('Name', '') seats = int(prov_elem.findtext('Seats', '0')) if seats > 0: provincial[prov_name] = seats # Determine official status based on seat count status = None if total >= 170: # Majority threshold status = "Government (Majority)" elif total >= 120: # Largest party but minority status = "Government (Minority)" elif total >= 40: # Substantial representation status = "Official Opposition" if len(standings) == 1 else "Third Party" standings.append(PartyStanding( party=party_name, total_seats=total, provincial_seats=provincial, official_status=status )) # Sort by seat count standings.sort(key=lambda p: p.total_seats, reverse=True) # Correct status for Official Opposition if len(standings) > 1 and standings[1].total_seats >= 12: standings[1].official_status = "Official Opposition" logger.info(f"Retrieved standings for {len(standings)} parties") return standings def get_speaker_and_officers(self) -> List[Dict]: """Fetch current Speaker and presiding officers. Returns: List of chair occupants with roles and dates """ url = f"{self.BASE_URL}/Members/en/chair-occupants/XML" logger.info("Fetching Speaker and officers from %s", url) response = self.session.get(url) root = ET.fromstring(response.content) officers = [] for officer_elem in root.findall('.//ChairOccupant'): officers.append({ 'name': officer_elem.findtext('PersonName', ''), 'role': officer_elem.findtext('ChairTitle', ''), 'start_date': self._parse_date(officer_elem.findtext('FromDate')), 'constituency': officer_elem.findtext('ConstituencyName', '') }) logger.info(f"Retrieved {len(officers)} presiding officers") return officers @staticmethod def _parse_date(date_str: Optional[str]) -> Optional[datetime]: """Parse date string to datetime object.""" if not date_str: return None try: # Try multiple date formats for fmt in ['%Y-%m-%d', '%Y-%m-%dT%H:%M:%S', '%Y/%m/%d']: try: return datetime.strptime(date_str, fmt) except ValueError: continue return None except Exception: return None # Example usage for Neo4j ingestion def ingest_government_structure(): """Example function to ingest government structure into Neo4j.""" from neo4j import GraphDatabase client = GovernmentRolesClient() # Fetch all data ministers = client.get_cabinet_ministers() secretaries = client.get_parliamentary_secretaries() standings = client.get_party_standings() officers = client.get_speaker_and_officers() # Example Neo4j queries with GraphDatabase.driver("bolt://localhost:7687") as driver: with driver.session() as session: # Create Cabinet Role nodes for minister in ministers: session.run(""" MERGE (r:Role {id: $role_id}) SET r.title = $title, r.type = 'Cabinet', r.department = $department, r.precedence = $precedence, r.active = true, r.start_date = $start_date WITH r MATCH (mp:MP {name: $mp_name}) MERGE (mp)-[:HOLDS_ROLE {current: true}]->(r) """, role_id=f"cabinet-{minister.title.lower().replace(' ', '-')}", title=minister.title, department=minister.department, precedence=minister.precedence, start_date=minister.start_date, mp_name=minister.name) # Update Party nodes with standings for standing in standings: session.run(""" MATCH (p:Party {name: $party_name}) SET p.total_seats = $total, p.official_status = $status, p.provincial_seats = $provincial, p.last_standing_update = datetime() """, party_name=standing.party, total=standing.total_seats, status=standing.official_status, provincial=standing.provincial_seats) print(f"✅ Ingested {len(ministers)} Cabinet ministers") print(f"✅ Ingested {len(secretaries)} Parliamentary Secretaries") print(f"✅ Updated {len(standings)} party standings") print(f"✅ Ingested {len(officers)} presiding officers") ``` ### MCP Server Integration Add to `/Users/matthewdufresne/FedMCP/src/fedmcp/server.py`: ```python # Add to imports from .clients.government_roles import GovernmentRolesClient # Initialize client gov_roles_client = GovernmentRolesClient() # Add new tools to list_tools(): { "name": "get_cabinet_ministers", "description": "Get current Cabinet ministers with portfolios and precedence order", "inputSchema": { "type": "object", "properties": {}, "required": [] } }, { "name": "get_parliamentary_secretaries", "description": "Get current Parliamentary Secretaries and their assigned ministers", "inputSchema": { "type": "object", "properties": {}, "required": [] } }, { "name": "get_party_standings", "description": "Get current party seat counts by province with official status", "inputSchema": { "type": "object", "properties": {}, "required": [] } }, # Add handlers in call_tool(): elif name == "get_cabinet_ministers": ministers = await run_sync(gov_roles_client.get_cabinet_ministers) result = "## Current Cabinet Ministers (by precedence)\n\n" for minister in ministers: result += f"**{minister.precedence}. {minister.name}**\n" result += f" - {minister.title}\n" if minister.department: result += f" - Department: {minister.department}\n" result += "\n" return [TextContent(type="text", text=result)] elif name == "get_parliamentary_secretaries": secretaries = await run_sync(gov_roles_client.get_parliamentary_secretaries) result = "## Current Parliamentary Secretaries\n\n" for ps in secretaries: result += f"**{ps.name}** ({ps.constituency})\n" result += f" - {ps.title}\n" result += f" - Supporting: {ps.minister_supported}\n\n" return [TextContent(type="text", text=result)] elif name == "get_party_standings": standings = await run_sync(gov_roles_client.get_party_standings) result = "## Current Party Standings in House of Commons\n\n" for party in standings: result += f"**{party.party}**: {party.total_seats} seats" if party.official_status: result += f" ({party.official_status})" result += "\n" # Show top 3 provinces top_provinces = sorted(party.provincial_seats.items(), key=lambda x: x[1], reverse=True)[:3] for prov, seats in top_provinces: result += f" - {prov}: {seats}\n" result += "\n" return [TextContent(type="text", text=result)] ``` ## Priority 2: Enhanced Constituencies Data (1 hour) ### Add to government_roles.py: ```python @dataclass class Constituency: """Electoral district information.""" id: str name: str province: str mp_name: Optional[str] mp_party: Optional[str] population: Optional[int] electors: Optional[int] def get_constituencies(self) -> List[Constituency]: """Fetch all constituency data with current MPs. Returns: List of Constituency objects """ url = f"{self.BASE_URL}/Members/en/constituencies/XML" logger.info("Fetching constituencies from %s", url) response = self.session.get(url) root = ET.fromstring(response.content) constituencies = [] for const_elem in root.findall('.//Constituency'): constituencies.append(Constituency( id=const_elem.findtext('ConstituencyId', ''), name=const_elem.findtext('ConstituencyName', ''), province=const_elem.findtext('ProvinceName', ''), mp_name=const_elem.findtext('CurrentMemberName'), mp_party=const_elem.findtext('CaucusName'), population=self._parse_int(const_elem.findtext('Population')), electors=self._parse_int(const_elem.findtext('Electors')) )) logger.info(f"Retrieved {len(constituencies)} constituencies") return constituencies ``` ## Testing & Validation ### Quick Test Script Create `/Users/matthewdufresne/FedMCP/test_gov_roles.py`: ```python #!/usr/bin/env python3 """Test government roles client.""" from src.fedmcp.clients.government_roles import GovernmentRolesClient def main(): client = GovernmentRolesClient() print("=" * 60) print("TESTING GOVERNMENT ROLES CLIENT") print("=" * 60) # Test Cabinet print("\n1. Cabinet Ministers:") ministers = client.get_cabinet_ministers() print(f" Found {len(ministers)} ministers") if ministers: print(f" Prime Minister: {ministers[0].name}") print(f" Sample: {ministers[1].title} - {ministers[1].name}") # Test Parliamentary Secretaries print("\n2. Parliamentary Secretaries:") secretaries = client.get_parliamentary_secretaries() print(f" Found {len(secretaries)} parliamentary secretaries") if secretaries: print(f" Sample: {secretaries[0].name} supporting {secretaries[0].minister_supported}") # Test Party Standings print("\n3. Party Standings:") standings = client.get_party_standings() for party in standings[:3]: print(f" {party.party}: {party.total_seats} seats ({party.official_status})") # Test Speaker print("\n4. Speaker and Officers:") officers = client.get_speaker_and_officers() print(f" Found {len(officers)} presiding officers") if officers: speaker = next((o for o in officers if 'Speaker' in o['role']), None) if speaker: print(f" Speaker: {speaker['name']}") print("\n✅ All tests passed!") if __name__ == "__main__": main() ``` ### Run Tests: ```bash cd /Users/matthewdufresne/FedMCP python test_gov_roles.py ``` ## Expected Impact ### Immediate Benefits (Day 1) 1. **Cabinet Tracking**: Know which MPs hold ministerial positions 2. **Party Power Map**: Real-time seat distribution by province 3. **Government Structure**: Complete hierarchy from PM to Parliamentary Secretaries 4. **Enhanced Accountability**: Link ministerial decisions to department activities ### Query Examples Available Immediately ```cypher // Which ministers voted on their own department's legislation? MATCH (mp:MP)-[:HOLDS_ROLE]->(r:Role {type: 'Cabinet'}) MATCH (mp)-[:VOTED]->(v:Vote)-[:SUBJECT_OF]->(b:Bill) WHERE b.title CONTAINS r.department RETURN mp.name, r.title, b.number, b.title, v.position // Parliamentary Secretary influence network MATCH (ps:MP)-[:HOLDS_ROLE]->(r1:Role {type: 'Parliamentary Secretary'}) MATCH (minister:MP)-[:HOLDS_ROLE]->(r2:Role {type: 'Cabinet'}) WHERE r1.minister_supported = minister.name MATCH (ps)-[:VOTED]->(v:Vote)<-[:VOTED]-(minister) RETURN ps.name, minister.name, count(v) as aligned_votes ORDER BY aligned_votes DESC ``` ## Next Steps 1. **Deploy & Test** (30 minutes) - Add government_roles.py to codebase - Update server.py with new tools - Test with Claude Desktop 2. **Schedule Updates** (30 minutes) - Set up daily cron to refresh Cabinet data - Weekly refresh for party standings - Monitor for Cabinet shuffles 3. **Extend to Neo4j Pipeline** (2 hours) - Add Role node creation to data pipeline - Create HOLDS_ROLE relationships - Update Party nodes with standings ## Success Metrics - ✅ 40+ Cabinet positions tracked - ✅ 35+ Parliamentary Secretary appointments - ✅ Real-time party standings by province - ✅ 100% MP role coverage for current Parliament - ✅ Zero API rate limiting (using XML feeds) This implementation provides maximum value with minimal effort, using authoritative government sources that require no API keys or authentication.

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/northernvariables/FedMCP'

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