sas-local-mcp
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 数据当作可查询的上下文来源
所有工具均为只读,不会修改任何数据集文件。
功能特性
支持 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"解析结果按 (路径, mtime, size) 缓存,多轮问答不必反复解析大文件
通过 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)返回
{ root, count, datasets: [{ path, name, format, size_bytes, modified }] }。
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 行。 此时本工具会退回全量读取数一次行数,结果同样进缓存,后续调用不会再重复解析。
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不传则统计全部变量。
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:pandas 查询表达式,如"AGE > 18"或"SEX == 'F'"。where是在columns投影之后求值的:过滤条件只能引用columns里保留的变量, 否则会返回「where 条件无法应用」。想按某列过滤时,记得把它一并放进columns。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把结果里的 coded 值译为值标签文本。返回查询结果 records,上限取
SAS_LOCAL_MCP_MAX_AGG_ROWS,支持offset翻页。
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而不中断整个搜索。
所有错误(文件不存在、格式损坏、路径越界、未知变量、SQL 语法错误)都会被转成 MCP ToolError 以
干净的消息返回给客户端,不会让服务进程崩溃。
环境要求
Python >= 3.11
支持的包(见
pyproject.toml):fastmcp4.xpandas>= 2.0pyreadstat>= 1.2.8(主力读取器)duckdb>= 1.1.0(SQL 查询)
开发/测试依赖:
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 失效); |
|
| 服务名(显示在客户端) |
两个上限分开的意义:
GROUP BY的基数常远超预览行数(例如 306 个受试者的USUBJID频数表)。 若聚合也被 200 行截断,模型会读到不完整的分布并可能得出错误结论。
SAS_DATA_ROOT支持~家目录展开;未设置时退化为启动进程的当前目录。
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/ 下随仓库附带真实数据集(来源见下)。
以数据目录作为 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 公开示例提交包,其许可与本仓库代码许可相互独立,请以该来源仓库的声明为准。
This server cannot be deployed
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.12 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
- AlicenseAqualityAmaintenanceMCP 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
- AlicenseAqualityCmaintenanceA 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