Skip to main content
Glama

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 目录树内(否则被拒绝)

工具

说明

关键参数

list_datasets

递归列出 SAS_DATA_ROOT 下所有数据集文件(.sas7bdat / .xpt / .sas7bcat)

pattern(文件名子串过滤,不区分大小写)、limit(默认 500)、offset

get_dataset_metadata

行数、每变量 label/type/informat、文件标签、创建时间(不加载数据内容;.xpt 例外,见下)

path

summarize_dataset

逐变量概要统计——了解一张表的首选入口:缺失率、唯一值数,按类型给分布指标

path、columns、apply_labels

frequency

单变量频数表:各取值的 n 与占比,默认自动套用 SAS 值标签

path、column、limit、offset、apply_labels、include_missing

read_dataset

按 offset/limit 分页读取 JSON records(NaN→null,日期→ISO),支持列子集、where 过滤、套用值标签

path、limit(默认 20)、offset、columns、where、apply_labels

get_value_labels

变量的 SAS 格式值标签映射(QC 解码 coded 值用)

path

query_dataset

duckdb SQL 查询(仅允许单条 SELECT / WITH 查询),主表为 dataset,可追加多表做 JOIN

path、sql、limit(默认 100)、offset、extra_paths、apply_labels

search_variables

跨数据集搜索含指定变量的数据集(只读元数据)

variable、pattern、contains、limit、max_datasets

建议调用顺序: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 继续获取。

字符编码(除 list_datasets 外的 7 个工具通用):都可选传 encoding(如 gb18030 / cp1252 / latin1),只对本次调用生效。 不传时先按默认(或 SAS_LOCAL_MCP_ENCODING)解码,失败再按 SAS_LOCAL_MCP_ENCODING_FALLBACKS 依次回退, 成功的编码记在文件版本上供后续读取复用,并在返回的 encoding_fallback 里给出(query_dataset 为 表名 -> 编码 的映射)。 显式指定的编码解码失败时直接报 ENCODING_ERROR,不会静默回退。

  • 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 行。 此时本工具单列一次性读取并计数(分块读取对 XPORT 是 O(n²)),只缓存计数;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 查询(可以使用 WITH CTE);多条语句和 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):

    • fastmcp 4.x

    • pandas >= 2.0

    • pyreadstat >= 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 字段传入即可。

变量

默认值

说明

SAS_DATA_ROOT

进程当前工作目录(CWD)

数据根目录,所有数据集路径必须落在其内(沙箱边界)

SAS_LOCAL_MCP_MAX_ROWS

200

行预览上限:read_dataset 一次最多返回多少条原始记录

SAS_LOCAL_MCP_MAX_AGG_ROWS

1000

聚合结果上限:frequency 的取值个数、query_dataset 的返回行数

SAS_LOCAL_MCP_ENCODING

空(pyreadstat 默认)

全局固定的字符编码;中文数据常需 utf-8 或 gbk。设置后不再自动回退,工具的 encoding 参数仍可按次覆盖

SAS_LOCAL_MCP_ENCODING_FALLBACKS

cp1252,latin1

未指定编码且默认解码失败时依次尝试的候选编码(逗号分隔,空字符串关闭回退)。默认不含 gbk/gb18030:它们会把 cp1252 的字节静默解成错误的汉字

SAS_LOCAL_MCP_CACHE_SIZE

8

缓存多少个已解析数据集(按 mtime/size 失效);0 关闭

SAS_LOCAL_MCP_METADATA_CACHE_SIZE

512

独立的元数据缓存条目数,元数据与格式目录各另有 32 MiB 预算

SAS_LOCAL_MCP_CACHE_MB

256

完整数据缓存预算,单位 MiB;超预算的单个数据集不保留

SAS_LOCAL_MCP_CHUNK_ROWS

10000

sas7bdat 带 where 过滤时的读取块大小(XPORT 一次读取,不分块)

SAS_LOCAL_MCP_MAX_RESPONSE_BYTES

524288

每份结构化 JSON 响应的 UTF-8 字节预算,协议封装另有开销

SAS_LOCAL_MCP_SQL_MEMORY_MB

512

