MDA DB MCP
# MDA 数据库问答
用中文或英文提问,LLM 自动搜索元数据、写 SQL、返回结果并画图。
覆盖 5 个调查项目、1149 张表的社会经济调查微观数据。
由两部分组成:
- **MCP Server** —— 把「查这个数据库」包装成 14 个标准工具。
既给自家网页用,也能直接挂到 Claude Desktop / Claude Code。
- **问答网页** —— 中英双语界面,API Key 在网页上配置,聊天历史本地留存。
## 启动
```bash
./mda_db_mcp_start.sh
```
打开 http://localhost:3344 就能用。
指定端口:
默认端口 3344。想换:
```bash
./mda_db_mcp_start.sh 8080 # 位置参数
PORT=8080 ./mda_db_mcp_start.sh # 或环境变量
```
其他参数:
| 命令 | 作用 |
|---|---|
| `./mda_db_mcp_start.sh --dev` | 改代码自动重载(开发用) |
| `./mda_db_mcp_start.sh --lan` | 允许局域网内其他设备访问 |
| `./mda_db_mcp_start.sh --help` | 看全部用法 |
`mda_db_mcp_start.sh` 启动前会跑一遍预检 —— 依赖、端口、数据库、元数据索引、API Key。
这几项是实际最容易卡住的地方,有问题时直接告诉你怎么修,
而不是等服务起来之后丢一段 traceback:
```
MDA 数据库问答 —— 启动预检
依赖就绪
端口 3344 可用
数据库已连接(mda_viewer 可见 1149 张表)
元数据索引:106,291 条,0.1 天前建立
API Key 已配置(deepseek / deepseek-flash,来自本地配置)
→ http://127.0.0.1:3344
Ctrl+C 停止
```
首次运行会自动 `uv sync` 装依赖、自动建立元数据索引(约 10 秒)。
### 怎么停
脚本会占住终端。**Ctrl+C 或直接关掉终端窗口都会停止服务并释放端口。**
脚本不是用 `exec` 交棒,而是后台跑 uvicorn 再配一个信号 trap,
为的是保证「退出时端口一定被释放」:
| 退出方式 | 端口释放 | 优雅关闭 |
|---|---|---|
| Ctrl+C | ✓ | ✓ |
| 关闭终端窗口 | ✓ | 直接终止(见下) |
| `kill <脚本 PID>` | ✓ | ✓(trap 转发 SIGTERM) |
| `kill -9` | ✓ | ✗(强杀无法优雅) |
trap 会等子进程自己关(最多 3 秒),没动静才转发 SIGTERM,
再等 7 秒仍不退就强杀 —— 所以不会出现「Ctrl+C 之后终端卡住」
或者「端口被僵尸进程占着」。
关于「优雅关闭」:指 uvicorn 执行 lifespan 关闭流程,
主动结束 MCP 子进程、关掉连接池。关闭终端时(SIGHUP)uvicorn 不处理这个信号
会直接终止,但实测**不会留下任何东西** —— MCP 子进程的 stdin 管道断开后
会自行退出,数据库连接由操作系统关闭 socket 时释放。
我按 Ctrl+C、关终端、kill 脚本、kill -9、以及 `--dev` 重载模式
五种情况逐一验证过:端口连接数和 `mda_viewer` 的数据库连接数都归零。
想确认有没有残留:
```bash
lsof -nP -iTCP:3344 # 应该没有输出
psql -d mda -c "select count(*) from pg_stat_activity where usename='mda_viewer'"
```
### 不用脚本直接起
```bash
uv run uvicorn backend.main:app --port 3344
```
只是少了预检。想单独跑预检:`uv run python -m backend.preflight`。
## 首次使用
启动后点「设置」,填入 DeepSeek 或 Gemini 的 API Key:
- **DeepSeek**:https://platform.deepseek.com/api_keys (便宜,中文好)
- **Gemini**:https://aistudio.google.com/apikey (有免费额度)
填完点「测试连接」再保存 —— 这样配错了能立刻看到原因,
而不是存下来之后提问时才报错。
模型名是**自由输入**的,下拉列表只是建议。两家的模型 ID 换得很快
(DeepSeek 已从 `deepseek-chat` 换成 `deepseek-flash`,Gemini 从 2.5 一路到 3.8),
所以别写死在代码里,去控制台看当前可用的名字直接填。
Key 加密后存在 `~/.mda_db_mcp/`(项目目录之外,不会被 git 提交)。
## 能问什么
| 你问 | 它做什么 |
|---|---|
| 现在哪些调查入库了? | `list_surveys` —— 5 个调查的规模、覆盖国家、年份 |
| MICS 包括哪些 sections? | `list_sections` —— 模块划分、每行代表什么、连接键 |
| 有没有和教育年限相关的变量? | `find_variables` —— 语义搜索 10.6 万条变量元数据 |
| `years_attended_school` 有多少个回答? | `variable_stats` —— 回答数、取值分布、加权统计 |
| 画一下做饭燃料的分布 | `plot_variable` —— 图直接显示在聊天里 |
每一步的工具调用和 SQL 都实时显示,可以展开核对它写得对不对。
## 结构
```
mda_db_mcp_start.sh 一条命令启动(含预检)
mcp_server/ MCP Server —— 把「查数据库」包装成 14 个标准工具
db.py 连接池 + 通用 SQL(元数据查询、安全检查)
catalog.py 调查目录层:四个调查族三种元数据形态,用适配器统一
index.py 本地 FTS5 变量索引(建索引 + 检索排序)
variables.py 单变量统计与可视化(缺失值、加权)
server.py 工具定义,也是给 LLM 的使用说明
backend/ 网页后端
main.py FastAPI 接口
config_store.py API Key 加密存储、供应商与推理参数定义
history.py 聊天历史(SQLite)
mcp_bridge.py 后端作为 MCP 客户端,翻译工具格式给 LLM
preflight.py 启动预检(数据库 / 索引 / Key)
agent.py agent 循环:LLM ↔ 工具的来回对话
web/index.html 前端(单文件,无需构建,中英双语)
test_mcp.py 手动测试 MCP Server
grants.sql 开通蒙古 HSES/LFS 只读权限(已执行;换机器时需重跑)
revoke_grants.sql 撤销上述授权
```
## MCP Server 提供的 14 个工具
### 按调查结构探索
| 工具 | 用途 |
|---|---|
| `list_surveys` | 有哪些调查入库了。区分**已入库可读 / 在库但无权限 / 未入库**三种状态 |
| `list_sections` | 某调查包含哪些 section。返回物理分表和问卷章节两种,附 grain 和连接键 |
| `read_guide` | 读库内自带的 `public._guide` / `_catalog` / `_data_issues` |
### 找变量(核心)
| 工具 | 用途 |
|---|---|
| `find_variables` | **主要工具**。在本地 FTS5 索引里按语义搜变量,< 10ms,带词干还原和相关性排序 |
| `rebuild_metadata_index` | 重建索引。新数据入库或新开通 schema 权限后需要跑一次 |
| `metadata_index_status` | 索引状态:建立时间、条目数、各调查覆盖情况 |
### 看变量和画图
| 工具 | 用途 |
|---|---|
| `variable_stats` | 有多少回答、取值分布(带码值含义)、描述统计、加权结果 |
| `plot_variable` | 画条形图或直方图,图直接显示在网页上 |
### 通用
| 工具 | 用途 |
|---|---|
| `list_schemas` / `list_tables` / `describe_table` | 物理结构 |
| `search_metadata` | 搜表名/列名/注释(精确子串匹配,找变量请用 `find_variables`) |
| `sample_rows` | 抽几行看真实取值 |
| `run_query` | 执行只读 SQL |
## 数据库里有什么
全部 5 个调查族都已开通只读访问(`mda_viewer` 可见 1339 张表):
| 调查 | 规模 | 位置 |
|---|---|---|
| **MICS**(UNICEF 多指标簇调查) | 255 数据集 / 97 国 / MICS2–MICS6(1999–2023) | `public` |
| **DHS**(人口与健康调查) | 112 调查 / 82 国 / 1985–2024 | `dhs_*` |
| **CSES**(柬埔寨社会经济调查) | 10 个波次 / 2004–2021 | `cses_*` |
| **HSES**(蒙古家庭社会经济调查) | 11 个波次 / 2002–2020 / 5.9 GB | `mng_hses_*` |
| **LFS**(蒙古劳动力调查) | 14 个波次 | `mng_lfs_*` |
另有气候数据(`final_CLIMATE_DAILY_ADMIN2` 等)和尼泊尔生活标准调查 2022,都在 `public`。
### 权限
蒙古的 6 个 schema 最初对 `mda_readonly` 没有 USAGE 权限,已通过 `grants.sql`
开通(只授予 SELECT,角色仍是只读)。撤销用 `revoke_grants.sql`。
**开通新 schema 后必须重建索引**,否则搜不到新数据。
### 四个调查族,三种元数据形态
`catalog.py` 用「每个调查一个适配器、输出统一 10 列」的方式抹平差异:
| 调查 | 协调层 | 变量层 | 值标签 | section 来源 |
|---|---|---|---|---|
| MICS | `ind_que_*_MICS`(有 `measure_type` / `canonical_text`) | 同左 | — | `public._catalog` |
| CSES / DHS | `*_canonical_variable` | `*_source_variable` | `*_value_mapping` | 表名正则 |
| HSES | `concept`(有 `module_family`) | `variable` | `value_label` | `module_family` |
| LFS | `canonical_variable` | `mng_lfs_meta.variable` | `mng_lfs_meta.value_label` | 无 |
加新调查(比如将来接别的国家)只需在 `PROJECTS` 里加一条并写好它的
`extracts` SQL,五个工具全部自动生效。
### 两个针对具体数据的处理
**HSES 的协调层有 3 个发布版本**(`hses-alignment-v1` / `v1.1` / `v1.2`),
每个概念在每个版本里各存一行。工具只取最新版本 —— 版本号用 `int[]` 比较
而不是字符串,否则 `v1.10` 会被判定小于 `v1.9`。
**HSES 只索引 concept 层,不索引原始列层**:1881 个原始列名里有 1877 个
已被 concept 覆盖(它的 `aligned_name` 常常就是原始列名),两层都索引会让
每次搜索结果出现两份重复。
**LFS 的协调变量会标注状态**:8 个变量里只有 2 个是 `qualified_core`,
其余 6 个是 `candidate_only`(尚未验证)。工具把状态拼进变量说明里,
避免 LLM 把候选变量当成已验证的推荐出去。
## 「语义搜索」是怎么做的
`find_variables` 不是向量检索。语义能力来自两层配合:
1. **LLM 扩词**:你说「教育年限」,LLM 展开成
`["education","schooling","grade","attainment","years attended","diploma"]`
2. **本地 FTS5 索引检索排序**:10 万条变量元数据,porter 词干还原 + BM25 排序
所以搜「教育年限」能同时找到 `years_attended_school`(教育年限)和
`highest_education_level`(最终学历)—— 这正是你要的「语义相关」。
为什么不直接查数据库:
| | 实时查库 | 本地 FTS5 索引 |
|---|---|---|
| 耗时 | 1.4 秒 | **< 10ms** |
| 词干还原 | 无 | ✅ education / educational 互通 |
| 相关性排序 | 手写 | ✅ 内置 BM25 |
| 数据库负载 | 每次全表扫 130 万行 | 零 |
装不了 `pg_trgm` / `pgvector`(只读角色没权限),所以索引放在本地
`~/.mda_db_mcp/metadata_index.db`(约 28 MB,建一次 9 秒)。
**代价**:索引是快照。新数据入库后要重建,否则搜不到新变量。
`find_variables` 会返回索引年龄,超过 30 天会附带提醒。
### 排序的实用性调整
除了 BM25 相关性,排序还做了几项调整,依据来自库内文档和数据分布:
- `canonical`(协调变量,有定义和语义分类)排在 `source`(原始列裸标签)前
- **`CP_` 前缀的列往前排** —— `_guide` 的 `cp_prefix` 章节明确说跨数据集分析要优先用它们
- `survey_specific_response` 类往后压 —— DHS 里有 777 个这种单一调查专用变量
(`JO_2023_woman_education_recode`),关键词一命中就会挤掉真正可跨国比较的变量
- 覆盖数据集多的变量往前排
## 库内自带的文档(重要)
数据库的 `public` schema 里有三张维护者手写的文档表,比任何从 `pg_catalog`
推断出来的信息都权威。`read_guide` 工具把它们读出来给 LLM:
| 表 | 内容 |
|---|---|
| `_guide` | 7 章使用指南:连接约定、编码约定、协调变量清单、`CP_` 前缀规则 |
| `_catalog` | 每张表的 grain、连接键、说明、caveats、行数、数据集数 |
| `_data_issues` | 已知数据问题(当前 1 个未解决、63 个已修) |
里面有几条推断不出来但极其关键的规则:
- **`CP_` 前缀 = carefully processed**,跨数据集分析优先用这些列
- 原始变量的缺失哨兵是 7/9、97/98/99、9997-9999;`*_harmonized` / `*_years` 已置 NULL
- 性别 1=男 2=女;是/否通常 1=是 2=否,**但 MICS2 时代(1999–2001)常是 0=否 1=是**
- `final_WM_MICS` 的户标识列是 `hh_number`,不是 `household_number`
- 连接键在部分表是 TEXT、部分是 DOUBLE PRECISION,跨表连接要先转类型
- 键只在同一 `dataset_name` 内唯一,连接必须带上 `dataset_name`
- 跨国分析用 `CP_country` / `CP_country_code`,不要解析 `dataset_name`
MCP Server 的工具说明已经把这些写给 LLM,并要求它涉及 MICS 或跨数据集比较时先读指南。
## 缺失值与加权
这是调查数据分析最容易出错的两点,工具层面都做了处理:
**缺失值编码**:98="don't know"、99="missing" 这类编码在元数据里是机器可读的
(`value_labels` JSONB),所以不靠猜 96–99,而是读元数据判断。
读不到时会明确说「无法判断」而不是假装没有。
**抽样权重**:`variable_stats` 和 `plot_variable` 会自动发现权重列
(`household_weight`、`person_weight` 等)并在返回里列出;传 `weight` 参数即加权。
未加权时会提示「这是样本数不是总体估计」。
实测差异:CSES 教育年限未加权均值 6.33,加权后 6.20。
## 界面语言
右上角按钮切换中文 / English。切换的不只是界面文案——**模型的回答语言也会跟着换**,
所以给客户演示时切成 English,整个界面和答案都是英文的。
语言选择存在浏览器 localStorage 里,同时同步到后端配置。
进度提示(「正在思考…」「返回 120 行 × 8 列」)也是按当前语言渲染的:
后端只发 `summary_key` + 参数,文案由前端拼,所以加语种不用改后端。
## Markdown 渲染
LLM 的回答按 Markdown 渲染:表格、标题、列表、代码块、粗体、链接。
给客户演示时表格能正常显示,不是一堆 `|---|---|`。
**不引 marked / DOMPurify**,自己写了一个约 150 行的渲染器,原因:
1. 保持单文件零依赖、离线可用 —— CDN 挂了不会让页面变空白
2. 安全模型简单到能一眼看完:**先把整段文本转义,之后渲染器生成的标签
就是页面上唯一的 HTML**,LLM 输出和数据库内容都无法注入
占位符用 `<F0>` 这种形式:转义之后文本里不可能再出现 `<`(都成了 `<`),
所以用户或 LLM 无论写什么都撞不上占位符。
### 故意不支持 `_斜体_` 和 `__粗体__`
这个库的变量名全是下划线。支持下划线强调会把回答毁掉:
```
years_attended_school → years<em>attended</em>school
q0611__2020_000cb8d1 → q0611<strong>2020</strong>000cb8d1
```
LLM 写强调基本都用 `*`,所以这个取舍没有实际损失。
### 链接白名单
只放行 `http://`、`https://`、`mailto:` 和站内相对路径。
`javascript:`、`data:`、`vbscript:` 以及 `//evil.com`(协议相对 URL,会跳外站)
都拒绝,原样显示成文本。
### 流式渲染节流
一次回答有上百个 token,每来一个就重排表格既抖又费。
所以流式期间 120ms 渲染一次,收到 `done` 时立即定稿。
原文存在元素的 `dataset` 上,定时器回调取最新值,不会渲染到过期内容。
### 错误信息不走 Markdown
报错文字是我们自己生成的,里面常带 SQL 报错原文(含 `*`、`_`、`|` 等字符),
按纯文本显示更不容易看错。
## 聊天历史
自动保存,存在 `~/.mda_db_mcp/history.db`(SQLite)。
**为什么不存在 mda 数据库里**:mda 是只读的(角色 `mda_viewer` 属于 `mda_readonly`
组),写不进去;而且调查数据库不该混进应用自己的数据。
每条助手消息除了正文,还存下:
- **工具调用过程**(包括完整 SQL)—— 打开历史会话时会原样重现,给客户演示时可以回放
- **思考过程** —— 模型的推理内容
- **token 用量** —— 用来核对推理档位的实际开销
侧边栏操作:单击打开、**双击标题重命名**(自动生成的标题多半不适合给客户看)、
悬停点 × 删除。设置页有「清空全部历史」。
历史库和 API Key 一样是 600 权限,包括 SQLite 的 `-wal` / `-shm` 旁支文件
(它们装着同样的内容,只锁主库等于没锁)。
## 推理与 Token 控制
设置页的「推理与 Token 控制」有三个旋钮,作用范围不同:
| 控件 | 作用 | 什么时候用 |
|---|---|---|
| 推理档位 `reasoning_effort` | 两家通用的标准档位 | 日常调节的首选 |
| 输出 token 上限 | 输出总量上限 | **限制 thinking 开销最直接的手段** |
| 高级:原生参数 (JSON) | 直接透传给 API | 上面两个表达不了时 |
**为什么用 `reasoning_effort` 而不是各家的原生参数**:各家把它映射到自己的原生
参数(Gemini 3.x → `thinking_level`,Gemini 2.5 → `thinking_budget`,
DeepSeek → thinking 模式档位),原生参数名一直在变,标准参数不用跟着改。
各家可选档位:
- **DeepSeek**:`none` / `low` / `high` / `max`,默认 `high`。
选 `none` 关闭思考,回答更快更便宜,简单查询够用。
- **Gemini**:`none` / `minimal` / `low` / `medium` / `high`。
`none` 只有 2.5 Flash 系列支持,**2.5 Pro 和 3.x 系列不能关闭思考**,传了会报错。
**关于输出上限**:思考 token 算在输出里,所以这是限制 thinking 花费最有效的办法。
但设太小会让回答被截断在半句话——建议至少留 4096。
**关于高级参数**:只在标准档位不够用时使用,比如要指定确切的预算:
```json
{"google": {"thinking_config": {"thinking_level": "low", "include_thoughts": true}}}
```
注意 `reasoning_effort` 和 `thinking_level` / `thinking_budget` **互斥**,
同时设置会报 400。
**参数被拒时不会静默降级**:如果你设的档位这个模型不支持,界面会明确报错并告诉你
是哪个设置的问题——不会偷偷丢掉参数重试,那样你会以为设置生效了。
**看实际花了多少**:每条回答下面会显示 `token:输入 8900 · 思考 100 · 输出 190`,
多轮工具调用是累加的。调完档位看这一行就知道有没有效果。
**DeepSeek 的一个坑**:思考模式下不支持指名工具调用(`required` 或指定某个工具),
会返回 400。本项目用的是自动模式(`auto`),不受影响,但你要是改代码去指定工具,
记得先关思考。
## 安全设计
三层防护,任一层单独失效都不会造成数据损坏:
1. **数据库层**:连接角色 `mda_viewer` 只属于 `mda_readonly` 组,
PostgreSQL 直接拒绝任何写操作,也访问不到 `mng_*` 那几个受限 schema。
2. **连接层**:连接池设 `default_transaction_read_only=on`,
每条查询再包一层 `readonly=True` 事务 + 30 秒超时。
3. **SQL 层**:只放行 `SELECT`/`WITH`/`EXPLAIN` 等开头的语句,
拒绝多语句,拉黑 `pg_read_file`、`dblink`、`COPY` 等函数。
另外:
- 查询结果用游标分页,只从数据库取需要的行数
(库里有几百 MB 的表,全量拉回来会打满内存)
- 单次最多返回 500 行
- **数据库密码不存在配置里**,走 `~/.pgpass`
- API Key 用 Fernet 加密存 `~/.mda_db_mcp/config.json`,
密钥文件 `secret.key` 权限 600,都在项目目录之外,不会被 git 提交
## 挂到 Claude Desktop / Claude Code
这个 MCP Server 是标准实现,不止能给自家网页用。
### Claude Code
在项目目录下执行,路径自动解析,不用手填:
```bash
claude mcp add mda-db \
--env MDA_DATABASE_URL=postgresql://mda_viewer@localhost:5432/mda \
-- "$(pwd)/.venv/bin/python" -m mcp_server.server
```
### Claude Desktop
配置文件不支持变量展开,所以要填绝对路径。先在项目目录下打印出该填的值:
```bash
echo "command: $(pwd)/.venv/bin/python"
echo "cwd: $(pwd)"
```
然后编辑 `~/Library/Application Support/Claude/claude_desktop_config.json`
(Windows 在 `%APPDATA%\Claude\`),把上面两个值填进去:
```json
{
"mcpServers": {
"mda-db": {
"command": "<上面打印的 command>",
"args": ["-m", "mcp_server.server"],
"cwd": "<上面打印的 cwd>",
"env": { "MDA_DATABASE_URL": "postgresql://mda_viewer@localhost:5432/mda" }
}
}
}
```
`command` 必须指向项目虚拟环境里的 python(`.venv/bin/python`),
不能用系统 python —— 否则找不到 mcp、asyncpg 这些依赖。
## 环境变量(可选,优先级高于网页配置)
| 变量 | 用途 |
|---|---|
| `MDA_DATABASE_URL` | 数据库连接串 |
| `MDA_LLM_API_KEY` | LLM API Key(设了它网页里就改不动) |
| `MDA_CONFIG_DIR` | 配置和历史库目录,默认 `~/.mda_db_mcp` |
## 排查问题
**端口被占用**
`mda_db_mcp_start.sh` 会告诉你是哪个进程占的,换个端口 `./mda_db_mcp_start.sh 3345` 或 `kill <PID>`。
**网页显示「数据库未连接」**
```bash
psql -h localhost -U mda_viewer -d mda -c "select 1" # 先确认数据库本身能连
uv run python test_mcp.py # 再单测 MCP Server
```
**提问报 HTTP 400 / 401**
Key 不对或没额度。设置页点「测试连接」,错误原文会显示出来。
**LLM 说找不到变量**
先确认索引存在:设置页看「元数据索引」,没有就点「重建索引」。
索引在的话,让它换一批英文同义词再搜——元数据是英文的,中文关键词搜不到。
**新入库的数据搜不到**
索引是快照,需要重建:设置页点「重建索引」,或让 LLM 调
`rebuild_metadata_index`。建一次约 9 秒。
**开通了新 schema 权限但还是搜不到**
`grants.sql` 只改数据库权限,索引不会自动更新。执行后必须重建索引。
**某个调查搜出来的结果成对重复**
说明它的协调层和原始列层指向同一批变量(HSES 就是这种情况,已在
`catalog.py` 里去掉了它的 source 层)。新接调查时要检查这一点:
比较协调层的变量名集合和原始列名集合的重叠度。
**回答里的数字看着不对**
调查数据的缺失值常用 96/97/98/99 编码,直接求平均会算错。
展开「查看 SQL」核对它有没有排除这些值、有没有用抽样权重加权。
**回答说到一半就断了**
输出 token 上限设太小了。思考 token 也算在里面,调大或设成 0。
**报错提到 thinking / reasoning / effort**
推理设置这个模型不接受。最常见两种:给 Gemini 2.5 Pro 或 3.x 传了
`reasoning_effort: none`(它们不能关思考);或者档位和高级参数里的
`thinking_level` 同时设了(两者互斥)。
**切了英文但模型还是回中文**
语言指令是在提问时发出去的,只影响新提问。已有的回答不会重新翻译。
TDQS
Scored across 14 tools
Although several tools touch exploration (find_variables, search_metadata, list_tables, describe_table), the descriptions explicitly delineate boundaries—e.g. find_variables searches the semantic harmonization layer while search_metadata does exact substring matching on physical structure. Hierarchical listers (schemas/tables/surveys/sections) and the stats/plot pair are clearly distinct. No two tools appear interchangeable.
Almost all names follow a clean verb_noun snake_case pattern (describe_table, run_query, list_surveys, find_variables, plot_variable, search_metadata, rebuild_metadata_index). Only variable_stats and metadata_index_status deviate into noun_noun form, but they read consistently as 'X_stats'/'X_status' queries and cause no confusion.
14 tools sit comfortably in the well-scoped range for a rich survey-microdata domain. Each tool maps to a distinct workflow stage—discovery, inspection, query, analysis, documentation, index maintenance—so nothing feels padded or redundant.
The surface covers discovery (schemas, tables, surveys, sections, variables), inspection (describe_table, sample_rows), querying (run_query), analysis (variable_stats, plot_variable), authoritative docs (read_guide) and index management, which is strong lifecycle coverage. Minor gaps remain—no result export/download or crosstab/join helpers—but agents can work around these with run_query.