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 数据当作可查询的上下文来源

所有工具均为只读,不会修改任何数据集文件。

功能特性

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

工具

说明

关键参数

list_datasets

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

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

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)

    • 返回 { 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 查询(可以使用 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 把结果里的 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):

    • fastmcp 4.x

    • pandas >= 2.0

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

变量

默认值

说明

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

SAS_LOCAL_MCP_CACHE_SIZE

8

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

SAS_LOCAL_MCP_NAME

sas-local-mcp

服务名(显示在客户端)

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

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

目录

内容

说明

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

统计语义层

无——需自行执行 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(中文数据常用 utf-8 / gbk)

解析缓存

—

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

安全边界

OAuth + Viya 侧权限体系

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

错误处理

—

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

格式覆盖

—

.sas7bcat 仅能被 list_datasets 列出,pyreadstat 无对应读取器

测试数据

—

取自 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 公开示例提交包,其许可与本仓库代码许可相互独立,请以该来源仓库的声明为准。

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.
    12 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
    A
    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
    1
    MIT
  • A
    license
    A
    quality
    C
    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