每个 DuckDB 连接的内存上限,禁止临时磁盘溢写;不包含 pandas 数据

SAS_LOCAL_MCP_SQL_THREADS

2

每个 DuckDB 查询的工作线程数

SAS_LOCAL_MCP_SQL_CONCURRENCY

2

同时执行的整表操作数:SQL、summarize_dataset、frequency 及 XPORT 过滤读取共用此上限

SAS_LOCAL_MCP_NAME

sas-local-mcp

服务名(显示在客户端)

两个上限分开的意义: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 公开示例提交包:

在此之上派生出三套等价数据,用于交叉验证同一份数据在不同格式 / 编码下的读取结果:

目录

内容

说明

tests/xpt/

原始 XPORT 文件

上述两个 original-*data/ 目录直接拷贝;源带 Windows-1252 字节(如 ts.xpt)

tests/sasdata/

UTF-8 编码的 sas7bdat

由 tests/xpt2sas7bdat.sas 转换

tests/sasdata_cn/

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 模板均随机生成。

类别

文件

大小

行数 × 列数

工具调用(通过/总数)

耗时

sasdata/adam

sasdata/adam/adsl.sas7bdat

192.0 KB

254 × 49

8/8

1173 ms

sasdata/adam

sasdata/adam/adlbcpv.sas7bdat

16.0 MB

37132 × 46

8/8

819 ms

sasdata/adam

sasdata/adam/adlbhpv.sas7bdat

11.2 MB

24966 × 46

8/8

863 ms

sasdata/sdtm

sasdata/sdtm/sc.sas7bdat

128.0 KB

254 × 14

8/8

407 ms

sasdata/sdtm

sasdata/sdtm/supplb.sas7bdat

54.6 MB

64403 × 10

8/8

771 ms

sasdata/sdtm

sasdata/sdtm/sv.sas7bdat

384.0 KB

3559 × 8

8/8

406 ms

sasdata_cn/adam

sasdata_cn/adam/adsl.sas7bdat

192.0 KB

254 × 49

8/8

362 ms

sasdata_cn/adam

sasdata_cn/adam/adlbcpv.sas7bdat

16.0 MB

37132 × 46

8/8

862 ms

sasdata_cn/adam

sasdata_cn/adam/adlbh.sas7bdat

9.9 MB

24966 × 46

8/8

1005 ms

sasdata_cn/sdtm

sasdata_cn/sdtm/sc.sas7bdat

128.0 KB

254 × 14

8/8

396 ms

sasdata_cn/sdtm

sasdata_cn/sdtm/supplb.sas7bdat

54.6 MB

64403 × 10

8/8

939 ms

sasdata_cn/sdtm

sasdata_cn/sdtm/ts.sas7bdat

128.0 KB

33 × 6

8/8

389 ms

xpt/adam

xpt/adam/adtte.xpt

71.8 KB

254 × 26

8/8

576 ms

xpt/adam

xpt/adam/adlbcpv.xpt

15.8 MB

37132 × 46

8/8

1265 ms

xpt/adam

xpt/adam/adcibc.xpt

283.7 KB

730 × 36

8/8

551 ms

xpt/sdtm

xpt/sdtm/suppds.xpt

4.8 KB

3 × 10

8/8

434 ms

xpt/sdtm

xpt/sdtm/supplb.xpt

54.1 MB

64403 × 10

8/8

1441 ms

xpt/sdtm

xpt/sdtm/cm.xpt

3.6 MB

7510 × 21

8/8

458 ms

sasdata/adam/adsl.sas7bdat(sasdata/adam,192.0 KB)

  • [PASS] list_datasets 633 ms — pattern='ads' 命中 3 个

  • [PASS] get_dataset_metadata 28 ms — 254 行 × 49 列

  • [PASS] summarize_dataset 38 ms — 49 变量概要(全量)

  • [PASS] get_value_labels 4 ms — 无值标签

  • [PASS] frequency 6 ms — SEX: 2 取值 / 返回 2 / 分母 254 / include_missing=True

  • [PASS] read_dataset 10 ms — 返回 1 行 / 过滤后 254 行 / offset=166

  • [PASS] query_dataset 78 ms — 1 行结果(group_by)

  • [PASS] search_variables 376 ms — AVGDD: 命中 3 个数据集 / contains=True

