Files

234 lines
8.0 KiB
Python
Executable File

#!/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()