sas-local-mcp
Read-only MCP server for exploring and querying local SAS datasets without SAS/Viya/network access.
List
.sas7bdat,.xpt, and.sas7bcatfiles under a sandboxed data root.Retrieve dataset metadata, row/column counts, variable labels, types, formats, and logical types.
Summarize datasets with missingness, distinct counts, numeric/date/character statistics, and top values.
Compute frequency tables with SAS value-label decoding and missing-value handling.
Read paginated records with column selection, safe
wherefilters, and optional label decoding.Fetch SAS value-label mappings for QC decoding.
Run read-only DuckDB SQL (
SELECT/WITH) including joins across datasets.Search across datasets for variables by exact or substring match using metadata only.
Use a read-only, path-sandboxed interface with no dataset modification or network access.
Click on "Deploy 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., "@sas-local-mcpCan you summarize 02Sdtm/dm.sas7bdat and show SEX frequency?"
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.
sas-local-mcp
一个让大模型(LLM)直接读取本地 SAS 数据集的 MCP 服务器。
基于 FastMCP 实现,经
pyreadstat 直接读取本地 .sas7bdat / .xpt 文件,
无需 Viya、无需联网、无需 SAS 授权。
适合的场景:
让 LLM 勘察
/浏览本地.sas7bdat/.xpt数据集的结构与取值TLF(Tables, Listings, Figures)QC:快速做计数、频数、跨数据集关联核对
在 IDE / Agent 中把 SAS 数据当作可查询的上下文来源
所有工具均为只读,不会修改任何数据集文件。
本次优化新增了局部读取、SQL 来源感知解码、独立缓存和响应字节预算。
升级后需更新依赖并重启 MCP 服务。where 现在使用受限表达式,先过滤后投影;
默认统计口径将字符空白计为 SAS 缺失。迁移说明与完整调用示例见
优化 1–6 使用说明。
功能特性
支持 SAS 两种常用格式:
.sas7bdat与传输格式.xpt(XPORT)统计语义层:
summarize_dataset逐变量给缺失率/唯一值数/分布指标,frequency给频数表并自动解码值标签—— 模型无需手写 SQL 就能读懂一张表路径相对
SAS_DATA_ROOT或绝对路径传入,越界路径会被拒绝(安全沙箱)读取时自动处理:缺失值(
NaN→null)、日期/时间(datetime→ISO 字符串)、字符编码行预览与聚合结果分开限流:
GROUP BY的基数不会被行预览上限截断(见环境变量)内置 duckdb SQL 查询:仅执行单条
SELECT(支持WITH/多表 JOIN),并支持结果值标签解码search_variables跨数据集变量搜索:直接回答"哪个数据集里有USUBJID"预览直接读取所需行列,过滤按块扫描;SQL 仅物化当前页,精确计数可以关闭
元数据与数据独立缓存,按文件版本、编码及格式目录失效;数据缓存有内存预算和并发读取合并
支持外部
.sas7bcat值标签;保留 SAS 特殊数值缺失,明确统计缺失口径八个工具均公开输出 schema,长结果按行数与 JSON 字节预算分页
通过 MCP 标准协议以 stdio 方式接入任意兼容客户端(CodeBuddy、Claude Desktop、Cursor 等)
Related MCP server: sqlite-mcp-server
工具一览
所有工具路径参数 path 支持两种写法:
相对
SAS_DATA_ROOT的路径,如02Sdtm/dm.sas7bdat绝对路径,但必须落在
SAS_DATA_ROOT目录树内(否则被拒绝)
工具 | 说明 | 关键参数 |
| 递归列出 |
|
| 行数、每变量 label/type/informat、文件标签、创建时间(不加载数据内容; |
|
| 逐变量概要统计——了解一张表的首选入口:缺失率、唯一值数,按类型给分布指标 |
|
| 单变量频数表:各取值的 |
|
| 按 |
|
| 变量的 SAS 格式值标签映射(QC 解码 coded 值用) |
|
| duckdb SQL 查询(仅允许单条 |
|
| 跨数据集搜索含指定变量的数据集(只读元数据) |
|
建议调用顺序:list_datasets / search_variables → get_dataset_metadata → summarize_dataset →
frequency(分类变量)→ read_dataset / query_dataset(按需取细节)。
各工具参数与返回
list_datasets(pattern?, limit=500, offset=0)返回
{ root, count, datasets: [{ path, name, format, size_bytes, modified }] }。count为完整目录计数,returned为当前页条数,按next_offset继续获取。
get_dataset_metadata(path)返回
{ number_rows, number_columns, file_label, created, modified, columns: [{ name, label, type, format, informat }] }。type的日期判定同时看informat与format(SAS 日期列常只带 format,如DATE9.)。.xpt需要额外计数:pyreadstat 对 XPORT 的 metadata-only 读取不返回行数 (meta.number_rows为None、df 为空),直接采信会把 6 万行的数据集报成 0 行。 此时本工具分块读取一列计数,只缓存计数;include_row_count=False可跳过扫描,未知行数返回null。新增
logical_type(date/time/datetime/string/numeric)、sas_unit和存储宽度; 变量元数据支持column_offset/column_limit分页。
summarize_dataset(path, columns?, apply_labels=False)每个变量返回
{ name, label, type, n, n_missing, pct_missing, n_distinct, has_value_labels, stats }。stats按类型不同:数值 →
{ mean, std, min, p25, median, p75, max }日期 →
{ min, max }字符 →
{ max_length, top_values: [{ value, label, n, pct }] }(Top5)
has_value_labels为True时提示改用frequency看解码后的分布。columns不传则选择全部变量,按offset/limit分页计算。stats_mode="counts"跳过分布计算。
frequency(path, column, limit=50, offset=0, apply_labels=True, include_missing=False)返回
{ total_rows, n_missing, n_distinct, denominator, value_labels_applied, levels, returned, truncated }。levels每项{ value, label, n, pct },按n降序;value是原始码值,label是解码后的文本。apply_labels默认True——coded 值本身不可读,默认直接译出标签。denominator回传百分比分母:include_missing=True时是总行数,否则是非缺失行数。include_missing=True会把缺失单列为一档(value=null、label 为<missing>)。结果上限取
SAS_LOCAL_MCP_MAX_AGG_ROWS(默认 1000),不受行预览上限影响。
read_dataset(path, limit=20, offset=0, columns?, where?, apply_labels=False)columns:只返回指定变量列表。where:受限表达式,如"AGE > 18 and SEX == 'F'";支持比较、数值算术、in/not in、is None/is not None和布尔组合,不允许函数调用、属性访问或@外部变量。过滤在
columns投影之前执行,过滤列可以不出现在返回列中。无过滤时直接局部读取;有过滤时分块扫描并仅保留当前页。
include_row_count=False可在找到下一页后提前停止,未知的rows_after_filter返回null。apply_labels=True时把 coded 值替换为 SAS 值标签文本。offset用于翻到数据中部/尾部取样——只看前几行容易误判(临床数据前几行常属同一受试者)。返回
{ columns, records, total_rows, rows_after_filter, offset, returned, truncated }, 行数受SAS_LOCAL_MCP_MAX_ROWS上限约束。
get_value_labels(path)返回
{ path, format, variables: { 变量: { 码值: 标签 } } }。
query_dataset(path, sql, limit=100, offset=0, extra_paths?, apply_labels=False)只接受一条
SELECT查询(可以使用WITHCTE);多条语句和INSERT/UPDATE/DELETE/ DDL 会被拒绝,DuckDB 外部文件与网络访问也会关闭。主数据集表名固定为
dataset,例如SELECT COUNT(*) AS n FROM dataset。extra_paths传入附加数据集,按文件名主干注册为额外表(lb.sas7bdat→lb), 冲突时回退dataset2/dataset3;返回的tables给出 表名 → 路径 映射。跨数据集对比示例:SELECT COUNT(*) AS n FROM dataset d INNER JOIN lb l USING (USUBJID)apply_labels=True只自动解码来源明确的原始列,支持别名和简单多表投影;聚合、表达式、 CTE、子查询和歧义列保留原值。可通过label_sources显式指定来源。label_decoding按列回传来源、状态与本页实际解码数量;未解码时value_labels_applied=false。返回查询结果 records,上限取
SAS_LOCAL_MCP_MAX_AGG_ROWS,支持offset翻页。include_total=False跳过精确计数,total_rows=null。分页在 DuckDB 中执行;order_by_present指示最外层是否带排序,稳定翻页需要唯一排序键。
search_variables(variable, pattern?, contains=False, limit=50, max_datasets=200)回答"哪个数据集里有某个变量"。默认精确匹配(不区分大小写),
contains=True改为子串匹配 (如AGE命中AGE/AGEU)。pattern先按文件名过滤数据集以缩小扫描范围;只读取元数据,不加载数据行。返回
{ datasets_scanned, datasets_matched, matches: [{ path, dataset, variable, label, type, number_rows }], scan_truncated, truncated, errors };scan_truncated=true表示达到max_datasets后仍有未扫描的数据集,结果可能不完整。 损坏文件记入errors而不中断整个搜索。先使用
next_offset取完当前扫描窗口的匹配结果,再以next_scan_offset继续扫描并重置offset=0。
读取、元数据、摘要、频数和值标签工具均可传入 catalog_path;SQL 使用
catalog_paths={"注册表名": "格式目录路径"}。格式目录也必须位于 SAS_DATA_ROOT 内。
missing_policy="sas" 默认把字符空白纳入缺失;传入 "pandas" 可保留旧的字符缺失口径。
.A–.Z、._ 在原始记录中保留为字符串,统计时计入缺失,细分数量见 missing_details;
SQL 将这些特殊缺失转为 NULL,并返回 special_missing_values。
所有成功响应包含 response_bytes 和 response_truncated。字节预算可能进一步缩短当前页,
请始终使用服务返回的下一页偏移;单条结果过大时返回 [RESPONSE_TOO_LARGE],不会切断字段内容。
所有错误(文件不存在、格式损坏、路径越界、未知变量、SQL 语法错误)都会被转成 MCP ToolError 以
[错误码] 消息 返回给客户端,例如 [UNKNOWN_VARIABLE]、[ENCODING_ERROR]、[QUERY_FAILED]。
环境要求
Python >= 3.11
支持的包(见
pyproject.toml):fastmcp4.xpandas>= 2.0pyreadstat>= 1.2.8(主力读取器)duckdb>= 1.1.0(SQL 查询)sqlglot>= 26.0(保守的 SQL 列来源分析)pydantic>= 2.0(输入与输出 schema)
开发/测试依赖:
pytest、pytest-asyncio
安装
克隆或下载本仓库后,在仓库根目录执行。
方式一:使用 uv(推荐)
# 安装依赖并进入环境
uv sync
# 运行
uv run sas-local-mcp方式二:使用 pip + 虚拟环境
cd <repo> # 仓库根目录
python -m venv .venv
# Windows
.venv\Scripts\python.exe -m pip install -e .
# macOS / Linux
.venv/bin/python -m pip install -e .安装后会在环境中生成一个 sas-local-mcp 可执行入口(脚本/控制台命令)。
若是把仓库从一个目录搬到另一个目录后再用既有
.venv,editable 安装记录的路径可能仍指向旧位置 (.venv/Lib/site-packages/sas_local_mcp.pth)。此时重新执行上面的pip install -e .即可。
方式三:仅起本地开发服务(不安装)
# 确保已 pip install -e . 或依赖已就位
python -m sas_local_mcp服务以 stdio 协议运行:通常由 MCP 客户端在配置后自动拉起,无需手动常驻。
如需手动调试,可直接运行入口可执行文件(如 .venv/bin/sas-local-mcp 或
.venv\Scripts\sas-local-mcp.exe)。
配置(环境变量)
服务通过环境变量配置,在 MCP 客户端配置中以 env 字段传入即可。
变量 | 默认值 | 说明 |
| 进程当前工作目录(CWD) | 数据根目录,所有数据集路径必须落在其内(沙箱边界) |
|
| 行预览上限: |
|
| 聚合结果上限: |
| 空(pyreadstat 默认) | 数据集字符编码;中文数据常需 |
|
| 缓存多少个已解析数据集(按 mtime/size 失效); |
|
| 独立的元数据缓存条目数,元数据与格式目录各另有 32 MiB 预算 |
|
| 完整数据缓存预算,单位 MiB;超预算的单个数据集不保留 |
|
| sas7bdat 带 |
|
| 每份结构化 JSON 响应的 UTF-8 字节预算,协议封装另有开销 |
|
| 每个 DuckDB 连接的内存上限,禁止临时磁盘溢写;不包含 pandas 数据 |
|
| 每个 DuckDB 查询的工作线程数 |
|
| 同时执行的整表操作数:SQL、 |
|
| 服务名(显示在客户端) |
两个上限分开的意义:
GROUP BY的基数常远超预览行数(例如 306 个受试者的USUBJID频数表)。 若聚合也被 200 行截断,模型会读到不完整的分布并可能得出错误结论。
SAS_DATA_ROOT支持~家目录展开;未设置时退化为启动进程的当前目录,启动时会在 stderr 打印警告。超出
SAS_LOCAL_MCP_CACHE_MB的大文件不整表读入:frequency、summarize_dataset和query_dataset只读取用到的列(SQL 中出现*、COLUMNS()、NATURAL JOIN、PIVOT 时仍读取全部列)。
MCP 客户端配置示例
在标准 MCP 客户端配置文件(如 mcp.json / claude_desktop_config.json / 各 IDE 的 MCP 设置)中加入:
{
"mcpServers": {
"sas-local-mcp": {
"type": "stdio",
"command": "/绝对/路径/到/sas-local-mcp",
"args": [],
"env": {
"SAS_DATA_ROOT": "/绝对/路径/到/你的数据根目录"
},
"description": "读取本地 SAS 数据集(sas7bdat/xpt)的只读 MCP 服务"
}
}
}command:填入上一步安装得到的sas-local-mcp入口绝对路径使用
uv:uv run sas-local-mcp的等效绝对路径,或直接"command": "uv", "args": ["--directory", "/绝对/路径/到/repo", "run", "sas-local-mcp"]使用
pip虚拟环境:.venv/bin/sas-local-mcp(macOS/Linux)或.venv\Scripts\sas-local-mcp.exe(Windows)
env.SAS_DATA_ROOT:务必设为你的数据根目录,否则只能用 CWD 作为根。
保存后重启客户端,即可在工具列表中看到 sas-local-mcp 提供的八个工具。
测试
# 安装测试依赖(若尚未)
pip install pytest pytest-asyncio # 或 uv sync
# 运行全部测试
pytest测试使用当前工作区 tests/ 下的真实数据集(来源见下)。这些数据文件被 Git 忽略,
仅克隆源码后需要另外准备;tests/test_optimizations.py 的新增用例使用受控数据及临时 XPORT。
以数据目录作为 SAS_DATA_ROOT(由 tests/conftest.py 自动解析),通过内存客户端挂载本服务逐个调用全部工具,并覆盖:
概要统计的数值/字符分支、列子集、未知变量报错
频数表的降序、占比、分母语义(
include_missing)聚合基数不被行预览上限截断
offset分页与越界行为多表注册与跨数据集 JOIN、SQL 结果值标签解码
跨数据集变量搜索(精确/子串、文件名过滤、未命中)
读取缓存命中(重复读取不再解析)
值标签解码与缺失分档(注入 meta 覆盖)
路径越界被拦截
另有少量用例依赖仓库外的旧工作区数据,找不到时会自动 skip。
测试数据来源
tests/ 下的测试数据取自 R Consortium 的 submissions-pilot5 公开示例提交包:
来源仓库:https://github.com/RConsortium/submissions-pilot5-datasetjson
取用目录:
original-sdtmdata/— SDTM 域的 XPORT(V5)文件,22 个(ae/cm/dm/lb/qs/supplb...)original-adamdata/— ADaM 数据集的 XPORT(V5)文件,12 个(adsl/adlbc/adtte...)
在此之上派生出三套等价数据,用于交叉验证同一份数据在不同格式 / 编码下的读取结果:
目录 | 内容 | 说明 |
| 原始 XPORT 文件 | 上述两个 |
| UTF-8 编码的 sas7bdat | 由 |
| EUC-CN(GBK)编码的 sas7bdat | 同上,用于中文 / 非 UTF-8 编码回归 |
文件规模跨度:4.8 KB(xpt/sdtm/suppds.xpt)~ 54.6 MB(sasdata/sdtm/supplb.sas7bdat,64403 × 10),
随机冒烟测试即按「每类别最大 + 最小」抽样来同时覆盖两端。
转换脚本需在 WLATIN1 会话下运行:
sas.exe -sysin tests/xpt2sas7bdat.sas -encoding wlatin1, 由 SAS 在写出时经ENCODING=数据集选项转码。
随机冒烟测试
tests/random_smoke.py 与上面的单元测试互补:它不走内存直连,而是以
stdio 子进程拉起安装后的 MCP 服务,端到端验证「客户端 → 协议 → 工具」的完整链路。
python tests/random_smoke.py --seed 20261020 --per-category 3 --out README.md自动发现
tests/下所有类别目录,每类至少抽 2 个文件,并强制包含该类中最小与最大的文件每个文件依次调用全部 8 个工具,limit / offset / columns / where / apply_labels / include_missing / SQL 模板随机生成
--out README.md把结果写回下一节的两行标记之间(重复运行就地覆盖)
随机冒烟测试(tests 数据)
以 stdio 子进程方式拉起安装后的 MCP 服务(.venv/Scripts/sas-local-mcp.exe),对 tests/ 下各类别目录随机抽检:每类强制包含该类中最小与最大的文件,每类至少 2 个文件;每个文件依次调用全部 8 个工具,limit / offset / columns / where / apply_labels / include_missing / SQL 模板均随机生成。
类别 | 文件 | 大小 | 行数 × 列数 | 工具调用(通过/总数) | 耗时 |
|
| 192.0 KB | 254 × 49 | 8/8 | 1173 ms |
|
| 16.0 MB | 37132 × 46 | 8/8 | 819 ms |
|
| 11.2 MB | 24966 × 46 | 8/8 | 863 ms |
|
| 128.0 KB | 254 × 14 | 8/8 | 407 ms |
|
| 54.6 MB | 64403 × 10 | 8/8 | 771 ms |
|
| 384.0 KB | 3559 × 8 | 8/8 | 406 ms |
|
| 192.0 KB | 254 × 49 | 8/8 | 362 ms |
|
| 16.0 MB | 37132 × 46 | 8/8 | 862 ms |
|
| 9.9 MB | 24966 × 46 | 8/8 | 1005 ms |
|
| 128.0 KB | 254 × 14 | 8/8 | 396 ms |
|
| 54.6 MB | 64403 × 10 | 8/8 | 939 ms |
|
| 128.0 KB | 33 × 6 | 8/8 | 389 ms |
|
| 71.8 KB | 254 × 26 | 8/8 | 576 ms |
|
| 15.8 MB | 37132 × 46 | 8/8 | 1265 ms |
|
| 283.7 KB | 730 × 36 | 8/8 | 551 ms |
|
| 4.8 KB | 3 × 10 | 8/8 | 434 ms |
|
| 54.1 MB | 64403 × 10 | 8/8 | 1441 ms |
|
| 3.6 MB | 7510 × 21 | 8/8 | 458 ms |
sasdata/adam/adsl.sas7bdat(sasdata/adam,192.0 KB)
[PASS]
list_datasets633 ms — pattern='ads' 命中 3 个[PASS]
get_dataset_metadata28 ms — 254 行 × 49 列[PASS]
summarize_dataset38 ms — 49 变量概要(全量)[PASS]
get_value_labels4 ms — 无值标签[PASS]
frequency6 ms — SEX: 2 取值 / 返回 2 / 分母 254 / include_missing=True[PASS]
read_dataset10 ms — 返回 1 行 / 过滤后 254 行 / offset=166[PASS]
query_dataset78 ms — 1 行结果(group_by)[PASS]
search_variables376 ms — AVGDD: 命中 3 个数据集 / contains=True
sasdata/adam/adlbcpv.sas7bdat(sasdata/adam,16.0 MB)
[PASS]
list_datasets5 ms — pattern='adl' 命中 15 个[PASS]
get_dataset_metadata12 ms — 37132 行 × 46 列[PASS]
summarize_dataset230 ms — 3 变量概要(列子集)[PASS]
get_value_labels2 ms — 无值标签[PASS]
frequency4 ms — AENTMTFL: 2 取值 / 返回 2 / 分母 37132 / include_missing=True[PASS]
read_dataset47 ms — 返回 1 行 / 过滤后 37122 行 / offset=10368[PASS]
query_dataset159 ms — 1 行结果(group_by)[PASS]
search_variables359 ms — RACEN: 命中 36 个数据集 / contains=False
sasdata/adam/adlbhpv.sas7bdat(sasdata/adam,11.2 MB)
[PASS]
list_datasets5 ms — pattern='adl' 命中 15 个[PASS]
get_dataset_metadata12 ms — 24966 行 × 46 列[PASS]
summarize_dataset332 ms — 46 变量概要(全量)[PASS]
get_value_labels3 ms — 无值标签[PASS]
frequency4 ms — SEX: 2 取值 / 返回 2 / 分母 24966 / include_missing=False[PASS]
read_dataset5 ms — 返回 3 行 / 过滤后 24966 行 / offset=3050[PASS]
query_dataset126 ms — COUNT(*)=24966(元数据 24966 行)[PASS]
search_variables377 ms — RACEN: 命中 36 个数据集 / contains=True
sasdata/sdtm/sc.sas7bdat(sasdata/sdtm,128.0 KB)
[PASS]
list_datasets4 ms — pattern='sc' 命中 5 个[PASS]
get_dataset_metadata6 ms — 254 行 × 14 列[PASS]
summarize_dataset8 ms — 6 变量概要(列子集)[PASS]
get_value_labels2 ms — 无值标签[PASS]
frequency3 ms — SCDTC: 207 取值 / 返回 10 / 分母 254 / include_missing=False[PASS]
read_dataset4 ms — 返回 3 行 / 过滤后 254 行 / offset=1[PASS]
query_dataset22 ms — COUNT(*)=254(元数据 254 行)[PASS]
search_variables358 ms — SCSTRESN: 命中 3 个数据集 / contains=True
sasdata/sdtm/supplb.sas7bdat(sasdata/sdtm,54.6 MB)
[PASS]
list_datasets4 ms — pattern='sup' 命中 12 个[PASS]
get_dataset_metadata29 ms — 64403 行 × 10 列[PASS]
summarize_dataset246 ms — 5 变量概要(列子集)[PASS]
get_value_labels3 ms — 无值标签[PASS]
frequency14 ms — QVAL: 61 取值 / 返回 50 / 分母 64403 / include_missing=False[PASS]
read_dataset4 ms — 返回 3 行 / 过滤后 64403 行 / offset=4184[PASS]
query_dataset126 ms — 100 行结果(projection)[PASS]
search_variables344 ms — USUBJID: 命中 87 个数据集 / contains=False
sasdata/sdtm/sv.sas7bdat(sasdata/sdtm,384.0 KB)
[PASS]
list_datasets6 ms — pattern='sv' 命中 3 个[PASS]
get_dataset_metadata4 ms — 3559 行 × 8 列[PASS]
summarize_dataset20 ms — 3 变量概要(列子集)[PASS]
get_value_labels3 ms — 无值标签[PASS]
frequency5 ms — VISIT: 36 取值 / 返回 10 / 分母 3559 / include_missing=False[PASS]
read_dataset4 ms — 返回 0 行 / 过滤后 2803 行 / offset=3335[PASS]
query_dataset29 ms — 20 行结果(projection)[PASS]
search_variables335 ms — VISITNUM: 命中 54 个数据集 / contains=True
sasdata_cn/adam/adsl.sas7bdat(sasdata_cn/adam,192.0 KB)
[PASS]
list_datasets3 ms — pattern='ads' 命中 3 个[PASS]
get_dataset_metadata5 ms — 254 行 × 49 列[PASS]
summarize_dataset9 ms — 2 变量概要(列子集)[PASS]
get_value_labels3 ms — 无值标签[PASS]
frequency2 ms — AGEGR1N: 3 取值 / 返回 3 / 分母 254 / include_missing=False[PASS]
read_dataset4 ms — 返回 1 行 / 过滤后 254 行 / offset=171[PASS]
query_dataset27 ms — 5 行结果(projection)[PASS]
search_variables309 ms — COMP16FL: 命中 3 个数据集 / contains=True
sasdata_cn/adam/adlbcpv.sas7bdat(sasdata_cn/adam,16.0 MB)
[PASS]
list_datasets5 ms — pattern='adl' 命中 15 个[PASS]
get_dataset_metadata12 ms — 37132 行 × 46 列[PASS]
summarize_dataset216 ms — 2 变量概要(列子集)[PASS]
get_value_labels3 ms — 无值标签[PASS]
frequency4 ms — PARAMN: 18 取值 / 返回 18 / 分母 37132 / include_missing=True[PASS]
read_dataset5 ms — 返回 10 行 / 过滤后 37132 行 / offset=27397[PASS]
query_dataset160 ms — 1 行结果(group_by)[PASS]
search_variables458 ms — R2A1HI: 命中 15 个数据集 / contains=True
sasdata_cn/adam/adlbh.sas7bdat(sasdata_cn/adam,9.9 MB)
[PASS]
list_datasets7 ms — pattern='adl' 命中 15 个[PASS]
get_dataset_metadata12 ms — 24966 行 × 46 列[PASS]
summarize_dataset461 ms — 46 变量概要(全量)[PASS]
get_value_labels4 ms — 无值标签[PASS]
frequency5 ms — SEX: 2 取值 / 返回 2 / 分母 24966 / include_missing=False[PASS]
read_dataset5 ms — 返回 25 行 / 过滤后 24966 行 / offset=3395[PASS]
query_dataset143 ms — 20 行结果(projection)[PASS]
search_variables369 ms — LBSEQ: 命中 15 个数据集 / contains=False
sasdata_cn/sdtm/sc.sas7bdat(sasdata_cn/sdtm,128.0 KB)
[PASS]
list_datasets5 ms — pattern='sc' 命中 5 个[PASS]
get_dataset_metadata5 ms — 254 行 × 14 列[PASS]
summarize_dataset15 ms — 14 变量概要(全量)[PASS]
get_value_labels4 ms — 无值标签[PASS]
frequency3 ms — SCSTRESN: 19 取值 / 返回 19 / 分母 254 / include_missing=True[PASS]
read_dataset5 ms — 返回 3 行 / 过滤后 254 行 / offset=22[PASS]
query_dataset23 ms — 5 行结果(projection)[PASS]
search_variables336 ms — SCCAT: 命中 3 个数据集 / contains=True
sasdata_cn/sdtm/supplb.sas7bdat(sasdata_cn/sdtm,54.6 MB)
[PASS]
list_datasets4 ms — pattern='sup' 命中 12 个[PASS]
get_dataset_metadata29 ms — 64403 行 × 10 列[PASS]
summarize_dataset377 ms — 6 变量概要(列子集)[PASS]
get_value_labels3 ms — 无值标签[PASS]
frequency13 ms — QVAL: 61 取值 / 返回 50 / 分母 64403 / include_missing=False[PASS]
read_dataset7 ms — 返回 10 行 / 过滤后 64403 行 / offset=43487[PASS]
query_dataset153 ms — COUNT(*)=64403(元数据 64403 行)[PASS]
search_variables353 ms — STUDYID: 命中 102 个数据集 / contains=True
sasdata_cn/sdtm/ts.sas7bdat(sasdata_cn/sdtm,128.0 KB)
[PASS]
list_datasets4 ms — pattern='ts' 命中 3 个[PASS]
get_dataset_metadata4 ms — 33 行 × 6 列[PASS]
summarize_dataset8 ms — 6 变量概要(全量)[PASS]
get_value_labels4 ms — 无值标签[PASS]
frequency3 ms — TSSEQ: 4 取值 / 返回 4 / 分母 33 / include_missing=False[PASS]
read_dataset5 ms — 返回 10 行 / 过滤后 33 行 / offset=7[PASS]
query_dataset22 ms — COUNT(*)=33(元数据 33 行)[PASS]
search_variables340 ms — STUDYID: 命中 102 个数据集 / contains=True
xpt/adam/adtte.xpt(xpt/adam,71.8 KB)
[PASS]
list_datasets9 ms — pattern='adt' 命中 3 个[PASS]
get_dataset_metadata25 ms — 254 行 × 26 列[PASS]
summarize_dataset33 ms — 26 变量概要(全量)[PASS]
get_value_labels3 ms — 无值标签[PASS]
frequency6 ms — SEX: 2 取值 / 返回 2 / 分母 254 / include_missing=True[PASS]
read_dataset7 ms — 返回 25 行 / 过滤后 254 行 / offset=153[PASS]
query_dataset35 ms — 100 行结果(projection)[PASS]
search_variables458 ms — TRTSDT: 命中 36 个数据集 / contains=True
xpt/adam/adlbcpv.xpt(xpt/adam,15.8 MB)
[PASS]
list_datasets5 ms — pattern='adl' 命中 15 个[PASS]
get_dataset_metadata629 ms — 37132 行 × 46 列[PASS]
summarize_dataset19 ms — 6 变量概要(列子集)[PASS]
get_value_labels4 ms — 无值标签[PASS]
frequency6 ms — COMP24FL: 2 取值 / 返回 2 / 分母 37132 / include_missing=True[PASS]
read_dataset5 ms — 返回 10 行 / 过滤后 37132 行 / offset=19184[PASS]
query_dataset189 ms — 1 行结果(group_by)[PASS]
search_variables408 ms — ALBTRVAL: 命中 12 个数据集 / contains=True
xpt/adam/adcibc.xpt(xpt/adam,283.7 KB)
[PASS]
list_datasets3 ms — pattern='adc' 命中 1 个[PASS]
get_dataset_metadata30 ms — 730 行 × 36 列[PASS]
summarize_dataset7 ms — 4 变量概要(列子集)[PASS]
get_value_labels4 ms — 无值标签[PASS]
frequency6 ms — DTYPE: 2 取值 / 返回 2 / 分母 730 / include_missing=False[PASS]
read_dataset6 ms — 返回 3 行 / 过滤后 730 行 / offset=673[PASS]
query_dataset51 ms — 1 行结果(group_by)[PASS]
search_variables444 ms — ADT: 命中 30 个数据集 / contains=True
xpt/sdtm/suppds.xpt(xpt/sdtm,4.8 KB)
[PASS]
list_datasets3 ms — pattern='sup' 命中 12 个[PASS]
get_dataset_metadata8 ms — 3 行 × 10 列[PASS]
summarize_dataset6 ms — 3 变量概要(列子集)[PASS]
get_value_labels2 ms — 无值标签[PASS]
frequency2 ms — QEVAL: 1 取值 / 返回 1 / 分母 3 / include_missing=False[PASS]
read_dataset3 ms — 返回 2 行 / 过滤后 3 行 / offset=1[PASS]
query_dataset20 ms — COUNT(*)=3(元数据 3 行)[PASS]
search_variables390 ms — QNAM: 命中 12 个数据集 / contains=False
xpt/sdtm/supplb.xpt(xpt/sdtm,54.1 MB)
[PASS]
list_datasets6 ms — pattern='sup' 命中 12 个[PASS]
get_dataset_metadata825 ms — 64403 行 × 10 列[PASS]
summarize_dataset108 ms — 4 变量概要(列子集)[PASS]
get_value_labels3 ms — 无值标签[PASS]
frequency12 ms — QLABEL: 2 取值 / 返回 2 / 分母 64403 / include_missing=False[PASS]
read_dataset10 ms — 返回 1 行 / 过滤后 64403 行 / offset=30747[PASS]
query_dataset127 ms — 5 行结果(projection)[PASS]
search_variables351 ms — QNAM: 命中 12 个数据集 / contains=True
xpt/sdtm/cm.xpt(xpt/sdtm,3.6 MB)
[PASS]
list_datasets4 ms — pattern='cm' 命中 3 个[PASS]
get_dataset_metadata98 ms — 7510 行 × 21 列[PASS]
summarize_dataset15 ms — 5 变量概要(列子集)[PASS]
get_value_labels2 ms — 无值标签[PASS]
frequency5 ms — VISIT: 15 取值 / 返回 10 / 分母 7510 / include_missing=False[PASS]
read_dataset3 ms — 返回 3 行 / 过滤后 7510 行 / offset=3856[PASS]
query_dataset42 ms — 1 行结果(group_by)[PASS]
search_variables289 ms — CMSPID: 命中 3 个数据集 / contains=True
跨数据集 JOIN
[PASS]
query_dataset(JOIN)398 ms — adsl ⋈ adlbcpv = 37132 行
跨数据集 JOIN
[PASS]
query_dataset(JOIN)255 ms — sc ⋈ supplb = 64403 行
跨数据集 JOIN
[PASS]
query_dataset(JOIN)400 ms — adsl ⋈ adlbcpv = 37132 行
跨数据集 JOIN
[PASS]
query_dataset(JOIN)346 ms — sc ⋈ supplb = 64403 行
跨数据集 JOIN
[PASS]
query_dataset(JOIN)1124 ms — adtte ⋈ adlbcpv = 37132 行
跨数据集 JOIN
[PASS]
query_dataset(JOIN)778 ms — suppds ⋈ supplb = 162 行
汇总
随机种子:
20261020(--seed可复现本次抽样)覆盖类别:6 个(
sasdata/adam、sasdata/sdtm、sasdata_cn/adam、sasdata_cn/sdtm、xpt/adam、xpt/sdtm)抽检文件:18 个
工具调用:150 次,成功 150 次,失败 0 次
端到端耗时:18.7 s
全部用例通过(无失败明细)。
与 sassoftware/sas-mcp-server 的差异
两个项目都用 Python + FastMCP 实现 MCP 服务器, 其余在数据源、运行依赖、工具集、运行形态与安全模型上均不同。
维度 | sassoftware/sas-mcp-server | 本仓库(sas-local-mcp) |
数据源 | Viya 平台(Compute / CAS / 模型 REST API) | 本地 |
认证 | OAuth2 PKCE 流程 + 本地 token 缓存 | 无需认证(无网络出口) |
运行形态 | HTTP(默认)、stdio、Docker、Kubernetes | 仅 stdio,由 MCP 客户端按需拉起 |
工具数量 | 92 个,按 10 个 tier 选择启用 | 8 个,全部只读(无写入 / 代码执行能力) |
代码组织 |
| 单个 |
Python 版本 |
|
|
运行依赖 |
|
|
统计语义层 | 无——需自行执行 SAS / SQL 取数后再推断 |
|
SQL 能力 | 依赖 Viya / CAS | 内置 duckdb:仅允许单条 |
结果限流 | — | 行预览( |
字符编码 | 由 Viya 侧决定 |
|
解析缓存 | — | 按 |
安全边界 | OAuth + Viya 侧权限体系 | 路径沙箱(越界即拒绝)+ 只读工具 + SQL 白名单 |
错误处理 | — |
|
格式覆盖 | — |
|
测试数据 | — | 取自 R Consortium submissions-pilot5 公开数据,派生为 XPORT / UTF-8 sas7bdat / GBK sas7bdat 三套(见测试数据来源) |
目录结构
sas-local-mcp/
├── pyproject.toml # 依赖与构建配置
├── README.md
├── LICENSE # MIT
├── src/
│ └── sas_local_mcp/
│ ├── __init__.py
│ ├── __main__.py # python -m sas_local_mcp 入口
│ ├── config.py # 环境变量配置(数据根、两个行数上限、服务名)
│ ├── dataset.py # 数据集读取 + 统计核心(pyreadstat + pandas 回退)
│ ├── stdio_server.py # FastMCP stdio 服务启动
│ └── tools.py # MCP 工具注册(register_tools)
└── tests/
├── conftest.py # 解析数据根(兼容根目录直放 / 分目录两种布局)
├── test_tools.py # 单元测试:in-memory Client 逐个调用全部工具
├── test_regressions.py # 不依赖数据集的定点回归(注入 meta)
├── random_smoke.py # 随机冒烟:stdio 子进程 + 各类别大小文件抽检
├── sasdata/ # 测试数据:英文 sdtm / adam
├── sasdata_cn/ # 测试数据:中文版(编码回归)
└── xpt/ # 测试数据:XPORT 版(格式回归)许可证
本项目代码采用 MIT License,详见 LICENSE。
注意:随仓库附带的测试数据取自 R Consortium submissions-pilot5-datasetjson 公开示例提交包,其许可与本仓库代码许可相互独立,请以该来源仓库的声明为准。
Available Tools
8 toolsfrequencyFrequencyARead-only
单变量频数表:各取值的 n 与占比,默认自动套用 SAS 值标签。
结果按 n 降序;原始 value 与解码后的 label 一并返回。 denominator 回传百分比的分母(include_missing 为 True 时是总行数,否则是非缺失行数)。
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | 数据集路径,相对 SAS_DATA_ROOT 或绝对路径。 | |
| limit | No | 返回取值个数(默认 50,上限 SAS_LOCAL_MCP_MAX_AGG_ROWS)。 | |
| column | Yes | 要统计的变量名。 | |
| offset | No | 起始序号,用于翻页看长尾取值。 | |
| apply_labels | No | 默认 True,把 coded 值译为值标签文本(coded 值本身不可读)。 | |
| include_missing | No | 默认 False;为 True 时把缺失单列为一档并计入分母。 |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly/non-destructive/closed-world, so safety is covered. The description adds real behavioral detail not in the annotations: results are sorted by n descending, labels are auto-applied by default, and the denominator rule changes with include_missing. It does not mention the row cap behavior beyond limit, but that is in the schema.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three short, front-loaded lines: what it computes, how output is shaped, and how denominators work. No filler; the only redundancy is restating label application that also appears in the schema.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With 6 params at 100% schema coverage, an output schema, and read-only annotations, the description covers the remaining interpretive gaps (ordering, label decoding, denominator). Nothing an agent needs to call it correctly is missing, though sibling disambiguation would complete it.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so baseline would be 3. The description earns more by explaining the consequence of include_missing on the percentage denominator ('total row count when True, otherwise non-missing'), which is a semantic effect the schema text does not spell out.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a specific verb+resource pair (单变量频数表 – univariate frequency table) and states what is produced (n and percentage per distinct value). It implicitly separates itself from summarize_dataset/query_dataset by being single-column distribution counting, but never names a sibling explicitly.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The usage context is implied by the label '单变量频数表' and the limits on value count, but there is no explicit guidance on when to choose this over summarize_dataset or query_dataset, nor any stated prerequisites (e.g. that a dataset path under SAS_DATA_ROOT is required).
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_dataset_metadataGet Dataset MetadataBRead-only
获取数据集元数据:行数、每变量 label/type/format、文件标签、创建时间。
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | 数据集路径,相对 SAS_DATA_ROOT 或绝对路径,如 "02Sdtm/dm.sas7bdat"。 |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The annotations already declare readOnlyHint=true, destructiveHint=false, and openWorldHint=false, so the safety profile is covered. The description adds a contents inventory (label/type/format, creation time), which helps set expectations, but it discloses nothing about failure behavior for missing paths or dataset size limits. With annotations carrying the safety burden, a 3 is appropriate.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
It is a single compact sentence that lands the resource first and then lists return contents; every clause earns its place with zero filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return structure need not be explained, and the one required parameter is fully documented. The only missing piece for a complete definition is guidance on when to prefer this over summarize_dataset.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% and the single path parameter is fully documented in the schema with a relative/absolute example. The description adds nothing about the path argument, so the baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
It gives a specific verb+resource ("获取数据集元数据") and enumerates exactly what metadata is returned (row count, per-variable label/type/format, file label, creation time). This distinguishes it in substance from list_datasets and read_dataset, but no sibling is named explicitly.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
There is no statement of when to use this tool versus read_dataset or summarize_dataset, and no prerequisites, such as whether the dataset must exist or whether a path prefix is required. Usage must be inferred entirely from the contents list.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_value_labelsGet Value LabelsBRead-only
获取变量的 SAS 格式值标签映射(QC 时解码 coded 值用)。
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | 数据集路径,相对 SAS_DATA_ROOT 或绝对路径。 |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, destructiveHint=false and openWorldHint=false, so the safety profile is covered. The description adds only that the result is a label mapping used for decoding; it leaves open whether the mapping covers the whole dataset or only selected variables, and gives no cost/size context.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
One short sentence with no filler, and the core purpose is front-loaded. It is however extremely terse given the ambiguity about mapping scope, so it sits slightly below ideal.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return-value shape needn't be explained, and annotations cover safety. The remaining gap is scope: whether labels are returned for every variable in the dataset or filtered somehow, which matters for an agent deciding whether to call this per dataset.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the single `path` parameter (relative to SAS_DATA_ROOT or absolute) is fully documented in the schema. The description adds nothing about the parameter, so the baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource: it returns SAS format value-label mappings. The parenthetical adds a concrete purpose (decoding coded values during QC), which distinguishes it from generic readers like read_dataset. It does not explicitly name a sibling to differentiate from, so it stops short of 5.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The phrase 'QC 时解码 coded 值用' implies when to reach for it (data-quality checks on coded values), which is more than nothing. However, no alternative is named (e.g., get_dataset_metadata for variable metadata) and no exclusion or prerequisite is stated.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_datasetsList DatasetsARead-only
列出 SAS_DATA_ROOT 下所有 SAS 数据集文件(.sas7bdat / .xpt / .sas7bcat)。
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | 最多返回条数(默认 500)。 | |
| pattern | No | 可选,文件名子串过滤(不区分大小写),如 "ae" 或 "14.1"。 |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare this a safe, non-destructive, closed-world read, so the description only needs to add context. It contributes the scope (SAS_DATA_ROOT) and the file-type filter, but says nothing about whether subdirectories are traversed, how results are ordered, or what happens when `limit` truncates the listing — the behaviors that actually matter for a directory scan.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
One front-loaded sentence with the scope first and the accepted extensions last; nothing is wasted. It is arguably under-specified rather than bloated, but as a structure it is clean.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be described, and annotations cover the safety profile and closed-world scope. Combined with a fully documented 2-parameter schema, the description supplies everything needed to call this correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, with both `limit` and `pattern` documented inline (default 500, case-insensitive substring match), so the schema carries parameter meaning. The description adds no syntax or matching details beyond that, making baseline 3 correct.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (列出/list) and resource (SAS 数据集文件), and names the root scope SAS_DATA_ROOT plus the accepted extensions (.sas7bdat / .xpt / .sas7bcat), which no sibling does. It does not explicitly distinguish itself from read_dataset or get_dataset_metadata, but the 'list files' framing is distinct enough that an agent will not confuse them.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is only implied: an agent can infer this is the discovery step before read_dataset or get_dataset_metadata, but the description never says so nor mentions any exclusion or prerequisite. No alternative is named, so guidance stops at the obvious.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
query_datasetQuery DatasetARead-only
对数据集执行 duckdb SQL(仅 SELECT / WITH)。
主数据集表名固定为 dataset。传入 extra_paths 可再注册附加表,表名取文件名主干 (如 lb.sas7bdat -> lb),冲突时回退 dataset2/dataset3;返回的 tables 给出 表名->路径 映射。用于跨数据集对比,例如: sql="SELECT COUNT(*) FROM dataset a FULL JOIN lb b USING(USUBJID)"
| Name | Required | Description | Default |
|---|---|---|---|
| sql | Yes | SQL 语句,如 "SELECT COUNT(*) AS n FROM dataset"。 | |
| path | Yes | 主数据集路径,相对 SAS_DATA_ROOT 或绝对路径。 | |
| limit | No | 返回行数(默认 100,上限 SAS_LOCAL_MCP_MAX_AGG_ROWS,默认 1000)。 | |
| offset | No | 起始行号,用于翻页。 | |
| extra_paths | No | 可选,附加数据集路径列表,注册为额外表参与 JOIN。 | |
| apply_labels | No | 为 True 时把结果里的 coded 值替换为 SAS 值标签文本。 |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint/openWorldHint/destructiveHint=false, and the description is consistent with them, adding the important enforcement detail that only SELECT/WITH statements are accepted. It also discloses the extra_paths table-registration and collision fallback behavior (dataset2/dataset3), which the agent could not infer otherwise. It does not discuss error behavior or result-size truncation, keeping it below a 5.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loads the core action and constraint in the first sentence, then adds the table-naming rule and a worked example, each of which earns its place. Dense but not padded; a short pointer to the paging parameters is the only missing piece.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values needn't be spelled out, and the description still helpfully notes that the response includes a tables name->path map. Combined with the SELECT/WITH restriction and extra_paths naming rules, an agent has enough to call it correctly; only truncation/paging behavior at the row cap is unaddressed.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
With 100% schema coverage the baseline is 3, and the description genuinely adds semantics beyond the schema: how extra_paths entries map to table names (filename stem, e.g. lb.sas7bdat -> lb) and how name conflicts resolve. The remaining parameters (limit, offset, apply_labels) are left entirely to the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Names a specific verb (execute DuckDB SQL) and resource (dataset), pins the main table name as `dataset`, and restricts the dialect to SELECT/WITH. That level of specificity cleanly separates it from read_dataset, summarize_dataset, and frequency.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly states the intended scenario — cross-dataset comparison via extra_paths — and backs it with a concrete FULL JOIN example. It stops short of saying when to prefer the simpler siblings (read_dataset/summarize_dataset) instead, so it is clear context without exclusions.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
read_datasetRead DatasetARead-only
按 offset/limit 分页读取记录(records 为 JSON,NaN 为 null,日期为 ISO 字符串)。
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | 数据集路径,相对 SAS_DATA_ROOT 或绝对路径。 | |
| limit | No | 返回行数(默认 20,上限 SAS_LOCAL_MCP_MAX_ROWS)。 | |
| where | No | 可选,pandas 查询表达式,如 "AGE > 18" 或 "SEX == 'F'"。 | |
| offset | No | 起始行号(默认 0)。用于翻到数据中部/尾部取样, 避免只看前几行(临床数据常是同一受试者)造成误判。 | |
| columns | No | 可选,只返回这些变量。 | |
| apply_labels | No | 为 True 时把 coded 值替换为 SAS 值标签文本。 |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover the safety profile (readOnly, non-destructive, closed-world). The description adds real value beyond them: records are returned as JSON, NaN becomes null, dates are ISO strings. It omits row-cap/limit enforcement behavior, but the schema's limit field documents the SAS_LOCAL_MCP_MAX_ROWS cap.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single sentence, front-loaded with the pagination purpose and followed by a compact parenthetical on serialization quirks. Nothing is wasted and no filler precedes the core statement.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be explained, and serialization behavior is covered. The missing piece is tool selection guidance versus the other read/query siblings, which matters for a six-parameter read tool in a crowded family.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so all six parameters are already documented, including the rationale for offset sampling and the where-expression syntax. The description only echoes offset/limit pagination, adding no syntax or edge-case detail beyond the schema; baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (读取/read) and resource (records from a dataset) and mentions pagination via offset/limit. It does not differentiate itself from siblings like query_dataset or summarize_dataset, so an agent cannot tell from the description alone which read tool to pick.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
No statement of when to use this versus query_dataset, summarize_dataset, or frequency. Pagination is implied as the purpose, but there are no exclusions or alternative-routing guidance anywhere in the description.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_variablesSearch VariablesARead-only
在 SAS_DATA_ROOT 下搜索含指定变量的数据集。
回答"哪个数据集里有 USUBJID / 哪些表有 AEDECOD",避免逐个 get_dataset_metadata 试错。只读取元数据,不加载数据行;损坏文件记入 errors 而不中断搜索。
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | 返回匹配条数(默认 50)。 | |
| pattern | No | 可选,先按文件名子串过滤数据集,缩小扫描范围。 | |
| contains | No | 为 True 时改为子串匹配(如 "AGE" 命中 AGE / AGEU)。 | |
| variable | Yes | 要查找的变量名(默认精确匹配,不区分大小写)。 | |
| max_datasets | No | 最多扫描多少个数据集(默认 200)。 |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint/openWorldHint=false, so safety is covered; the description adds genuinely new behavior: only metadata is read, no data rows are loaded, and corrupted files are collected in errors without aborting the search. That error-tolerance disclosure is exactly the kind of context annotations cannot carry.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three short sentences, front-loaded with the scope constraint, then the use case, then the read-only and error behavior. Nothing repeats the tool name or the schema, and every sentence earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present and full parameter documentation, the description only needs to cover purpose, read-only semantics, and failure behavior, which it does. The only gap is guidance on choosing among siblings such as list_datasets versus this tool, which would make it fully complete.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already explains variable, pattern, contains, limit, and max_datasets. The description only restates that a variable name is supplied and adds no format, matching, or scoping detail beyond the schema, so the baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource (search datasets containing a given variable) plus its scope (under SAS_DATA_ROOT), which is distinct from every sibling. It also explicitly names the sibling it replaces, get_dataset_metadata trial-and-error, so the agent can tell them apart without opening schemas.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives two concrete motivating questions ("which dataset has USUBJID", "which tables have AEDECOD") and names the alternative it avoids (iterating get_dataset_metadata). It lacks explicit when-not-to-use guidance, e.g. when list_datasets or read_dataset is preferable, so it falls short of a 5.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
summarize_datasetSummarize DatasetARead-only
逐变量概要统计——了解一张表的首选入口,省去手写 SQL。
每个变量给出:类型、非空数 n、缺失数 n_missing、缺失率 pct_missing、 唯一值数 n_distinct,以及 has_value_labels(有值标签时改用 frequency 看解码分布)。 再按类型给分布指标: 数值 -> mean/std/min/p25/median/p75/max; 日期 -> min/max; 字符 -> max_length 与 Top5 取值(value/label/n/pct)。
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | 数据集路径,相对 SAS_DATA_ROOT 或绝对路径。 | |
| columns | No | 可选,只统计这些变量;不传则统计全部。 | |
| apply_labels | No | 字符型 Top5 是否套用 SAS 值标签。 |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover the safety profile (readOnly, non-destructive, closed-world), and the description goes further by enumerating exactly which per-variable metrics are produced and how metrics vary by column type. That is genuine behavioral context beyond the annotations, though it omits practical traits such as cost or handling of very wide tables.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The purpose and the 'entry point' claim are front-loaded in the first clause, followed by a structured enumeration of returned metrics. The per-type metric list is dense but each line carries information an agent needs; only slightly longer than necessary.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present, return values need not be explained, yet the description still documents them at a semantic level, and annotations plus full schema coverage handle safety and parameters. The only gap is guidance relative to the other read-oriented siblings, which a user of this complex a tool family would benefit from.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so path, columns and apply_labels are already fully documented in the schema. The description adds no syntax, default or format detail for those parameters — the mention of value labels only implies what apply_labels touches. Baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a specific verb+resource (per-variable summary statistics) and states the tool's role explicitly as the preferred entry point for understanding a table. It also names the sibling it defers to (frequency) for labelled variables, so an agent can distinguish it from alternatives without opening any schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It gives a clear usage condition — use this first to understand a table without writing SQL — and an explicit exclusion: when has_value_labels is true, use frequency to see decoded distributions. It does not, however, position itself against read_dataset, get_dataset_metadata or query_dataset, so the routing picture is only partial.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
8 tool updates
v0.1.0- First observed
frequency - First observed
get_dataset_metadata - First observed
get_value_labels - First observed
list_datasets - First observed
query_dataset - First observed
read_dataset - First observed
search_variables - First observed
summarize_dataset
TDQS
Scored across 8 tools
Each tool targets a distinct step in the SAS data exploration workflow: discovery (list_datasets, search_variables), schema (get_dataset_metadata, get_value_labels), and content (read_dataset, summarize_dataset, frequency, query_dataset). The closest pair is summarize_dataset vs frequency, but the descriptions explicitly delineate overview-per-variable versus single-variable frequency table. read_dataset vs query_dataset also overlap slightly (both return rows), though paging vs SQL is clear enough.
Seven of eight tools follow a clean verb_noun snake_case pattern (list_datasets, get_dataset_metadata, read_dataset, summarize_dataset, get_value_labels, query_dataset, search_variables). The outlier is 'frequency', a bare noun that breaks the verb-first convention. Minor deviation, still readable and predictable overall.
Eight tools is well-scoped for a local SAS dataset inspection server, with each tool earning its place across discovery, metadata, reading, summarization, and querying. No redundant or filler tools are present.
The surface covers discovery, schema inspection, row reading, statistical summaries, value-label decoding, and arbitrary SQL querying — a full read-side lifecycle for local SAS data. Gaps are minor: no export/materialization of results (e.g., write to CSV/parquet) and no explicit write/modify operations, though the latter is plausibly intentional for a read-only analysis server.
Maintenance
Related MCP Connectors
Query, join, profile, clean and convert CSV/JSON/Parquet with server-side DuckDB over MCP.
Token-free MCP server for structured RevoGrid Core, Pro, and Enterprise knowledge retrieval.
Agent-native MCP server over the public saagarpatel.dev corpus. Read-only, stateless.
Hosted MCP server exposing US hospital procedure cost data to AI assistants
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceA local MCP server enabling AI assistants to query and analyze data via DuckDB SQL engine, supporting local files, memory, S3, and MotherDuck.23 npmApache 2.0
- AlicenseNot gradedqualityDmaintenanceA read-only MCP server that enables LLMs to safely explore and query any SQLite database via natural language. It exposes tools for listing tables, describing schemas, and executing SELECT/WITH queries with built-in safety guards like write prevention and row limits.MIT
- AlicenseAqualityCmaintenanceMCP server that profiles local data files (CSV, Parquet, JSON, Excel) and returns compact structured summaries with data-quality flags, enabling AI agents to understand datasets without seeing raw rows.11MIT
- AlicenseAqualityDmaintenanceA read-only DuckDB MCP server offering context-efficient analytics tools (list_datasets, describe_table, profile_column, explain, query) with a semantic layer for business rules, security guards, and disclosed truncation to help LLMs produce correct answers while minimizing token usage.5MIT