sasdata/adam/adlbcpv.sas7bdat(sasdata/adam,16.0 MB)

  • [PASS] list_datasets 5 ms — pattern='adl' 命中 15 个

  • [PASS] get_dataset_metadata 12 ms — 37132 行 × 46 列

  • [PASS] summarize_dataset 230 ms — 3 变量概要(列子集)

  • [PASS] get_value_labels 2 ms — 无值标签

  • [PASS] frequency 4 ms — AENTMTFL: 2 取值 / 返回 2 / 分母 37132 / include_missing=True

  • [PASS] read_dataset 47 ms — 返回 1 行 / 过滤后 37122 行 / offset=10368

  • [PASS] query_dataset 159 ms — 1 行结果(group_by)

  • [PASS] search_variables 359 ms — RACEN: 命中 36 个数据集 / contains=False

sasdata/adam/adlbhpv.sas7bdat(sasdata/adam,11.2 MB)

  • [PASS] list_datasets 5 ms — pattern='adl' 命中 15 个

  • [PASS] get_dataset_metadata 12 ms — 24966 行 × 46 列

  • [PASS] summarize_dataset 332 ms — 46 变量概要(全量)

  • [PASS] get_value_labels 3 ms — 无值标签

  • [PASS] frequency 4 ms — SEX: 2 取值 / 返回 2 / 分母 24966 / include_missing=False

  • [PASS] read_dataset 5 ms — 返回 3 行 / 过滤后 24966 行 / offset=3050

  • [PASS] query_dataset 126 ms — COUNT(*)=24966(元数据 24966 行)

  • [PASS] search_variables 377 ms — RACEN: 命中 36 个数据集 / contains=True

sasdata/sdtm/sc.sas7bdat(sasdata/sdtm,128.0 KB)

  • [PASS] list_datasets 4 ms — pattern='sc' 命中 5 个

  • [PASS] get_dataset_metadata 6 ms — 254 行 × 14 列

  • [PASS] summarize_dataset 8 ms — 6 变量概要(列子集)

  • [PASS] get_value_labels 2 ms — 无值标签

  • [PASS] frequency 3 ms — SCDTC: 207 取值 / 返回 10 / 分母 254 / include_missing=False

  • [PASS] read_dataset 4 ms — 返回 3 行 / 过滤后 254 行 / offset=1

  • [PASS] query_dataset 22 ms — COUNT(*)=254(元数据 254 行)

  • [PASS] search_variables 358 ms — SCSTRESN: 命中 3 个数据集 / contains=True

sasdata/sdtm/supplb.sas7bdat(sasdata/sdtm,54.6 MB)

  • [PASS] list_datasets 4 ms — pattern='sup' 命中 12 个

  • [PASS] get_dataset_metadata 29 ms — 64403 行 × 10 列

  • [PASS] summarize_dataset 246 ms — 5 变量概要(列子集)

  • [PASS] get_value_labels 3 ms — 无值标签

  • [PASS] frequency 14 ms — QVAL: 61 取值 / 返回 50 / 分母 64403 / include_missing=False

  • [PASS] read_dataset 4 ms — 返回 3 行 / 过滤后 64403 行 / offset=4184

  • [PASS] query_dataset 126 ms — 100 行结果(projection)

  • [PASS] search_variables 344 ms — USUBJID: 命中 87 个数据集 / contains=False

sasdata/sdtm/sv.sas7bdat(sasdata/sdtm,384.0 KB)

  • [PASS] list_datasets 6 ms — pattern='sv' 命中 3 个

  • [PASS] get_dataset_metadata 4 ms — 3559 行 × 8 列

  • [PASS] summarize_dataset 20 ms — 3 变量概要(列子集)

  • [PASS] get_value_labels 3 ms — 无值标签

  • [PASS] frequency 5 ms — VISIT: 36 取值 / 返回 10 / 分母 3559 / include_missing=False

  • [PASS] read_dataset 4 ms — 返回 0 行 / 过滤后 2803 行 / offset=3335

  • [PASS] query_dataset 29 ms — 20 行结果(projection)

  • [PASS] search_variables 335 ms — VISITNUM: 命中 54 个数据集 / contains=True

