234 lines
8.0 KiB
Python
Executable File
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()
|