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 继续获取。

  • 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 查询(可以使用 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

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

统计语义层

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

Available Tools

8 tools
frequencyFrequencyA
Read-only

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

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

NameRequiredDescriptionDefault
pathYes数据集路径,相对 SAS_DATA_ROOT 或绝对路径。
limitNo返回取值个数(默认 50,上限 SAS_LOCAL_MCP_MAX_AGG_ROWS)。
columnYes要统计的变量名。
offsetNo起始序号,用于翻页看长尾取值。
apply_labelsNo默认 True,把 coded 值译为值标签文本(coded 值本身不可读)。
include_missingNo默认 False;为 True 时把缺失单列为一档并计入分母。

Output Schema

NameRequiredDescription

No output parameters

TDQS

A3.8/5.0
Behavior4/5

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.

Conciseness4/5

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.

Completeness4/5

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.

Parameters4/5

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.

Purpose4/5

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.

Usage Guidelines3/5

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 MetadataB
Read-only

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

NameRequiredDescriptionDefault
pathYes数据集路径,相对 SAS_DATA_ROOT 或绝对路径,如 "02Sdtm/dm.sas7bdat"。

Output Schema

NameRequiredDescription

No output parameters

TDQS

B3.4/5.0
Behavior3/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines2/5

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 LabelsB
Read-only

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

NameRequiredDescriptionDefault
pathYes数据集路径,相对 SAS_DATA_ROOT 或绝对路径。

Output Schema

NameRequiredDescription

No output parameters

TDQS

B3.4/5.0
Behavior3/5

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.

Conciseness4/5

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.

Completeness3/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines3/5

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 DatasetsA
Read-only

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

NameRequiredDescriptionDefault
limitNo最多返回条数(默认 500)。
patternNo可选,文件名子串过滤(不区分大小写),如 "ae" 或 "14.1"。

Output Schema

NameRequiredDescription

No output parameters

TDQS

A3.6/5.0
Behavior3/5

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.

Conciseness4/5

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.

Completeness5/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines3/5

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 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)"

NameRequiredDescriptionDefault
sqlYesSQL 语句,如 "SELECT COUNT(*) AS n FROM dataset"。
pathYes主数据集路径,相对 SAS_DATA_ROOT 或绝对路径。
limitNo返回行数(默认 100,上限 SAS_LOCAL_MCP_MAX_AGG_ROWS,默认 1000)。
offsetNo起始行号,用于翻页。
extra_pathsNo可选,附加数据集路径列表,注册为额外表参与 JOIN。
apply_labelsNo为 True 时把结果里的 coded 值替换为 SAS 值标签文本。

Output Schema

NameRequiredDescription

No output parameters

TDQS

A4.3/5.0
Behavior4/5

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.

Conciseness4/5

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.

Completeness4/5

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.

Parameters4/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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 DatasetA
Read-only

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

NameRequiredDescriptionDefault
pathYes数据集路径,相对 SAS_DATA_ROOT 或绝对路径。
limitNo返回行数(默认 20,上限 SAS_LOCAL_MCP_MAX_ROWS)。
whereNo可选,pandas 查询表达式,如 "AGE > 18" 或 "SEX == 'F'"。
offsetNo起始行号(默认 0)。用于翻到数据中部/尾部取样, 避免只看前几行(临床数据常是同一受试者)造成误判。
columnsNo可选,只返回这些变量。
apply_labelsNo为 True 时把 coded 值替换为 SAS 值标签文本。

Output Schema

NameRequiredDescription

No output parameters

TDQS

A3.5/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness3/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines2/5

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 VariablesA
Read-only

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

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

NameRequiredDescriptionDefault
limitNo返回匹配条数(默认 50)。
patternNo可选,先按文件名子串过滤数据集,缩小扫描范围。
containsNo为 True 时改为子串匹配(如 "AGE" 命中 AGE / AGEU)。
variableYes要查找的变量名(默认精确匹配,不区分大小写)。
max_datasetsNo最多扫描多少个数据集(默认 200)。

Output Schema

NameRequiredDescription

No output parameters

TDQS

A4.2/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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 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)。

NameRequiredDescriptionDefault
pathYes数据集路径,相对 SAS_DATA_ROOT 或绝对路径。
columnsNo可选,只统计这些变量;不传则统计全部。
apply_labelsNo字符型 Top5 是否套用 SAS 值标签。

Output Schema

NameRequiredDescription

No output parameters

TDQS

A4.1/5.0
Behavior4/5

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.

Conciseness4/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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.

  1. 8 tool updatesv0.1.0
    • First observedfrequency
    • First observedget_dataset_metadata
    • First observedget_value_labels
    • First observedlist_datasets
    • First observedquery_dataset
    • First observedread_dataset
    • First observedsearch_variables
    • First observedsummarize_dataset

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
    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