sasdata_cn/adam/adsl.sas7bdat(sasdata_cn/adam,192.0 KB)

  • [PASS] list_datasets 3 ms — pattern='ads' 命中 3 个

  • [PASS] get_dataset_metadata 5 ms — 254 行 × 49 列

  • [PASS] summarize_dataset 9 ms — 2 变量概要(列子集)

  • [PASS] get_value_labels 3 ms — 无值标签

  • [PASS] frequency 2 ms — AGEGR1N: 3 取值 / 返回 3 / 分母 254 / include_missing=False

  • [PASS] read_dataset 4 ms — 返回 1 行 / 过滤后 254 行 / offset=171

  • [PASS] query_dataset 27 ms — 5 行结果(projection)

  • [PASS] search_variables 309 ms — COMP16FL: 命中 3 个数据集 / contains=True

sasdata_cn/adam/adlbcpv.sas7bdat(sasdata_cn/adam,16.0 MB)

  • [PASS] list_datasets 5 ms — pattern='adl' 命中 15 个

  • [PASS] get_dataset_metadata 12 ms — 37132 行 × 46 列

  • [PASS] summarize_dataset 216 ms — 2 变量概要(列子集)

  • [PASS] get_value_labels 3 ms — 无值标签

  • [PASS] frequency 4 ms — PARAMN: 18 取值 / 返回 18 / 分母 37132 / include_missing=True

  • [PASS] read_dataset 5 ms — 返回 10 行 / 过滤后 37132 行 / offset=27397

  • [PASS] query_dataset 160 ms — 1 行结果(group_by)

  • [PASS] search_variables 458 ms — R2A1HI: 命中 15 个数据集 / contains=True

sasdata_cn/adam/adlbh.sas7bdat(sasdata_cn/adam,9.9 MB)

  • [PASS] list_datasets 7 ms — pattern='adl' 命中 15 个

  • [PASS] get_dataset_metadata 12 ms — 24966 行 × 46 列

  • [PASS] summarize_dataset 461 ms — 46 变量概要(全量)

  • [PASS] get_value_labels 4 ms — 无值标签

  • [PASS] frequency 5 ms — SEX: 2 取值 / 返回 2 / 分母 24966 / include_missing=False

  • [PASS] read_dataset 5 ms — 返回 25 行 / 过滤后 24966 行 / offset=3395

  • [PASS] query_dataset 143 ms — 20 行结果(projection)

  • [PASS] search_variables 369 ms — LBSEQ: 命中 15 个数据集 / contains=False

sasdata_cn/sdtm/sc.sas7bdat(sasdata_cn/sdtm,128.0 KB)

  • [PASS] list_datasets 5 ms — pattern='sc' 命中 5 个

  • [PASS] get_dataset_metadata 5 ms — 254 行 × 14 列

  • [PASS] summarize_dataset 15 ms — 14 变量概要(全量)

  • [PASS] get_value_labels 4 ms — 无值标签

  • [PASS] frequency 3 ms — SCSTRESN: 19 取值 / 返回 19 / 分母 254 / include_missing=True

  • [PASS] read_dataset 5 ms — 返回 3 行 / 过滤后 254 行 / offset=22

  • [PASS] query_dataset 23 ms — 5 行结果(projection)

  • [PASS] search_variables 336 ms — SCCAT: 命中 3 个数据集 / contains=True

sasdata_cn/sdtm/supplb.sas7bdat(sasdata_cn/sdtm,54.6 MB)

  • [PASS] list_datasets 4 ms — pattern='sup' 命中 12 个

  • [PASS] get_dataset_metadata 29 ms — 64403 行 × 10 列

  • [PASS] summarize_dataset 377 ms — 6 变量概要(列子集)

  • [PASS] get_value_labels 3 ms — 无值标签

  • [PASS] frequency 13 ms — QVAL: 61 取值 / 返回 50 / 分母 64403 / include_missing=False

  • [PASS] read_dataset 7 ms — 返回 10 行 / 过滤后 64403 行 / offset=43487

  • [PASS] query_dataset 153 ms — COUNT(*)=64403(元数据 64403 行)

  • [PASS] search_variables 353 ms — STUDYID: 命中 102 个数据集 / contains=True

