#!/usr/bin/env python3 # /// script # dependencies = [ # "beautifulsoup4>=4.12.0", # "requests>=2.31.0", # ] # /// """ Regensburg Stadtrat (ALLRIS) MCP Server Exposes municipal council documents, upcoming meetings, agenda items (TOPs), and binding resolutions as Model Context Protocol (MCP) tools over stdio. """ import os import sys import json import traceback # Ensure current directory is on sys.path to import AllrisClient sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from regensburg_ris import AllrisClient TOOLS = [ { "name": "stadtrat_search", "description": "Search Regensburg city council documents, resolutions (Beschlüsse), and motions (Vorlagen) by keyword (e.g. 'Kaufhof', 'Protzenweiherbrücke', 'Stadtbahn', 'Haushalt').", "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "description": "Keyword to search in ALLRIS net (e.g. 'Brücke', 'Kaufhof', 'Verkehr')." }, "limit": { "type": "integer", "description": "Maximum number of results to return (default: 10).", "default": 10 } }, "required": ["query"] } }, { "name": "stadtrat_calendar", "description": "Fetch upcoming municipal council and committee meetings (Stadtrat, Ausschüsse) in Regensburg.", "inputSchema": { "type": "object", "properties": { "days": { "type": "integer", "description": "Number of days in the future to search (default: 30).", "default": 30 }, "from_date": { "type": "string", "description": "Start date in DD.MM.YYYY format (default: today)." }, "to_date": { "type": "string", "description": "End date in DD.MM.YYYY format." } } } }, { "name": "stadtrat_session", "description": "Fetch detailed meeting agenda items (Tagesordnungspunkte / TOPs) and linked Vorlagen IDs for a meeting ID (SILFDNR).", "inputSchema": { "type": "object", "properties": { "session_id": { "type": "string", "description": "ALLRIS session ID (SILFDNR, e.g. '2665')." } }, "required": ["session_id"] } }, { "name": "stadtrat_vorlage", "description": "Inspect a specific council proposal (Vorlage) by VOLFDNR ID: extracts voting outcomes, responsible department, and verbatim resolution text.", "inputSchema": { "type": "object", "properties": { "vorlage_id": { "type": "string", "description": "ALLRIS proposal ID (VOLFDNR, e.g. '21362')." }, "convert_attachments": { "type": "boolean", "description": "Whether to convert linked PDF attachments to Markdown (default: false).", "default": False } }, "required": ["vorlage_id"] } }, { "name": "stadtrat_compile_topic", "description": "Compile a multi-document synthesis of a topic across ALLRIS into the Obsidian knowledge-wiki.", "inputSchema": { "type": "object", "properties": { "topic": { "type": "string", "description": "Topic keyword (e.g. 'Protzenweiherbrücke', 'Kaufhof')." }, "vault": { "type": "string", "description": "Path to the Obsidian knowledge-wiki vault.", "default": "/home/node/.openclaw/wiki/main" } }, "required": ["topic"] } } ] def run_mcp_server(): client = AllrisClient() for line in sys.stdin: line = line.strip() if not line: continue try: req = json.loads(line) except json.JSONDecodeError: continue method = req.get("method") msg_id = req.get("id") params = req.get("params", {}) if method == "initialize": resp = { "jsonrpc": "2.0", "id": msg_id, "result": { "protocolVersion": "2024-11-05", "serverInfo": { "name": "regensburg-stadtrat-mcp", "version": "1.0.0" }, "capabilities": { "tools": {} } } } sys.stdout.write(json.dumps(resp) + "\n") sys.stdout.flush() elif method == "notifications/initialized": pass elif method == "tools/list": resp = { "jsonrpc": "2.0", "id": msg_id, "result": { "tools": TOOLS } } sys.stdout.write(json.dumps(resp) + "\n") sys.stdout.flush() elif method == "tools/call": tool_name = params.get("name") args = params.get("arguments", {}) try: if tool_name == "stadtrat_search": data = client.search( query=args.get("query", ""), limit=int(args.get("limit", 10)) ) elif tool_name == "stadtrat_calendar": data = client.get_calendar( days=int(args.get("days", 30)), from_date=args.get("from_date"), to_date=args.get("to_date") ) elif tool_name == "stadtrat_session": data = client.get_session( silfdnr=str(args.get("session_id", "")) ) elif tool_name == "stadtrat_vorlage": data = client.get_vorlage( volfdnr=str(args.get("vorlage_id", "")), convert_attachments=bool(args.get("convert_attachments", False)) ) elif tool_name == "stadtrat_compile_topic": vault_path = args.get("vault", "/home/node/.openclaw/wiki/main") data = client.compile_topic( topic=args.get("topic", ""), vault=vault_path ) else: raise ValueError(f"Unknown tool: {tool_name}") text_result = json.dumps(data, indent=2, ensure_ascii=False) resp = { "jsonrpc": "2.0", "id": msg_id, "result": { "content": [{"type": "text", "text": text_result}] } } except Exception as e: resp = { "jsonrpc": "2.0", "id": msg_id, "result": { "isError": True, "content": [{"type": "text", "text": f"Error executing {tool_name}: {str(e)}\n{traceback.format_exc()}"}] } } sys.stdout.write(json.dumps(resp) + "\n") sys.stdout.flush() else: if msg_id is not None: resp = { "jsonrpc": "2.0", "id": msg_id, "error": {"code": -32601, "message": f"Method not found: {method}"} } sys.stdout.write(json.dumps(resp) + "\n") sys.stdout.flush() if __name__ == "__main__": run_mcp_server()