la-legislative
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@la-legislativeShow me the votes for council file 24-0764"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
legi-mcp
A read-only Model Context Protocol server (server name:
la-legislative) that exposes Los Angeles City legislative records — Council Files, votes, council-member activity, and Neighborhood Council engagement — to an MCP client (Claude Code / Claude Desktop) as a small set of fixed, typed, parameterized query tools.
You point Claude at your local database of LA City legislative data, and it can answer questions like "how did each council member vote on Council File 24-0764?" or "what housing files touched Council District 5 last year?" without ever writing SQL itself.
Why this design (Tier 1, not Tier 0)
This server does not let the model write raw SQL. That is a deliberate choice.
The underlying schema needs 5+ table joins to answer even basic questions (a single Council File's "dossier" pulls from projects, file_activities, file_activities_url, online_documents, project_movers, votes, council_members, and project_address). Letting an LLM improvise joins across that surface is exactly where you get silent wrong-join errors — answers that look right and are quietly wrong.
So this is a Tier 1 server: every capability is a hand-written, parameterized query behind a typed tool. The model chooses which tool and supplies typed parameters; it never composes the query. There is no raw-SQL tool.
Scope is intentionally narrow: personal, single-user, local only. No public access, no auth, no write operations, no hosting. It talks to a local PostgreSQL database (citywise_full) over stdio — there is no remote URL or HTTP endpoint. The MCP client launches it as a subprocess.
Related MCP server: sql-explorer-mcp
Project structure
legi-mcp/
├── .env # SQL_CONNECTION (gitignored — never commit)
├── .gitignore
├── requirements.txt
├── server.py # FastMCP instance + tool registration
├── db.py # connection pool + run_query helper
├── test_tools.py # standalone verification harness (not an MCP tool)
└── tools/
├── get_project.py
├── search_projects.py
├── get_votes.py
├── get_member_activity.py
└── get_nc_engagement.pyThe tools
The server exposes five tools. All are read-only and take typed parameters.
get_project(project_id)
The full "dossier" for one Council File. Bundles the base record (including the aggregate vote tally vote_given), the activity timeline, attached documents, online documents, movers/seconders, individual per-member votes, and location/topic metadata (address, lat/long, topics, places).
project_id— the Council File number, e.g."24-0764".
Returns a dict keyed project, timeline, documents, online_documents, movers, votes, location. Returns {"error": ...} if the file is not found.
search_projects(keyword, council_district, status, date_from, date_to, topic, address, limit=25)
Advanced search for when you don't have a Council File number. All filters are optional and combined with AND. Returns lightweight rows.
keyword— matched (case-insensitive) against project name OR about text.council_district— exact council district integer.status— exact status value, e.g."passed","planned".date_from/date_to— inclusive ISO-date range onstart_date.topic— matched against topic metadata.address— matched against address metadata.limit— default 25, hard-capped server-side at 50.
Returns a list of {id, name, status, council_district, start_date, about}.
get_votes(project_id)
Just the per-member roll-call vote for one file.
project_id— the Council File number, e.g."24-0764".
Returns a list of {member_name, district, vote}.
get_member_activity(council_member, date_from, date_to)
What a council member moved/seconded and how they voted. The free-text name is resolved via case-insensitive match against name / first name / last name. If it matches 0 members it returns an error; if it matches 2 or more it returns a disambiguation list of candidates instead of guessing.
council_member— full or partial member name.date_from/date_to— optional inclusive ISO-date range on projectstart_date.
Returns {member, moved_or_seconded, votes} on a unique match, or a disambiguation/error dict otherwise.
get_nc_engagement(council_district, nc_name)
Neighborhood Council engagement (Community Impact Statement counts, date ranges) by district or by NC name. At least one parameter is required.
council_district— matches NCs whose territory overlaps the district; results carryoverlap_fractionandis_primary.nc_name— matched against NC name / normalized name;overlap_fractionandis_primaryare null in this mode.
Returns a list of {nc_name, council_member_name, cis_count, first_date, last_date, overlap_fraction, is_primary}. Capped at 200 rows.
Architecture
Built on the official MCP SDK's mcp.server.fastmcp.FastMCP. Each tool lives in its own module under tools/, and server.py imports and registers them on the FastMCP("la-legislative") instance. All database access is centralized in db.py.
db.py maintains one module-level psycopg2.pool.SimpleConnectionPool (min 1 / max 5) and exposes a single run_query(sql, params) helper that runs a parameterized query with a RealDictCursor and returns rows as dicts.
Security posture
This is a first-class part of the design, not an afterthought:
Read-only DB role. The server connects only as a read-only Postgres role (
mcp_readonly). Write attempts are denied at the database level, not just by convention in the app code.No string-built SQL. Every query uses
%splaceholders with a params tuple. There is zero f-string / string-formatted SQL anywhere in the codebase. Where filters are dynamic (e.g.search_projects), only the values are parameters — column names and clause fragments are hardcoded literals.Server-enforced row caps. Every list-returning tool has a hard cap (
search_projects≤ 50,get_nc_engagement≤ 200) applied server-side regardless of what the caller requests.No leaking internals. Errors surfaced to the model are always generic (
"Database query failed."). The real exception is logged to stderr only, so the raw error and the connection string — which contains the password — can never reach the caller.
How to replicate
Prerequisites: Python 3.10+ (built and tested on 3.13), PostgreSQL, and access to a populated citywise_full database. The database itself (the legislative data) is a prerequisite this repo does not create — this project is a query layer over an existing DB, not an ingestion pipeline.
1. Create the read-only DB role
Run once as a DB admin (in psql or pgAdmin). Choose a real password in place of the placeholder:
CREATE ROLE mcp_readonly WITH LOGIN PASSWORD '<choose-a-password>';
GRANT CONNECT ON DATABASE citywise_full TO mcp_readonly;
GRANT USAGE ON SCHEMA public TO mcp_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO mcp_readonly;Then verify it is genuinely read-only — an INSERT or UPDATE as mcp_readonly must fail with a permission error:
-- Connect as mcp_readonly and try:
INSERT INTO projects (id) VALUES ('test');
-- Expected: ERROR: permission denied for table projects2. Create .env
In the project root, create a .env file (already gitignored — never commit it) with one line:
SQL_CONNECTION="postgresql://mcp_readonly:<choose-a-password>@localhost:15432/citywise_full"Adjust host / port / database to match your setup. The reference deployment uses port 15432.
3. Set up the virtualenv and install dependencies
python3 -m venv .venv
./.venv/bin/pip install -r requirements.txtDependencies (see requirements.txt for exact pins): the official mcp[cli] (>=1.2,<2), psycopg2-binary, and python-dotenv.
4. Verify against the live DB
Before wiring into any client, run the included harness. It exercises each tool function directly against your database and prints sample output (including proof that the search_projects limit cap holds and that member disambiguation triggers):
./.venv/bin/python test_tools.py5. Register with the MCP client
Claude Code (CLI):
claude mcp add --transport stdio la-legislative -- /full/path/to/.venv/bin/python /full/path/to/server.py
claude mcp list # should show la-legislative ✔ ConnectedClaude Desktop: add an entry under mcpServers in claude_desktop_config.json:
{
"mcpServers": {
"la-legislative": {
"command": "/full/path/to/legi-mcp/.venv/bin/python",
"args": ["/full/path/to/legi-mcp/server.py"]
}
}
}Gotcha:
commandmust point at the Python binary inside the venv (.venv/bin/python), not at the.venvdirectory. Pointing at the directory causes a "Permission denied" spawn failure.
Gotchas / notes
.envis loaded by absolute path, not cwd.db.pyresolves.envrelative to the source file (os.path.dirname(os.path.abspath(__file__))), because the MCP client launches the server from its own working directory. A plain cwd-relativeload_dotenv()would silently fail to find.env. This is already handled — don't "simplify" it back to a bareload_dotenv().Always use the venv's Python in the launch command, so
mcpandpsycopg2resolve. A systempython3will not have the dependencies.Restart / reconnect the MCP client after any config change before expecting it to take effect.
Out of scope
By explicit design, this server does not include:
Any write / insert / update tools.
Hosting, auth, or multi-user access.
A raw-SQL tool.
Tools against
project_graphs/graph_typesorfile_activities_url.activity_file_summary— those are currently unpopulated in the source data, so nothing is built on them yet.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/Rehaan12345/legi-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server