sasdata_cn/sdtm/ts.sas7bdat(sasdata_cn/sdtm,128.0 KB)

  • [PASS] list_datasets 4 ms — pattern='ts' 命中 3 个

  • [PASS] get_dataset_metadata 4 ms — 33 行 × 6 列

  • [PASS] summarize_dataset 8 ms — 6 变量概要(全量)

  • [PASS] get_value_labels 4 ms — 无值标签

  • [PASS] frequency 3 ms — TSSEQ: 4 取值 / 返回 4 / 分母 33 / include_missing=False

  • [PASS] read_dataset 5 ms — 返回 10 行 / 过滤后 33 行 / offset=7

  • [PASS] query_dataset 22 ms — COUNT(*)=33(元数据 33 行)

  • [PASS] search_variables 340 ms — STUDYID: 命中 102 个数据集 / contains=True

xpt/adam/adtte.xpt(xpt/adam,71.8 KB)

  • [PASS] list_datasets 9 ms — pattern='adt' 命中 3 个

  • [PASS] get_dataset_metadata 25 ms — 254 行 × 26 列

  • [PASS] summarize_dataset 33 ms — 26 变量概要(全量)

  • [PASS] get_value_labels 3 ms — 无值标签

  • [PASS] frequency 6 ms — SEX: 2 取值 / 返回 2 / 分母 254 / include_missing=True

  • [PASS] read_dataset 7 ms — 返回 25 行 / 过滤后 254 行 / offset=153

  • [PASS] query_dataset 35 ms — 100 行结果(projection)

  • [PASS] search_variables 458 ms — TRTSDT: 命中 36 个数据集 / contains=True

xpt/adam/adlbcpv.xpt(xpt/adam,15.8 MB)

  • [PASS] list_datasets 5 ms — pattern='adl' 命中 15 个

  • [PASS] get_dataset_metadata 629 ms — 37132 行 × 46 列

  • [PASS] summarize_dataset 19 ms — 6 变量概要(列子集)

  • [PASS] get_value_labels 4 ms — 无值标签

  • [PASS] frequency 6 ms — COMP24FL: 2 取值 / 返回 2 / 分母 37132 / include_missing=True

  • [PASS] read_dataset 5 ms — 返回 10 行 / 过滤后 37132 行 / offset=19184

  • [PASS] query_dataset 189 ms — 1 行结果(group_by)

  • [PASS] search_variables 408 ms — ALBTRVAL: 命中 12 个数据集 / contains=True

xpt/adam/adcibc.xpt(xpt/adam,283.7 KB)

  • [PASS] list_datasets 3 ms — pattern='adc' 命中 1 个

  • [PASS] get_dataset_metadata 30 ms — 730 行 × 36 列

  • [PASS] summarize_dataset 7 ms — 4 变量概要(列子集)

  • [PASS] get_value_labels 4 ms — 无值标签

  • [PASS] frequency 6 ms — DTYPE: 2 取值 / 返回 2 / 分母 730 / include_missing=False

  • [PASS] read_dataset 6 ms — 返回 3 行 / 过滤后 730 行 / offset=673

  • [PASS] query_dataset 51 ms — 1 行结果(group_by)

  • [PASS] search_variables 444 ms — ADT: 命中 30 个数据集 / contains=True

xpt/sdtm/suppds.xpt(xpt/sdtm,4.8 KB)

  • [PASS] list_datasets 3 ms — pattern='sup' 命中 12 个

  • [PASS] get_dataset_metadata 8 ms — 3 行 × 10 列

  • [PASS] summarize_dataset 6 ms — 3 变量概要(列子集)

  • [PASS] get_value_labels 2 ms — 无值标签

  • [PASS] frequency 2 ms — QEVAL: 1 取值 / 返回 1 / 分母 3 / include_missing=False

  • [PASS] read_dataset 3 ms — 返回 2 行 / 过滤后 3 行 / offset=1

  • [PASS] query_dataset 20 ms — COUNT(*)=3(元数据 3 行)

  • [PASS] search_variables 390 ms — QNAM: 命中 12 个数据集 / contains=False

