Build a legislation Q&A agent
This guide answers a plain-language question about a title in three calls: find the title, find the provisions about the question, fetch the text. The answer carries everything a reader needs to rely on it: the compilation, the status as confirmed with the Federal Register, the verification label and a citation they can follow.
Before you begin
- Python 3.10 or later and the official SDK:
pip install mcp. - A partner API key in
LAWATLAS_API_KEY. See Get a credential.
Steps
- Treat errors as answers. Every tool returns JSON. Check for
errorbefore reading anything else, and branch onerror.code. A lapsed subscription returns plain text with a renewal link, so handle that too. - Find the title with
resolve_reference, and stop if it is not in force: Advisor will not serve its text, and neither should your agent. - Find the provisions with
search_provisions, scoped to the title and to sections. - Fetch the text with
get_provision_text, and keep itsstatus,anchorandlinkwith it. - Show the status with the text. Say when a newer compilation exists (
stateother thancurrent).
import asyncio
import json
import os
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
URL = os.environ.get("LAWATLAS_MCP_URL", "https://advisor.lawatlas.ai/api/mcp-http/mcp")
class AdvisorError(Exception):
def __init__(self, error):
super().__init__(f"{error['code']}: {error['message']}")
self.code = error["code"]
self.error = error
async def call(session, tool, arguments):
"""Call one tool and return its JSON, raising on an error envelope."""
result = await session.call_tool(tool, arguments)
text = result.content[0].text
try:
body = json.loads(text)
except ValueError:
raise RuntimeError(text) # plain text: a lapsed subscription's renewal message
if isinstance(body, dict) and "error" in body:
raise AdvisorError(body["error"])
return body
async def answer(title, question):
headers = {"Authorization": f"Bearer {os.environ['LAWATLAS_API_KEY']}"}
async with streamablehttp_client(URL, headers=headers) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
# 1. Which title? The best candidate; show close alternatives to your user.
found = await call(session, "resolve_reference", {"text": title})
best = found["candidates"][0]
if not best["in_force"]["in_force"]:
return f"{best['title']} is not in force. See {best['in_force']['frl_link']}"
# 2. Which provisions are about the question?
hits = await call(session, "search_provisions", {
"query": question, "legislation_id": best["id"],
"provision_types": ["section"], "limit": 3})
if not hits["result"]:
return f"No provision of {best['title']} matched. Try other words."
top = hits["result"][0]
# 3. The text, with its status and verification.
prov = await call(session, "get_provision_text", {
"legislation_id": best["id"], "provision_id": top["provision_id"]})
status, comp = prov["status"], prov["status"]["compilation"]
lines = [
f"{best['title']}, {prov['pinpoint']} {prov['heading']}",
prov["text"],
"",
f"Compilation {comp['compilation_no']} ({comp['register_id']}), state: {comp['state']}.",
f"In force, as confirmed with the Federal Register at {status['confirmed_with_frl_at']}.",
f"Text check: {prov['anchor']['label']}.",
f"Cite: https://advisor.lawatlas.ai{prov['link']['url']}" if prov.get("link") else
f"Source: {status['frl_link']}",
]
if comp["state"] != "current":
lines.append("A newer compilation is registered; check the Federal Register before relying on this.")
return "\n".join(lines)
if __name__ == "__main__":
print(asyncio.run(answer("privacy act", "notifying individuals of an eligible data breach")))
Check it worked
Run python qa_agent.py. Run on 10 October 2026, it printed section 26WL of the Privacy Act 1988 ("Entity must notify eligible data breach") with these closing lines:
Compilation 104 (C2026C00227), state: current.
In force, as confirmed with the Federal Register at 2026-10-09T16:37:46.550767Z.
Text check: Verified.
Cite: https://advisor.lawatlas.ai/l/C2004A03712/compilation_104/main/sec_26wl
Letting a model choose the tools
To let a language model decide which tools to call, connect Advisor to an MCP client (Connect a client): the model then calls these same tools itself. Two rules keep its answers sound.
- The law comes from the tool results, never from the model's memory. Instruct the model to quote only text a tool returned, and to say so when nothing was returned.
- The status travels with the text. Instruct the model to state
confirmed_with_frl_at, the compilation and the citation with every quotation, and to treatTITLE_NOT_IN_FORCEas an answer, not a gap to fill.
What to do if it did not
| Problem | Solution |
|---|---|
AdvisorError: INSUFFICIENT_SCOPE | Give the key's client the legislation:read scope |
| No provision matched | Use the words the law uses, or scope with provision_types removed |
| HTTP 401 | Check LAWATLAS_API_KEY; see Troubleshooting |