Use this file to discover all available pages before exploring further.
MCP-grounded Taiwan (ROC) legal research over 司法院 judgments, 全國法規資料庫 regulations, and 憲法法庭 interpretations. Every answer is backed by a live tool call against an official Taiwan government source.
Create bindu_agent.py with the code below, or save it directly from your editor.
"""Lex Taiwan exposed as a Bindu A2A agent.Bridges agno's async MCP toolkit to Bindu's sync handler contract:- A background asyncio event loop runs in a daemon thread.- The MCP stdio connection is opened ONCE on that loop at module load, so the Taiwan legal MCP server starts a single time and stays warm across every A2A `message/send` request.- The sync handler that Bindu invokes hops onto the background loop with `run_coroutine_threadsafe(...)` and waits for the result.Run with: .venv/bin/python bindu_agent.py"""from __future__ import annotationsimport asyncioimport atexitimport osimport threadingfrom pathlib import Pathfrom dotenv import load_dotenvload_dotenv()from agno.tools.mcp import MCPToolsfrom agent import _mcp_server_params, agentfrom prompts import AGENT_DESCRIPTIONfrom bindu.penguin.bindufy import bindufyHERE = Path(__file__).parent.resolve()# --- Background event loop ---------------------------------------------------# Bindu's handler contract is sync, but agno + MCPTools are async-only.# We run a dedicated asyncio loop in a daemon thread and marshal each# handler call onto it. This also lets us keep ONE MCP connection alive# across requests instead of fork-restarting the stdio server every time._loop: asyncio.AbstractEventLoop = asyncio.new_event_loop()_mcp_tools: MCPTools | None = None_ready = threading.Event()def _run_loop() -> None: asyncio.set_event_loop(_loop) _loop.run_forever()_loop_thread = threading.Thread(target=_run_loop, name="lex-taiwan-loop", daemon=True)_loop_thread.start()async def _open_mcp() -> None: global _mcp_tools _mcp_tools = MCPTools(server_params=_mcp_server_params()) await _mcp_tools.connect() agent.tools = [_mcp_tools]async def _close_mcp() -> None: if _mcp_tools is not None: await _mcp_tools.close()asyncio.run_coroutine_threadsafe(_open_mcp(), _loop).result()_ready.set()def _shutdown() -> None: if not _loop.is_running(): return try: asyncio.run_coroutine_threadsafe(_close_mcp(), _loop).result(timeout=5) except Exception: pass _loop.call_soon_threadsafe(_loop.stop)atexit.register(_shutdown)# --- Bindu handler -----------------------------------------------------------async def _arun(content: str) -> str: result = await agent.arun(content) return result.content if hasattr(result, "content") else str(result)def handler(messages: list[dict[str, str]]): """Sync Bindu handler. Hops the prompt onto the background loop.""" if not messages: return ( "Ask a Taiwan legal question — judgments (司法院裁判書), " "regulations (全國法規資料庫), or constitutional court " "interpretations (憲法法庭). I cite primary sources for every answer." ) last = messages[-1] content = last.get("content", "") if isinstance(last, dict) else str(last) if not content.strip(): return "Empty message — please send a question." _ready.wait(timeout=30) future = asyncio.run_coroutine_threadsafe(_arun(content), _loop) return future.result()# --- Bindu config ------------------------------------------------------------config = { "author": os.getenv("BINDU_AGENT_AUTHOR", "bindu.builder@getbindu.com"), "name": os.getenv("BINDU_AGENT_NAME", "lex-taiwan"), "description": AGENT_DESCRIPTION, "deployment": { "url": os.getenv("BINDU_AGENT_URL", "http://localhost:3773"), "expose": True, "cors_origins": ["http://localhost:5173", "http://localhost:3775"], }, "capabilities": {"streaming": False},}if __name__ == "__main__": bindufy(config, handler)
The Bindu wrapper imports the agent and MCP launch params from agent.py. Create agent.py alongside bindu_agent.py:
"""Lex Taiwan — agno agent wired to the mcp-taiwan-legal-db MCP server."""from __future__ import annotationsimport osimport shutilimport sysfrom contextlib import asynccontextmanagerfrom pathlib import Pathfrom agno.agent import Agentfrom agno.db.sqlite import SqliteDbfrom agno.models.openrouter import OpenRouterfrom agno.os import AgentOSfrom agno.tools.mcp import MCPToolsfrom fastapi import FastAPIfrom mcp import StdioServerParametersfrom prompts import AGENT_DESCRIPTION, AGENT_NAME, SYSTEM_PROMPTHERE = Path(__file__).parent.resolve()DB_PATH = HERE / "tmp" / "lex_taiwan.db"DB_PATH.parent.mkdir(parents=True, exist_ok=True)def _mcp_server_params() -> StdioServerParameters: """Build the stdio launch command for the Taiwan legal MCP server.""" entry_point = shutil.which("mcp-taiwan-legal-db") or str( HERE / ".venv" / "bin" / "mcp-taiwan-legal-db" ) if Path(entry_point).exists(): return StdioServerParameters(command=entry_point, args=[]) return StdioServerParameters( command=sys.executable, args=["-m", "mcp_server.server"], )def _build_model(): """Pick the LLM via OpenRouter. Defaults to Claude Sonnet 4.5 — strong on tool use + Traditional Chinese. Override with BINDU_AGENT_MODEL (any OpenRouter model id). """ model_id = os.getenv("BINDU_AGENT_MODEL", "anthropic/claude-sonnet-4.5") api_key = os.getenv("OPENROUTER_API_KEY") if not api_key: raise RuntimeError( "OPENROUTER_API_KEY is not set. Add it to your .env (see .env.example)." ) return OpenRouter( id=model_id, api_key=api_key, max_tokens=int(os.getenv("BINDU_AGENT_MAX_TOKENS", "4096")), )def build_agent() -> Agent: """Create the agent shell. Tools are attached at MCP-connect time.""" return Agent( name=AGENT_NAME, description=AGENT_DESCRIPTION, instructions=SYSTEM_PROMPT, model=_build_model(), db=SqliteDb(db_file=str(DB_PATH)), update_memory_on_run=True, enable_session_summaries=True, add_history_to_context=True, num_history_runs=3, add_datetime_to_context=True, markdown=True, )# Module-level agent + MCP handle so `AgentOS` can find itagent: Agent = build_agent()mcp_tools: MCPTools | None = None@asynccontextmanagerasync def lifespan(app: FastAPI): """Manage the MCP stdio connection for the FastAPI server lifetime.""" global mcp_tools mcp_tools = MCPTools(server_params=_mcp_server_params()) await mcp_tools.connect() agent.tools = [mcp_tools] try: yield finally: await mcp_tools.close()agent_os = AgentOS( description="Lex Taiwan — Taiwan legal research agent (judgments + regulations + 憲法法庭).", agents=[agent], lifespan=lifespan,)app = agent_os.get_app()if __name__ == "__main__": agent_os.serve(app="agent:app", reload=False)
The Windsurf-style XML-tagged prompt lives in prompts.py:
"""System prompt for the Taiwan Legal Research Agent."""from textwrap import dedentAGENT_NAME = "Lex Taiwan"AGENT_DESCRIPTION = ( "An agentic Taiwan legal research assistant: judgments, regulations, " "and constitutional court interpretations, sourced live from the " "司法院, 全國法規資料庫, and 憲法法庭.")SYSTEM_PROMPT = dedent( """\ You are Lex Taiwan, a powerful agentic AI legal-research assistant for Taiwan (ROC) law. You operate on an MCP-first paradigm: every authoritative answer must be backed by a tool call against an official Taiwan government source — never your training memory. <tool_calling> 1. Only call tools when they are necessary to ground the answer in a primary source. 2. If you state that you will look something up, immediately issue the tool call as your next action. 3. Always follow each tool's parameter schema exactly. Never invent fields. 4. Never call tools that are not listed in <available_tools>. The MCP surface is fixed at 8 tools. 5. Before each tool call, write ONE short sentence explaining why. 6. Chain tools in the obvious order: search → get. 7. Prefer the cheapest precise call. A known 案號 is a precise call; a keyword sweep is not. 8. Batch independent lookups in parallel. 9. If a tool returns an error or empty result, inspect the message before retrying. 10. NEVER fabricate a JID, a pcode, a 釋字 number, or a citation. </tool_calling> <available_tools> Judgments (司法院裁判書): - `search_judgments(keyword?, case_word?, case_number?, year_from?, year_to?, court?, case_type?, main_text?)` - `get_judgment(jid? | url?)` Regulations (全國法規資料庫): - `get_pcode(law_name)` - `query_regulation(pcode? | law_name?, article_no?, mode?)` - `search_regulations(keyword)` Constitutional Court (憲法法庭): - `search_interpretations(keyword)` - `get_interpretation(id, reasoning_keyword?)` - `get_citations(id, include_context?)` </available_tools> <citation_format> - Judgment: `<court> <year>年度<案號字><案號>號 (<date>)` + JID. - Regulation article: `《<法規名稱>》第 <條> 條<項?><款?>`. - Constitutional interpretation: `司法院釋字第 <N> 號解釋` or `憲法法庭 <year> 年憲判字第 <N> 號判決`. Always end the answer with a `### Sources` section. </citation_format> """)
The shipped prompts.py is longer (full <user_information>, <legal_research_method>, <handling_uncertainty>, <communication_style> sections plus a tool-selection cheat sheet). Read it in the source repo for the exact text.
The system prompt enforces a hard constraint: every authoritative answer must be backed by a live tool call against an official Taiwan source — never the model’s training memory
General procedural questions (“what is a 釋字?”) may be answered without a tool call; anything that names a specific statute, judgment, or interpretation must hit the MCP
The 8-tool MCP surface (mcp-taiwan-legal-db) is enumerated inside <available_tools> so the model can’t drift to a fabricated tool name
MCP integration (stdio transport, kept warm)
The mcp-taiwan-legal-db server is launched as a stdio subprocess via StdioServerParameters
bindu_agent.py opens the MCP connection ONCE on module import and keeps it alive across every A2A request — no fork-per-request overhead
Bindu’s handler contract is sync, but agno + MCPTools are async-only. A daemon thread runs a dedicated asyncio loop; the sync handler hops onto it with run_coroutine_threadsafe(...) and waits for the result
atexit closes the MCP session cleanly on shutdown
Backed by 司法院 (judgments), 全國法規資料庫 (regulations), and 憲法法庭 (constitutional court) — proxied through httpx, with an optional Playwright fallback for 司法院’s F5 WAF
The MCP server mcp-taiwan-legal-db is published on PyPI — installing it provides the mcp-taiwan-legal-db entry-point binary that _mcp_server_params() launches over stdio.Optional (only needed if 司法院’s F5 WAF starts blocking):
# OpenRouter API key. Get one at https://openrouter.ai/keysOPENROUTER_API_KEY=sk-or-v1-...# Optional: override the model id. Any OpenRouter model id works.# Default: anthropic/claude-sonnet-4.5 (strong tool use + zh-TW)# BINDU_AGENT_MODEL=anthropic/claude-sonnet-4.5# Optional: max tokens per response (default 4096).# BINDU_AGENT_MAX_TOKENS=4096# Optional: display name in the Bindu agent card (default: lex-taiwan).# BINDU_AGENT_NAME=lex-taiwan
# Clone the Bindu repositorygit clone https://github.com/GetBindu/Bindu# Navigate to frontend directorycd frontend# Install dependenciesnpm install# Start frontend development servernpm run dev