xpt/sdtm/supplb.xpt(xpt/sdtm,54.1 MB)

  • [PASS] list_datasets 6 ms — pattern='sup' 命中 12 个

  • [PASS] get_dataset_metadata 825 ms — 64403 行 × 10 列

  • [PASS] summarize_dataset 108 ms — 4 变量概要(列子集)

  • [PASS] get_value_labels 3 ms — 无值标签

  • [PASS] frequency 12 ms — QLABEL: 2 取值 / 返回 2 / 分母 64403 / include_missing=False

  • [PASS] read_dataset 10 ms — 返回 1 行 / 过滤后 64403 行 / offset=30747

  • [PASS] query_dataset 127 ms — 5 行结果(projection)

  • [PASS] search_variables 351 ms — QNAM: 命中 12 个数据集 / contains=True

xpt/sdtm/cm.xpt(xpt/sdtm,3.6 MB)

  • [PASS] list_datasets 4 ms — pattern='cm' 命中 3 个

  • [PASS] get_dataset_metadata 98 ms — 7510 行 × 21 列

  • [PASS] summarize_dataset 15 ms — 5 变量概要(列子集)

  • [PASS] get_value_labels 2 ms — 无值标签

  • [PASS] frequency 5 ms — VISIT: 15 取值 / 返回 10 / 分母 7510 / include_missing=False

  • [PASS] read_dataset 3 ms — 返回 3 行 / 过滤后 7510 行 / offset=3856

  • [PASS] query_dataset 42 ms — 1 行结果(group_by)

  • [PASS] search_variables 289 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)

本地 .sas7bdat / .xpt 文件

认证

OAuth2 PKCE 流程 + 本地 token 缓存

无需认证(无网络出口)

运行形态

HTTP(默认)、stdio、Docker、Kubernetes

仅 stdio,由 MCP 客户端按需拉起

工具数量

92 个,按 10 个 tier 选择启用

8 个,全部只读(无写入 / 代码执行能力)

代码组织

tools/ 下约 10 个模块

单个 tools.py + 核心 dataset.py

Python 版本

>= 3.12

>= 3.11

运行依赖

fastmcp、httpx、pydantic、uvicorn、anyio、python-dotenv

fastmcp、pandas、pyreadstat、duckdb、sqlglot、pydantic

统计语义层

无——需自行执行 SAS / SQL 取数后再推断

summarize_dataset / frequency 直接给出缺失率、唯一值数、按类型的分布指标,并自动套用 SAS 值标签

SQL 能力

依赖 Viya / CAS

内置 duckdb:仅允许单条 SELECT(支持 WITH),关闭外部文件与网络访问

结果限流

—

行预览(SAS_LOCAL_MCP_MAX_ROWS)与聚合结果(SAS_LOCAL_MCP_MAX_AGG_ROWS)分开限流,聚合基数不被预览上限截断

字符编码

由 Viya 侧决定

全局 SAS_LOCAL_MCP_ENCODING、每个读取工具的 encoding 参数,以及解码失败时的自动回退(结果里回传 encoding_fallback)

解析缓存

—

按 (路径, mtime, size, metadata_only) 失效的 LRU 缓存,多轮问答不重复解析大文件

安全边界

OAuth + Viya 侧权限体系

路径沙箱(越界即拒绝)+ 只读工具 + SQL 白名单

错误处理

—

DatasetError 统一转 MCP ToolError,返回干净消息而不崩溃进程

格式覆盖

—

.sas7bdat / .xpt 可读;.sas7bcat 通过 catalog_path 作为外部值标签目录读取,也可被 list_datasets 列出

测试数据

—

