内容
# 法规查询服务器
> 只读 [模型上下文协议](https://modelcontextprotocol.io) 服务器(服务器名称:`la-legislative`),用于向 MCP 客户端(Claude Code / Claude Desktop)公开洛杉矶市立法记录 — 议会文件、投票、议会成员活动和社区议会参与 — 作为一组小型、固定的、参数化的查询工具。
您可以将 Claude 指向您本地的 LA 市立法数据数据库,它可以回答诸如“每个议会成员对议会文件 24-0764 的投票结果如何?”或“去年有哪些住房文件涉及第五议会区?”之类的问题,而无需您自己编写 SQL 查询。
## 为什么采用这种设计(一级,不是零级)
该服务器**不允许**模型编写原始 SQL 查询。这是一个刻意的设计选择。
底层架构需要 5+ 个表连接才能回答即使是基本的问题(单个议会文件的“档案”从 `projects`、`file_activities`、`file_activities_url`、`online_documents`、`project_movers`、`votes`、`council_members` 和 `project_address` 等表中提取)。允许大型语言模型(LLM)在这些表上随意连接可能会导致 silent wrong-join 错误 — 看起来正确但实际上是错误的答案。
因此,这是一个**一级**服务器:每个功能都是一个手写的、参数化的查询,位于一个输入参数工具后面。模型选择**哪个**工具并提供**输入参数**;它永远不会自己组合查询。没有原始 SQL 工具。
范围故意限制为**个人、单用户、仅限本地**。无公共访问权限,无身份验证,无写操作,无托管。它通过 **stdio** 与本地 PostgreSQL 数据库(`citywise_full`)通信 — 没有远程 URL 或 HTTP 端点。MCP 客户端将其作为子进程启动。
## 项目结构
```
legi-mcp/
├── .env # SQL_CONNECTION(gitignored — 永不提交)
├── .gitignore
├── requirements.txt
├── server.py # FastMCP 实例 + 工具注册
├── db.py # 连接池 + run_query 助手
├── test_tools.py # 独立验证框架(不是 MCP 工具)
└── tools/
├── get_project.py
├── search_projects.py
├── get_votes.py
├── get_member_activity.py
└── get_nc_engagement.py
```
## 工具
服务器公开五个工具。所有工具均为只读,并接受输入参数。
### `get_project(project_id)`
获取一个议会文件的完整“档案”。包括基本记录(包括汇总投票 tally `vote_given`)、活动时间线、附件文档、在线文档、提案人/支持者、个人成员投票和位置/主题元数据(地址、纬度/经度、主题、地点)。
- `project_id` — 议会文件编号,例如 `"24-0764"`。
返回一个字典,键为 `project`、`timeline`、`documents`、`online_documents`、`movers`、`votes`、`location`。如果文件未找到,则返回 `{"error": ...}`。
### `search_projects(keyword, council_district, status, date_from, date_to, topic, address, limit=25)`
高级搜索,当您没有议会文件编号时使用。所有筛选条件均为可选,并使用 AND 组合。返回轻量级行。
- `keyword` — 与项目名称或关于文本(不区分大小写)匹配。
- `council_district` — 精确的议会区整数。
- `status` — 精确的状态值,例如 `"passed"`、`"planned"`。
- `date_from` / `date_to` — `start_date` 的包含 ISO 日期范围。
- `topic` — 与主题元数据匹配。
- `address` — 与地址元数据匹配。
- `limit` — 默认 25,**服务器端硬上限为 50**。
返回一个列表,包含 `{id, name, status, council_district, start_date, about}`。
### `get_votes(project_id)`
仅获取一个文件的成员投票记录。
- `project_id` — 议会文件编号,例如 `"24-0764"`。
返回一个列表,包含 `{member_name, district, vote}`。
### `get_member_activity(council_member, date_from, date_to)`
获取议会成员的提案/支持记录和投票记录。自由文本名称通过不区分大小写的名称/名/姓匹配解析。如果匹配 **0** 个成员,则返回错误;如果匹配 **2 或以上** 个成员,则返回消歧义列表,而不是猜测。
- `council_member` — 完整的或部分的成员名称。
- `date_from` / `date_to` — 可选的 `start_date` 包含 ISO 日期范围。
返回 `{member, moved_or_seconded, votes}`,如果匹配唯一,则返回;否则返回消歧义/错误字典。
### `get_nc_engagement(council_district, nc_name)`
按区或社区议会名称获取社区议会参与度(社区影响声明计数,日期范围)。**至少需要一个参数**。
- `council_district` — 匹配社区议会领土与区重叠;结果包含 `overlap_fraction` 和 `is_primary`。
- `nc_name` — 与社区议会名称/标准化名称匹配;`overlap_fraction` 和 `is_primary` 为空。
返回一个列表,包含 `{nc_name, council_member_name, cis_count, first_date, last_date, overlap_fraction, is_primary}`。上限为 200 行。
## 架构
基于官方 MCP SDK 的 `mcp.server.fastmcp.FastMCP` 构建。每个工具都位于 `tools/` 下的独立模块中,`server.py` 导入并在 `FastMCP("la-legislative")` 实例上注册它们。所有数据库访问都集中在 `db.py` 中。
`db.py` 维护一个模块级 `psycopg2.pool.SimpleConnectionPool`(最小 1/最大 5),并公开一个 `run_query(sql, params)` 助手,该助手运行参数化查询并返回行作为字典。
### 安全立场
这是设计中的一等公民,而不是事后补充:
- **只读数据库角色。** 服务器仅作为只读 Postgres 角色(`mcp_readonly`)连接。写入尝试在数据库级别被拒绝,而不仅仅是在应用程序代码中约定。
- **无字符串构建 SQL。** 每个查询都使用 `%s` 占位符和参数元组。代码库中没有任何 f 字符串/字符串格式化 SQL。筛选条件动态时(例如 `search_projects`),只有**值**是参数 — 列名和子句片段是硬编码字面量。
- **服务器强制的行上限。** 每个返回列表的工具都有硬上限(`search_projects` ≤ 50,`get_nc_engagement` ≤ 200),在服务器端强制执行,无论调用者请求什么。
- **不泄露内部信息。** 模型显示的错误始终是通用的(`"数据库查询失败。"`)。实际异常记录到 stderr,只有原始错误和连接字符串(其中包含密码)永远无法到达调用者。
## 如何复制
**前提条件:** Python 3.10+(构建和测试于 3.13),PostgreSQL,以及访问已填充的 `citywise_full` 数据库。数据库本身(立法数据)是**此仓库不创建的前提条件** — 该项目是现有数据库上的查询层,而不是数据摄入管道。
### 1. 创建只读数据库角色
在 `psql` 或 pgAdmin 中作为数据库管理员运行一次。选择一个真正的密码代替占位符:
```sql
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;
```
然后验证它确实是只读的 — 以 `mcp_readonly` 身份运行 `INSERT` 或 `UPDATE` 操作必须失败并出现权限错误:
```sql
-- 以 mcp_readonly 身份连接并尝试:
INSERT INTO projects (id) VALUES ('test');
-- 预期:ERROR: permission denied for table projects
```
### 2. 创建 `.env`
在项目根目录中创建一个 `.env` 文件(已经 gitignored — 永不提交),包含一行:
```
SQL_CONNECTION="postgresql://mcp_readonly:<choose-a-password>@localhost:15432/citywise_full"
```
根据您的设置调整主机/端口/数据库。参考部署使用端口 `15432`。
### 3. 设置虚拟环境并安装依赖项
```
python3 -m venv .venv
./.venv/bin/pip install -r requirements.txt
```
依赖项(参见 `requirements.txt` 获取确切的版本号):官方 `mcp[cli]` (>=1.2,<2)、 `psycopg2-binary` 和 `python-dotenv`。
### 4. 验证 live DB
在将其连接到任何客户端之前,运行包含的测试框架。它直接针对您的数据库运行每个工具函数,并打印示例输出(包括 `search_projects` 限制上限的证明和成员消歧义触发器):
```
./.venv/bin/python test_tools.py
```
### 5. 注册到 MCP 客户端
**Claude Code (CLI):**
```
claude mcp add --transport stdio la-legislative -- /full/path/to/.venv/bin/python /full/path/to/server.py
claude mcp list # 应该显示 la-legislative ✔ 已连接
```
**Claude Desktop:** 在 `claude_desktop_config.json` 中添加一个条目,位于 `mcpServers` 下:
```json
{
"mcpServers": {
"la-legislative": {
"command": "/full/path/to/legi-mcp/.venv/bin/python",
"args": ["/full/path/to/legi-mcp/server.py"]
}
}
}
```
> **注意事项:** `command` 必须指向虚拟环境中的 Python **二进制文件** (`.venv/bin/python`),**而不是** `.venv` 目录。指向目录会导致“权限被拒绝”的启动失败。
## 注意事项 / 说明
- **`.env` 通过绝对路径加载,而不是 cwd.** `db.py` 从源文件解析 `.env` 到绝对路径 (`os.path.dirname(os.path.abspath(__file__))`),因为 MCP 客户端从其**自己的**工作目录启动服务器。普通的 cwd 相对 `load_dotenv()` 将默默地找不到 `.env`。这已经处理 — 不要“简化”为裸 `load_dotenv()`。
- **始终使用虚拟环境的 Python** 在启动命令中,以便 `mcp` 和 `psycopg2` 解析。系统 `python3` 将没有依赖项。
- **在配置更改后重启/重新连接 MCP 客户端**,然后再期望其生效。
## 超出范围
按明确的设计,此服务器不包括:
- 任何写入/插入/更新工具。
- 托管、身份验证或多用户访问。
- 原始 SQL 工具。
- 针对 `project_graphs` / `graph_types` 或 `file_activities_url.activity_file_summary` 的工具 — 目前源数据中未填充这些内容,因此尚未构建任何内容。