取自 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 入口
│       ├── cache.py        # 有界 LRU 缓存(字节预算 + 单次加载)
│       ├── config.py       # 环境变量配置(数据根、行数上限、缓存与 SQL 资源、编码)
│       ├── dataset.py      # 数据集读取 + 统计核心(pyreadstat + pandas 回退)
│       ├── errors.py       # DatasetError 统一错误类型
│       ├── filtering.py    # where 表达式的受限求值器(不使用 eval)
│       ├── responses.py    # 响应字节预算与输出 schema
│       ├── sql_labels.py   # SQL 列裁剪与标签来源分析(sqlglot)
│       ├── stdio_server.py # FastMCP stdio 服务启动
│       └── tools.py        # MCP 工具注册(register_tools)
└── tests/
    ├── conftest.py         # 解析数据根(兼容根目录直放 / 分目录两种布局)
    ├── test_tools.py       # 单元测试:in-memory Client 逐个调用全部工具
    ├── test_regressions.py # 不依赖数据集的定点回归(注入 meta)
    ├── test_bugfixes.py    # 缺陷修复回归
    ├── test_optimizations.py # 性能与响应预算相关回归
    ├── test_random_smoke.py  # 随机冒烟的 pytest 入口
    ├── random_smoke.py     # 随机冒烟:stdio 子进程 + 各类别大小文件抽检
    ├── sasdata/            # 测试数据:英文 sdtm / adam
    ├── sasdata_cn/         # 测试数据:中文版(编码回归)
    └── xpt/                # 测试数据:XPORT 版(格式回归)

许可证

本项目代码采用 MIT License,详见 LICENSE。

注意:随仓库附带的测试数据取自 R Consortium submissions-pilot5-datasetjson 公开示例提交包,其许可与本仓库代码许可相互独立,请以该来源仓库的声明为准。

Available Tools

8 tools
frequencyFrequencyA
Read-only

单变量频数表:各取值的 n 与占比,默认自动套用 SAS 值标签。

结果按 n 降序;原始 value 与解码后的 label 一并返回。 denominator 回传百分比的分母(include_missing 为 True 时是总行数,否则是非缺失行数)。

Inputs: path (required), column (required), limit, offset, +2 more. Returns: object

get_dataset_metadataGet Dataset MetadataB
Read-only

获取数据集元数据:行数、每变量 label/type/format、文件标签、创建时间。

Inputs: path (required). Returns: object

get_value_labelsGet Value LabelsB
Read-only

获取变量的 SAS 格式值标签映射(QC 时解码 coded 值用)。

Inputs: path (required). Returns: object

list_datasetsList DatasetsA
Read-only

列出 SAS_DATA_ROOT 下所有 SAS 数据集文件(.sas7bdat / .xpt / .sas7bcat)。

Inputs: limit, pattern. Returns: object

query_datasetQuery DatasetA
Read-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)"

Inputs: sql (required), path (required), limit, offset, +2 more. Returns: object

read_datasetRead DatasetA
Read-only

按 offset/limit 分页读取记录(records 为 JSON,NaN 为 null,日期为 ISO 字符串)。

Inputs: path (required), limit, where, offset, +2 more. Returns: object

search_variablesSearch VariablesA
Read-only

在 SAS_DATA_ROOT 下搜索含指定变量的数据集。

回答"哪个数据集里有 USUBJID / 哪些表有 AEDECOD",避免逐个 get_dataset_metadata 试错。只读取元数据,不加载数据行;损坏文件记入 errors 而不中断搜索。

Inputs: variable (required), limit, pattern, contains, +1 more. Returns: object

summarize_datasetSummarize DatasetA
Read-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)。

Inputs: path (required), columns, apply_labels. Returns: object

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.8/5.0

    Scored across 8 tools

    Disambiguation4/5

    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.

    Naming Consistency4/5

    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.

    Tool Count5/5

    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.

    Completeness4/5

    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

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      A local MCP server enabling AI assistants to query and analyze data via DuckDB SQL engine, supporting local files, memory, S3, and MotherDuck.
      23 npm
      Apache 2.0
    • A
      license
      Not graded
      quality
      D
      maintenance
      A 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
    • A
      license
      A
      quality
      C
      maintenance
      MCP 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.
      1
      32 PyPI
      1
      MIT
    • A
      license
      A
      quality
      D
      maintenance
      A 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.
      5
      MIT