关于 mini-app-mcp
mini-app-mcp 是面向需要结构化持久存储的 Agent 的 MCP 服务,以 schema.yaml 定义表结构,并通过 SQLite 在单个进程中管理多张表。它仅通过 MCP 提供 CRUD 与模式管理操作。
主要功能
- 01以 schema.yaml 定义表结构
- 02单个进程挂载多张表
- 03提供记录的 CRUD 操作
- 04管理表模式与查询别名
- 05支持批量操作和快照
- 06提供只读模式及文档资源
适用场景
- 01为 Agent 保存结构化项目数据
- 02按项目和用户范围管理多张表
- 03批量修改数据或表模式
- 04通过快照备份及恢复数据
MCP 相关
服务通过 MCP 工具提供数据 CRUD、模式管理、查询别名和快照操作,并提供 schema、错误码及工具文档等只读资源。支持 stdio 和 Streamable HTTP 传输。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
docker run -i --rm ghcr.io/ynishi/mini-app-mcp:0.20.0本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
ghcr.io/ynishi/mini-app-mcp:0.20.0stdio工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
mini-app-mcp
以 Agent 为先的 CRUD 存储 MCP 服务——由 schema.yaml 驱动,使用 SQLite 后端,可在单个守护进程中管理多张表。
功能
mini-app-mcp 是一个轻量级 MCP 服务,可在单个运行进程中管理一张或多张 SQLite 表。每张表的结构完全由 schema.yaml 文件定义;无需迁移、REST API 或 GUI。CRUD 功能仅通过 MCP 工具提供,因此它可以自然地作为需要结构化持久存储的 Agent 的后端。
设计原则
schema.yaml是唯一的结构定义依据——字段名称、类型和必填约束在启动时从 YAML 读取。应用代码中没有硬编码字段。- 单个守护进程管理多张表——单个服务进程会发现并挂载配置的 User 和 Project 作用域目录下的所有表。专用的旧版模式(
MINI_APP_SCHEMA+MINI_APP_DB)保留原有的单表行为。 - 仅通过 MCP 入口访问——不提供 HTTP/REST/CLI CRUD 接口。所有读写操作都通过 MCP 工具完成。
- 结构化 JSON 错误——每个错误响应都包含机器可读的
code字段,方便 Agent 以编程方式处理故障。
schema.yaml 格式
table: issues
title: "Issue tracker"
description: "Tracks bugs and feature requests for a project."
fields:
- name: title
type: string
required: true
description: "Short one-line summary of the issue."
- name: state
type: string
required: false
- name: tags
type: array
required: false
表级别的可选 title 和 description 键用于提供便于人类和 AI 理解的表元数据。每个字段条目也可以包含可选的 description 字符串。这三个键均遵循 OpenAPI 3.1 / JSON Schema 2020-12 命名约定,并包含在 info 工具输出和 schema://json 资源中。
支持的类型:string、number、boolean、array、object。
配置
多表模式(推荐)
| 环境变量 | 默认值 | 说明 |
|---|---|---|
MINI_APP_USER_DIR | ~/.mini-app/ | User 作用域表的基础目录。每个子目录都会被视为一个表名,且必须包含 schema.yaml 和 <table>.db。 |
MINI_APP_PROJECT_DIR | ./.mini-app/ | Project 作用域覆盖目录。此处存在的表会完全替代同名的 User 作用域定义。 |
MINI_APP_BACKUP_RETENTION | 10 | 每张表在 _backup/ 下保留的备份副本(YAML + DB 快照对)最大数量。每次写入备份后,超出此上限的较旧副本都会立即删除。 |
MINI_APP_SNAPSHOT_RETENTION | 10 | 每张表在 _snapshots/ 下保留的快照代数上限。此设置与 MINI_APP_BACKUP_RETENTION 严格分开;只清理 _snapshots/ 文件,不会触及 _backup/。 |
启动时会扫描这两个目录以发现表。同名表的 Project 作用域定义优先于 User 作用域定义。
旧版单表模式
| 环境变量 | 默认值 | 说明 |
|---|---|---|
MINI_APP_SCHEMA | ./schema.yaml | 结构定义文件的路径 |
MINI_APP_DB | (无——必须设置) | SQLite 数据库文件的路径 |
设置 MINI_APP_SCHEMA 和 MINI_APP_DB 后,服务会以旧版模式启动,并且只挂载一张表。在此模式下,所有工具都可以省略 table 参数。
所有变量也可以放在工作目录中的 .mini-app-mcp.env 文件里。
MCP 工具
所有工具都接受可选的 table 参数,用于选择目标表。在多表模式下,此参数为必填;省略时会返回错误代码 TABLE_REQUIRED。提供未知表名时会返回错误代码 TABLE_NOT_FOUND。在旧版单表模式下,此参数可以省略。
| 工具 | 描述 |
|---|---|
info | 以 JSON 格式返回解析后的架构(表名、字段定义) |
create | 插入新行;根据架构验证 data 对象 |
get | 根据 id 检索单行。如果 id 少于 36 个字符,则将其视为 UUID 前缀:无匹配项时返回 NOT_FOUND;有两个或更多匹配项时返回 AMBIGUOUS_ID,并附带候选列表。完整的 36 字符 UUID 始终使用精确匹配路径。接受可选的 fields 选择器,将返回的 data 对象投影为指定的架构字段子集。 |
list | 返回行,并支持可选的 limit / offset 分页。接受可选的 fields 选择器,将返回的 data 对象投影为指定的架构字段子集。 |
update | 根据 id 更新现有行。如果 id 少于 36 个字符,则将其视为 UUID 前缀(解析规则见 get)。默认模式为 merge(RFC 7396):缺失字段将从存储的行中保留,null 值会删除可选字段,或对必填字段引发 Validation 错误。传入 "mode": "replace" 可进行完整替换(0.9 之前的行为)。 |
delete | 根据 id 删除行。如果 id 少于 36 个字符,则将其视为 UUID 前缀(解析规则见 get)。 |
reload | 重新扫描 MINI_APP_USER_DIR / MINI_APP_PROJECT_DIR,并原子性地替换表注册表。如果已设置旧版 MINI_APP_SCHEMA + MINI_APP_DB,则会重新应用。返回 { mounted, added, removed }。限制:不监视文件(仅显式调用);替换整个注册表(不支持逐表部分重新加载);不会迁移现有行的架构;并发调用 reload 时,以最后完成的调用为准。 |
schema_create | 在指定的 scope(project 或 user)下创建新的 schema.yaml,并实时注册该表。传入 dry_run: true 可预览而不写入。如果表已存在,则返回 SCHEMA_EXISTS。 |
schema_update | 用新定义替换现有表的 schema.yaml(完整覆盖)。写入前,会将旧版 YAML 和 SQLite 快照备份到 _backup/。传入 dry_run: true 可预览字段变更而不触碰磁盘。 |
schema_delete | 删除表的 schema.yaml(移至 _backup/),并将其从实时注册表中注销。不会修改或删除 SQLite 表——DDL 变更仍由操作员负责。传入 dry_run: true 可预览。 |
schema_batch | 在单个 SQLite SAVEPOINT 下原子性地执行 ops[] 数组。任何操作失败都会回滚之前的所有操作,使 YAML 和 DB 保持不变。所有操作必须针对同一张表。返回每个操作的结果,或返回带有失败操作索引的 BATCH_ABORTED 错误。 |
data_snapshot | 使用 SQLite 热备份 API,为一个或所有已挂载的表创建某一时刻的 SQLite 快照。快照写入 <scope_root>/_snapshots/<table>.<unix_secs>.db。传入 table 和/或 scope 可限定目标范围;两者均省略时,将为所有已挂载的表创建快照。传入 dry_run: true 可预览操作(目标表、行数、将清除的数量),而不创建任何文件。保留策略由 MINI_APP_SNAPSHOT_RETENTION 控制(默认值为 10),与 MINI_APP_BACKUP_RETENTION 相互独立。 |
alias_create | 为表注册具名查询别名。接受 name、filter(ListFilter 表达式)或 filter_template(MiniJinja 模板字符串;不能与 filter 同时使用)、可选的 params_schema(用于模板别名的参数名称字符串数组)、可选的 default_limit,以及可选的 description。别名名称在每张表内必须唯一;名称重复时返回 ALIAS_ALREADY_EXISTS。别名按表划分作用域,并存储在表自己的 SQLite 数据库中。 |
alias_list | 将为表注册的所有别名作为 JSON 数组返回,每个元素为 { name, filter, default_limit, description, params_schema } 对象。 |
alias_run | 按名称执行已存储的别名。接受可选的运行时 limit 和 offset,可在调用时覆盖已存储的 default_limit。对于参数化别名(通过 filter_template 创建的别名),还接受 params 对象,其键值对会注入模板。接受可选的 fields 选择器,将返回的 data 对象投影为指定的架构字段子集。如果设置了 params_schema,但省略了 params,则返回 ALIAS_PARAMS_REQUIRED。模板渲染失败时返回 ALIAS_TEMPLATE_ERROR。返回与 list 工具相同的结构。名称未知时返回 ALIAS_NOT_FOUND。 |
alias_delete | 删除表的具名别名。如果别名不存在,则返回 ALIAS_NOT_FOUND。 |
MCP 资源
除了上述工具外,服务还通过 URI 提供 7 个只读 资源。资源面向希望获取架构定义或参考文档、而无需调用会修改数据的工具的 Agent。
| URI | MIME | 内容 |
|---|---|---|
schema://yaml | application/yaml | 原始 schema.yaml 文件内容(每次请求时从磁盘读取) |
schema://json | application/json | 解析后的 SchemaConfig,以 JSON 格式返回(结构与 info 工具返回的相同) |
schema://json-schema | application/schema+json | 根据架构字段生成的 JSON Schema(draft-07)。调用 create / update 前,可用它验证 data 参数 |
docs://quickstart | text/markdown | Agent 快速入门指南(模式检测 + 首次调用步骤 + 其他 docs:// 资源的指引),编译到二进制文件中。与面向人类读者的本 README 不同——请在 MCP 会话中读取此资源 |
docs://tools | text/markdown | 全部 17 个 MCP 工具及其输入结构的速查表 |
docs://errors | text/markdown | 服务返回的错误代码参考表 |
docs://filters | text/markdown | 用于 list 和 alias_create 的筛选器对象(Eq/In/Like/Or/And)构造指南 |
info 工具和 schema://json 资源返回的内容等效,但用途不同:info 是可调用的工具(适合在对话中进行一次性检查),而资源可通过 URI 寻址,客户端可以订阅或缓存它们。
用法
通过 --mcp(stdio 传输)或 --mcp-http(可流式传输的 HTTP 传输)启动服务。这是仅有的入口点;没有 REST/CLI-CRUD 接口。
多表模式
将每个表的 schema.yaml 和 <table>.db 放在 ~/.mini-app/<table>/(用户范围)或 ./.mini-app/<table>/(项目范围)下,然后无需设置其他环境变量即可启动:
mini-app-mcp --mcp
在 .mcp.json 中注册一次,即可提供所有已挂载的表:
{
"mcpServers": {
"mini-app": {
"command": "mini-app-mcp",
"args": ["--mcp"]
}
}
}
多设备模式(可流式传输的 HTTP)
运行一个管理 SQLite 文件的中央守护进程,然后从任意数量的设备连接为远程 MCP 客户端。由于所有设备都与同一个进程通信,因此可保留单写入者存储模型——不存在跨设备同步或冲突处理,也无需这些机制。
# Central host — loopback (same-machine clients, e.g. behind an SSH tunnel)
mini-app-mcp --mcp-http # default bind 127.0.0.1:8484
# Central host — LAN exposure requires a token (generate one once):
# openssl rand -hex 32
MINI_APP_HTTP_TOKEN=<random-string> mini-app-mcp --mcp-http --bind 0.0.0.0:8484
在 .mcp.json 中注册客户端(网络上的任意设备均可):
{
"mcpServers": {
"mini-app": {
"type": "http",
"url": "http://central-host:8484/mcp",
"headers": { "Authorization": "Bearer <random-string>" }
}
}
}
安全模型:
- 回环绑定(默认):
MINI_APP_HTTP_TOKEN为可选项。传输内置的Host标头验证(仅限回环地址)可防范 DNS 重绑定攻击。 - 非回环绑定:必须设置
MINI_APP_HTTP_TOKEN,否则将拒绝启动。此后,每个请求都必须携带Authorization: Bearer <token>。 - 传输加密(TLS)和更完善的授权功能不在设计范围内;如有需要,请在反向代理处终止 TLS。
中央主机的守护进程模板位于 contrib/ 下:
- Linux:
contrib/systemd/mini-app-mcp.service(systemd 用户单元;安装说明见文件头部) - macOS:
contrib/launchd/io.github.ynishi.mini-app-mcp.plist(launchd 用户代理;安装说明见文件头部)
如需改为在 Fly.io 上托管中央守护进程,而非运行在自己的机器上(持久化卷、边缘 TLS、Bearer 身份验证、每日备份 cron),请参阅 docs/runbooks/fly-io-deploy.md 和 contrib/fly/fly.toml。
旧版单表模式
MINI_APP_SCHEMA=./schema.yaml MINI_APP_DB=./issues.db mini-app-mcp --mcp
或者通过 .mini-app-mcp.env 配置:
MINI_APP_SCHEMA=./schema.yaml
MINI_APP_DB=./issues.db
导出 / 文件物化
mini-app-mcp 可以将每条新建或更新的记录写入磁盘上的 Markdown 文件。这对从文件中读取上下文的 Agent、使用 git 对记录进行版本控制,或快速人工检查都很有用。
工作原理
每次成功调用 create 或 update 后,服务都会写入(或覆盖)一个文件:
<dump-dir>/<id>.md
文件格式如下:
# <title-field value>
<body-field value>
默认情况下,delete 不会删除导出文件(记录会作为存档保留在磁盘上)。
在 schema.yaml 中启用 dump
在 schema.yaml 中添加 dump: 部分:
table: issues
fields:
- name: title
type: string
required: true
- name: body
type: string
required: false
dump:
dir: ./issues # optional; default: <cwd>/.mini-app/<table>/
title_field: title # optional; default: title
body_field: body # optional; default: body
sync: write-only # optional; default: write-only
| 键 | 默认值 | 描述 |
|---|---|---|
dump.dir | <cwd>/.mini-app/<table>/ | 写入 <id>.md 文件的目录。相对路径以服务的工作目录为基准解析。 |
dump.title_field | title | 存储的 JSON 行中用作 Markdown 标题的字段名。 |
dump.body_field | body | 存储的 JSON 行中用作 Markdown 正文的字段名。 |
dump.sync | write-only | 同步方向。目前仅实现了 write-only。设置为 bidirectional 时不会报错,但会记录警告并按 write-only 处理。 |
在 git 中忽略 dump 文件
如果不希望版本控制跟踪 dump 文件,请将 .mini-app/(或自定义的 dump.dir)添加到 .gitignore 中:
.mini-app/
Schema 管理
mini-app-mcp 提供四个工具,可在运行时管理表 schema,而无需重启服务。
创建表
schema_create(scope="project", table="notes", title="Notes", description="Personal notes.", fields=[...])
该工具会写入 <scope_dir>/notes/schema.yaml,并立即在实时注册表中注册新表。再次调用该工具创建同名表时,会返回 SCHEMA_EXISTS。可选参数 title 和 description 会写入 YAML 文件,并立即通过 info 或 schema://json 显示。单个字段条目也可以包含 description 字符串。
更新 schema
schema_update(scope="project", table="notes", title="Notes", description="Updated notes.", fields=[...])
覆盖前,服务会将现有的 schema.yaml 和某一时刻的 SQLite 快照备份到 <scope_dir>/_backup/notes.<timestamp>.yaml 和 <scope_dir>/_backup/notes.<timestamp>.db。写入后会刷新实时注册表。不会应用 DDL 迁移——底层表结构保持不变。
删除 schema
schema_delete(scope="project", table="notes")
schema.yaml 会移至 _backup/,该表也会从实时注册表中注销。SQLite 数据库文件不会被修改。 删除或更改表仍由操作员负责。
原子批量操作
schema_batch(ops=[
{ "op": "create", "table": "notes", "scope": "project", "fields": [...] },
{ "op": "data_insert", "table": "notes", "data": { "title": "first note" } }
])
所有操作都在单个 SQLite SAVEPOINT 下执行。如果任一操作失败,整个批次都会回滚——不会写入 YAML 文件,也不会更改注册表。同一批次中的所有操作都必须针对同一张表。
dry_run 预览
所有四个 schema 工具都接受 dry_run: true。在 dry-run 模式下,工具会计算并返回影响数量(rows、fields_added、fields_removed),但不会写入任何 YAML 文件、SQLite 表或备份目录。
备份保留
备份文件会累积在 <scope_dir>/_backup/ 中。服务会自动删除超出保留上限的最旧副本(默认每张表保留 10 对)。可通过 MINI_APP_BACKUP_RETENTION 覆盖此上限。
数据快照
data_snapshot 会创建一个或多个已挂载表的独立 SQLite 快照,不会触及任何 YAML 文件或更改 schema:
data_snapshot(table="notes", scope="project")
data_snapshot() # snapshot all mounted tables
data_snapshot(dry_run=true) # preview without writing
快照会使用 SQLite 热备份 API(rusqlite::Connection::backup)写入 <scope_root>/_snapshots/<table>.<unix_secs>.db,因此操作期间源数据库仍保持打开且可写。保留上限(默认 10)通过 MINI_APP_SNAPSHOT_RETENTION 独立控制,且绝不会与 _backup/ 交互。
快照上传(兼容 S3,选择启用)
启用 s3-upload 构建功能后,data_snapshot 可将每个已写入的快照推送到任意 S3 协议目标位置——AWS S3、Backblaze B2(兼容 S3 的 API)、Cloudflare R2、MinIO——仅通过端点选择:
cargo install mini-app-mcp --features s3-upload
data_snapshot(upload=true) # snapshot all tables, then upload each
data_snapshot(table="notes", upload=true)
配置仅通过环境变量设置(从 .mini-app-mcp.env 读取);凭据绝不会通过工具调用参数传递:
| 环境变量 | 必需 | 说明 |
|---|---|---|
MINI_APP_S3_ENDPOINT | 是 | 兼容 S3 的端点 URL。B2:https://s3.<region>.backblazeb2.com |
MINI_APP_S3_BUCKET | 是 | 存储桶名称 |
MINI_APP_S3_ACCESS_KEY_ID | 是 | 访问密钥 ID(B2:应用密钥 ID) |
MINI_APP_S3_SECRET_ACCESS_KEY | 是 | 访问密钥(B2:应用密钥) |
MINI_APP_S3_PREFIX | 否 | 对象键前缀(默认值为 mini-app-snapshots/) |
MINI_APP_S3_REGION | 否 | 签名区域。未设置时,会从 s3.<region>.<domain> 端点自动推导(B2 / AWS 区域端点),因此 B2 用户可以省略;不包含区域信息的主机(MinIO、R2)会回退到虚拟值 us-east-1。显式设置的值始终优先——B2 会拒绝不匹配的区域,因此仅当其与端点匹配时才设置 |
MINI_APP_S3_VIRTUAL_HOSTED_STYLE | 否 | true = 虚拟主机寻址(bucket.endpoint/key);默认值 false = 路径样式(endpoint/bucket/key),MinIO 要求使用此样式,AWS / B2 / R2 则支持此样式 |
MINI_APP_S3_CHECKSUM | 否 | sha256 = 上传时发送 x-amz-checksum-sha256;默认值为 none,因为某些兼容 S3 的提供商会以 400 InvalidArgument: Unsupported header 拒绝校验和标头 |
语义:
- 在写入任何快照之前验证配置——未启用此功能构建的二进制文件,或环境变量不完整时,会立即返回
UPLOAD_NOT_CONFIGURED。 - 单个上传失败不会导致操作失败:本地快照和清理结果仍然有效,失败情况会按表格列在响应字段
upload_errors[]中。成功情况会以{table, key, bytes}的形式出现在uploaded[]中。 - 远程保留策略不在支持范围内(已知限制):服务端永远不会删除远程对象。请配置存储桶生命周期规则,以过期方式清除旧版本。
- 兼容性说明:上传使用单次
PutObject请求(不使用分段上传),并且除非通过MINI_APP_S3_CHECKSUM=sha256选择启用,否则不会发送任何x-amz-checksum-*标头——会导致某些兼容 S3 的提供商出问题的两个 aws-sdk 端陷阱默认都不会触发。
不启用此功能的替代方案:保持服务端不变,并通过外部工具发送快照,例如添加 cron 条目 mini-app snapshot → rclone copy ~/.mini-app/_snapshots b2:my-bucket/mini-app-snapshots,或运行 restic backup ~/.mini-app 任务——效果相同,且凭据由外部工具持有。
定时备份
contrib/backup/mini-app-backup.sh 可将上传操作变成 cron 任务:它会在运行中的 --mcp-http 守护进程上调用 data_snapshot(upload=true)(通过可流式传输的 HTTP 执行 initialize → tools/call;依赖:curl、jq),并在调用失败或 upload_errors[] 非空时以非零状态码退出,以便在 cron 邮件 / journal / supercronic 日志中显示失败情况。针对本地守护进程:
# daily 03:00 — MINI_APP_URL / MINI_APP_HTTP_TOKEN via env if non-default
0 3 * * * /path/to/contrib/backup/mini-app-backup.sh
在容器镜像中,该脚本与 supercronic 一同打包为 mini-app-backup;设置 BACKUP_CRON(例如 0 3 * * *)后,入口点会安排脚本与服务端一同运行(参见 contrib/docker/entrypoint.sh)。
关系图用法
mini-app-mcp 与表结构无关,但一种常见模式是将单个表用作关系图层,为需要节点间带类型边的 Agent 提供支持(例如,persona-AI 记忆:sister_of、member_of_studio、mother_of)。
参考架构位于 examples/schemas/relations.schema.yaml:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
from | string | 是 | 源节点 ID |
to | string | 是 | 目标节点 ID |
type | string | 是 | 边标签(sister_of、member_of_studio……) |
ts | number | 是 | 断言时的 Unix 时间戳(秒) |
strength | number | 否 | 边权重,范围为 [0.0, 1.0],默认为 1.0 |
metadata | object | 否 | 自由格式属性 |
source_entry_id | string | 否 | 指向日志 / 记录条目的外部引用 |
批量插入(原子操作插入 N 条边)
使用 schema_batch 搭配 query 操作,在一个 SAVEPOINT 中注册 N 条边:
{
"ops": [
{
"op": "query",
"sql": "INSERT INTO rows (id, data, created_at, updated_at) VALUES (?1, ?2, ?3, ?4)",
"params": ["<uuid>", "{\"from\":\"persona-x\",\"to\":\"a\",\"type\":\"sister_of\",\"ts\":1779330000,\"strength\":1.0}", "1779330000", "1779330000"]
}
]
}
任何操作失败都会回滚整个批次。
批量替换(完整替换边集合)
使用专用的 replace 操作,在一个原子 SAVEPOINT 中替换边集合。服务端负责生成 UUID、时间戳和序列化 JSON;调用方只需提供 match 范围和 items 列表。
{
"ops": [
{
"op": "replace",
"table": "relations",
"match": {"from": "persona-x", "type": "sister_of"},
"items": [
{"from": "persona-x", "to": "a", "type": "sister_of", "ts": 1779330000, "strength": 1.0},
{"from": "persona-x", "to": "b", "type": "sister_of", "ts": 1779330000, "strength": 1.0},
{"from": "persona-x", "to": "c", "type": "sister_of", "ts": 1779330000, "strength": 0.8}
]
}
]
}
replace 操作会在单个 SAVEPOINT 中执行 DELETE WHERE(match)和 N 次 INSERT。任一阶段发生失败,两个阶段都会一并回滚。响应中包含每个操作的影响行数:
{
"committed": true,
"ops_executed": 1,
"affects": {"deleted": 5, "inserted": 3}
}
语义:
match中的每个键都会转换为一个独立的json_extract(data, '$.key') = value谓词,并以 AND 连接。匹配值必须是标量(字符串 / 数字 / 布尔值 / null)。- 空
match({})会被拒绝并返回 VALIDATION_ERROR,以防意外清空整张表。 items中的每一项都会在执行任何 SQL 之前根据表架构进行验证。query操作(原始 SQL 逃生口,见批量插入)仍可用于replace操作无法满足的场景。
列出边(范围少于 1000 条)
list 工具按 created_at DESC 顺序返回行,limit 上限为 1000。可选的 filter 参数支持服务端根据经过架构验证的字段筛选行。
筛选条件类型
| type | 说明 |
|---|---|
eq | 相等匹配:{"type": "eq", "field": "...", "value": ...} |
in | 集合成员匹配:{"type": "in", "field": "...", "values": [...]} |
like | 对字符串字段进行部分匹配:{"type": "like", "field": "...", "pattern": "..."}。% 匹配任意子字符串;_ 匹配任意单个字符。仅限 string 类型字段。 |
or | OR 组合:{"type": "or", "filters": [...]} |
and | AND 组合:{"type": "and", "filters": [...]} |
or 和 and 接受递归的 Vec<ListFilter>,可构造任意深度的嵌套组合。
约束:字段名必须在表的 schema.yaml 中注册(未知字段会返回 VALIDATION_ERROR)。值会根据架构中字段声明的类型进行类型检查,与针对 BatchOp::Replace 匹配值执行的类型化标量验证相同。不传入 filter 时,会在 limit / offset 范围内返回所有行(完全向后兼容)。
邮箱式 OR 筛选示例
单次调用即可获取发送给 "alice" 或 "broadcast" 的消息:
{
"filter": {
"type": "or",
"filters": [
{"type": "eq", "field": "to", "value": "alice"},
{"type": "eq", "field": "to", "value": "broadcast"}
]
}
}
对于适合在客户端进行筛选的小型图:
edges = mini_app.list(table="relations", limit=1000)
sisters_of_x = [
e for e in edges
if e["data"]["from"] == "persona-x" and e["data"]["type"] == "sister_of"
]
更新语义:合并与替换
update 工具支持两种模式,由可选的 "mode" 字段控制。
合并模式(默认)
update(table="notes", id="<uuid>", data={"state": "done"})
// or equivalently:
update(table="notes", id="<uuid>", data={"state": "done"}, mode="merge")
合并模式遵循 RFC 7396 (JSON Merge Patch) 的浅层语义:
- 缺失字段会保留 —
data中未出现的键会保留其已存储的值。 - 非 null 值会覆盖 —
data中存在且值非 null 的键会替换对应键的已存储值。嵌套对象会整体替换(不进行深度合并)。 null会删除可选字段 — 如果键的值为null,且架构中该字段为required: false,则会从已存储的行中移除该字段。- 必填字段为
null时会产生验证错误 — 如果键的值为null,且该字段为required: true,调用会在进行任何写入前以VALIDATION_ERROR失败。 - 合并后的结果会进行完整的架构验证 — 合并后,会根据架构验证完整的合并对象。任何约束违规(缺少必填字段、类型不匹配)都会返回验证错误,且不会更新该行。
替换模式
update(table="notes", id="<uuid>", data={...}, mode="replace")
替换模式会执行完整替换:根据架构验证 data,然后将其原样写入,完全覆盖已存储的行。这与 0.9 版本之前 update 的行为相同。依赖旧默认行为并发送不完整 data 对象的调用方,应切换到 mode="replace" 以恢复原有行为。
选择模式
| 模式 | 适用场景 |
|---|---|
merge(默认) | 部分更新 — 只发送要更改的字段。省略的现有字段不会受到影响。 |
replace | 完整重写 — 提供该行预期完整状态。省略的字段会被删除。 |
字段投影
list、get 和 alias_run 均接受可选的 fields 参数,用于限制返回的 data 对象中包含哪些架构字段。无论选择器如何设置,响应封装中始终包含 id、created_at 和 updated_at。
选择器形式
{"mode": "all"} // default: return all schema fields
{"mode": "list", "fields": ["title", "state"]} // return only the named fields
省略 fields 完全向后兼容 — 现有调用方仍会收到完整行,行为不会改变。
使用示例
list(table="issues", fields={"mode": "list", "fields": ["title", "state"]})
get(table="issues", id="<uuid>", fields={"mode": "list", "fields": ["title"]})
alias_run(table="issues", name="recent_open", fields={"mode": "list", "fields": ["title", "state"]})
验证
未知字段名会在执行任何查询前以 VALIDATION_ERROR 拒绝。字段名验证会查询 schema.yaml 中的规范字段定义,而非已存储行中实际存在的键,因此即使字段仅在某一行中缺失,也能可靠地捕获投影错误。
查询别名
查询别名可将 ListFilter 表达式(或 MiniJinja 筛选模板)保存为短名称并重放 — 还可按调用覆盖 limit / offset — 无需每次重复传入筛选 JSON。
创建静态别名
alias_create(
table="notes",
name="recent_open",
filter={"type": "eq", "field": "state", "value": "open"},
default_limit=20,
description="Open notes, newest first"
)
别名存储在表自身的 SQLite 数据库 _aliases 中。别名名称在每张表内唯一;再次使用相同名称调用 alias_create 会返回 ALIAS_ALREADY_EXISTS。
创建参数化别名(筛选模板)
参数化别名使用 MiniJinja 模板字符串,而非固定筛选条件。运行时,调用方提供参数值,这些值会被渲染到模板中,然后执行。
alias_create(
table="issues",
name="by_state",
filter_template='{"type": "eq", "field": "state", "value": "{{ state }}"}',
params_schema=["state"],
default_limit=50,
description="Filter issues by state (parameterized)"
)
filter_template 和 filter 互斥 — 必须且只能提供其中一个。params_schema 是可选的参数名称数组;提供它可说明 alias_run 在其 params 对象中需要哪些键。
运行别名
静态别名(无参数):
alias_run(table="notes", name="recent_open")
# → uses stored default_limit=20
alias_run(table="notes", name="recent_open", limit=5)
# → runtime limit overrides the stored default_limit
alias_run(table="notes", name="recent_open", limit=10, offset=20)
# → pagination with runtime overrides
带参数的别名(带有 params):
alias_run(table="issues", name="by_state", params={"state": "open"})
# → renders template → {"type": "eq", "field": "state", "value": "open"}
# → validates as ListFilter → executes Store::list
alias_run(table="issues", name="by_state", params={"state": "open"}, limit=10)
# → runtime limit override with params injection
alias_run 会解析已存储的筛选条件,或渲染模板,并将结果传递给 Store::list。如果调用时提供了 limit,则会覆盖 default_limit;如果未提供,则使用别名中的 default_limit。offset 始终是仅在运行时使用的参数(不会存储)。
带参数别名的错误情况:
| 情况 | 错误代码 |
|---|---|
设置了 params_schema,但未提供 params | ALIAS_PARAMS_REQUIRED |
| MiniJinja 渲染失败(语法错误或缺少变量) | ALIAS_TEMPLATE_ERROR |
渲染结果不是有效 JSON 或不是有效的 ListFilter | VALIDATION_ERROR |
列出和删除别名
alias_list(table="notes")
# → [{"name": "recent_open", "filter": {...}, "default_limit": 20, "description": "...", "params_schema": null}]
alias_list(table="issues")
# → [{"name": "by_state", "filter": null, "default_limit": 50, "description": "...", "params_schema": ["state"]}]
alias_delete(table="notes", name="recent_open")
别名隔离
每个表的 _aliases 存储在物理上相互独立:notes 的别名存储在 notes.db 中,issues 的别名存储在 issues.db 中。不存在全局别名命名空间,也无法通过别名操作读取或写入属于其他表的别名。
聚合
query_aggregate 可在一次工具调用中,对一个或多个表执行 COUNT / SUM / AVG / MIN / MAX / GROUP BY。多表数据源通过 SQLite ATTACH DATABASE 与字面量 UNION ALL 合并,从而消除调用方逐表获取行并在客户端侧汇总时产生的 N+1 次往返。该工具为只读且幂等。
结构
{
"sources": { "kind": "single", "value": "<table>" }, // or "multi" + ["t1","t2",...]
"filter": null, // optional, same ListFilter shape as `list`
"aggregator": { "kind": "count" } // or sum / avg / min / max / group_by
}
结果采用外部标记格式,方便调用方根据 kind 进行分派:
{ "kind": "count", "value": 42 }
{ "kind": "value", "value": 3.14 }
{ "kind": "groups", "value": [ { "key": "a", "count": 12, "value": 7.0 }, ... ] }
单数据源聚合
// COUNT all rows in `emo`.
{ "name": "query_aggregate", "arguments": {
"sources": { "kind": "single", "value": "emo" },
"aggregator": { "kind": "count" }
}}
// SUM the `amount` field.
{ "name": "query_aggregate", "arguments": {
"sources": { "kind": "single", "value": "ledger" },
"aggregator": { "kind": "sum", "field": "amount" }
}}
多表聚合
Multi 会通过 SQLite ATTACH DATABASE 挂载每个后端 .db 文件(上限为 10 个),并在应用外层聚合前,将各表的子查询通过 UNION ALL 组合起来。
// COUNT rows across two same-shape tables.
{ "name": "query_aggregate", "arguments": {
"sources": { "kind": "multi", "value": ["events_a", "events_b"] },
"aggregator": { "kind": "count" }
}}
// GROUP BY tag with HAVING and an inner SUM(amount).
{ "name": "query_aggregate", "arguments": {
"sources": { "kind": "multi", "value": ["events_a", "events_b"] },
"aggregator": {
"kind": "group_by",
"by_field": "tag",
"having": { "type": "eq", "field": "tag", "value": "a" },
"inner": { "kind": "sum", "field": "amount" }
}
}}
调用方负责确保所有 Multi 数据源的架构兼容;逐表字段验证属于 Phase 2 的后续工作。聚合器特有的错误(数据源为空、超出 ATTACH 上限、嵌套 GroupBy、非 UTF-8 数据库路径)会返回 AGGREGATOR_ERROR(data.code);未知表名会返回 TABLE_NOT_FOUND;字段或标识符被拒绝会返回 VALIDATION_ERROR。
全局别名(Phase 2)
Phase 2 会将各表的别名统一存储在一个全局存储中
(<project_dir>/_global.db + <user_dir>/_global.db,查找
优先级为 Project → User)。alias_create MCP 工具接受新增的可选参数
sources 和 aggregator,因此一个别名可以跨多个表,并保存聚合逻辑:
// Multi-table alias with COUNT aggregator (replays via execute_aggregate).
{ "name": "alias_create", "arguments": {
"name": "combined_events_count",
"sources": { "kind": "multi", "value": ["events_a", "events_b"] },
"aggregator": { "kind": "count" },
"filter": { "type": "like", "field": "tag", "pattern": "%" }
}}
// Pattern source — resolved against the live registry's table list at
// alias_run time. Matches all currently-mounted `events_*` tables.
{ "name": "alias_create", "arguments": {
"name": "all_events_count",
"sources": { "kind": "pattern", "value": "events_*" },
"aggregator": { "kind": "count" },
"filter": { "type": "like", "field": "tag", "pattern": "%" }
}}
// alias_run dispatches to execute_aggregate transparently.
{ "name": "alias_run", "arguments": { "name": "all_events_count" } }
// → { "kind": "count", "value": <sum across all events_* tables> }
仍然接受旧版 table 参数,并会静默将其规范化为
sources = { "kind": "single", "value": "<table>" };
同时指定 table 和 sources 会报错。
迁移:每次调用 TableRegistry::mount_from_dirs 时,都会自动将现有的逐表 _aliases 行复制到项目作用域的 _global_aliases 中。迁移不会丢失数据
(保留全部五个旧字段,并填入 sources = Single(<table>)),且具有幂等性
(INSERT OR IGNORE 会跳过冲突项),因此每次服务器重启时运行都是安全的。逐表 _aliases 表不会被删除,以保留回滚路径,直到未来的次要版本发布。
Phase 2 的限制
Multi/Pattern数据源别名需要提供aggregator;不带聚合器运行 会返回结构化错误(逐表list路径无法提供跨表行)。- Pattern glob 仅支持
*(一个或多个);?/[]为未来版本保留。 - Pattern 匹配结果为零个表时,会在
alias_run时返回AGGREGATOR_ERROR(提前暴露错误——不会延迟到ATTACH DATABASE时)。 - 筛选条件验证仅使用第一个数据源表的架构;
跨表架构差异由调用方负责处理(与
query_aggregate的约束相同)。 - 旧版单表模式(未设置
MINI_APP_USER_DIR/MINI_APP_PROJECT_DIR) 会回退到逐表Store::alias_*路径;此模式下不接受Pattern数据源。
行历史自动日志
每次 create、update 和 delete 调用都会自动将受影响行的完整 JSON 快照写入同一 SQLite 数据库中的 _row_history 表。快照会与数据变更在同一事务中原子写入——数据 DML 和历史记录要么一起提交,要么都不提交。
记录内容
| 事件 | op 值 | data_json |
|---|---|---|
create | "create" | 新行 JSON |
update | "update" | 合并后的结果 JSON |
delete | "delete" | 已删除行的 JSON |
每条历史记录还包含一个单调递增的 version 计数器(按 table + row id 分别计数)以及一个 Unix 秒级时间戳 recorded_at。
任何条目的操作前状态都可以通过前一条记录的 data_json 获取,因此无需再次存储(由旧版本写入的数据库中,旧版 prev_data_json 列仍可读取,但新条目会将其留为 null——这会使历史记录写入量减半)。
两层存储:原始记录 + 压缩归档
历史记录永不丢弃,但也不能占满磁盘(一个 178KB 的行更新 949 次,过去会留下 222MB 的完整副本历史记录)。原始 _row_history 表只保留每行最新的记录;较早的记录会在同一写事务中自动压缩到 _row_history_archive 中的 zstd 压缩 JSONL 块。近似重复的 JSON 版本压缩后体积可缩小约三个数量级,因此长历史记录仍然占用很少空间,同时可以完整恢复:row_restore 和版本列表会透明地读取归档。
| 环境变量 | 默认值 | 含义 |
|---|---|---|
MINI_APP_HISTORY_KEEP_RECENT | 16 | 每行保留的原始(未压缩)记录数——用于快速撤销的热数据集 |
MINI_APP_HISTORY_CHUNK_MIN | 48 | 合并到一个归档块中的最少记录数(超过 KEEP_RECENT + CHUNK_MIN 后触发归档) |
按表选择退出(history: off)
自动化任务高频写入的表(如心跳时间戳、日志缓存类行)可以在 schema.yaml 中完全禁用历史记录:
table: tick_stamps
history: off # default: full
fields:
- name: label
type: string
required: true
设置 history: off 后,对该表进行任何变更都不会写入 _row_history 记录(row_restore 也没有可供恢复的内容——仅在逐次写入历史没有恢复价值时使用)。
使用 row_restore 恢复行
row_restore 会获取指定 Unix 时间戳之前或当时的最新快照,并将其写回为当前行。如果该行已删除,则会使用相同的 id 重新插入;如果该行仍然存在,则会用快照数据替换。
{
"name": "row_restore",
"arguments": {
"table": "issues",
"id": "550e8400-e29b-41d4-a716-446655440000",
"at_unix_secs": 1750000000
}
}
响应格式与 get 相同——包含 id、data、created_at 和 updated_at 的完整行对象。
清理 SOP
历史记录不会在每次更新时清理。清理是单独的操作,需要在应用代码中调用 purge_old_history,并指定所需的保留策略:
- 保留期限:30 天(删除超过 30 天的记录)
- 每行最大版本数:100(逐行删除超出 100 个的最旧版本)
清理不会接入任何自动钩子,以避免历史记录累积影响正常 create / update / delete 调用的延迟。
局部编辑工具
有三个工具可让 Agent 查看并精确编辑单个字符串字段,而无需获取或替换整行。非字符串字段(数组、对象、数字、布尔值)会被拒绝,并返回 TYPE_ERROR。
content_view
返回字符串字段的内容,并标注行号(cat -n 样式)。传入 view_range 可将输出限制为连续片段(从 1 开始计数,包含两端)。
// View lines 12–30 of a body field
content_view(table="issue", id="26a8a7f9", field="body", view_range=[12, 30])
// → {"content": " 12 first visible line\n 13 ...\n"}
// Omit view_range to get the full field
content_view(table="issue", id="26a8a7f9", field="body")
content_replace
在字符串字段中,将 old_str 的唯一匹配项替换为 new_str。
// Unique replacement — returns the number of substitutions
content_replace(table="issue", id="26a8a7f9", field="body",
old_str="foo", new_str="bar")
// → {"matches": 1}
// Scope the search to a line range before replacing
content_replace(table="issue", id="26a8a7f9", field="body",
old_str="foo", new_str="bar", view_range=[1, 10])
// Replace every occurrence in one call
content_replace(table="issue", id="26a8a7f9", field="body",
old_str="foo", new_str="bar", replace_all=true)
// → {"matches": 4}
如果未找到 old_str,工具会返回 STRING_NOT_FOUND。如果它出现多次且未设置 replace_all,工具会返回 AMBIGUOUS,并附带 candidates 列表。
content_insert
在指定位置向字符串字段中插入一行或多行。
// Prepend (line=0 inserts before the first line)
content_insert(table="issue", id="26a8a7f9", field="body",
line=0, content="Prepended line")
// → {"line": 0, "inserted_lines": 1}
// Insert after line N (1-indexed)
content_insert(table="issue", id="26a8a7f9", field="body",
line=5, content="Inserted after line 5")
// → {"line": 5, "inserted_lines": 1}
line 值大于 total_lines + 1 时会返回 OUT_OF_RANGE。这三个工具都会通过常规的 row_history 钩子记录更改,因此每次编辑都可完整审计,并可通过 row_restore 还原。
存储说明
SQLite 数据库以 WAL 日志模式打开,以便在 reload 期间安全地并发访问。每个 .db 文件旁边都会创建边车文件 <db>.db-wal 和 <db>.db-shm——这些文件由 SQLite 管理,不应手动删除。
MCP Registry
mini-app-mcp 已列入 MCP Registry。每次发布带标签的版本时,都会将 OCI 镜像发布到 GitHub Container Registry。
Docker(无需 Rust 工具链)
{
"mcpServers": {
"mini-app": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-v", "/path/to/data:/data",
"ghcr.io/ynishi/mini-app-mcp:latest"
]
}
}
}
许可证
MIT OR Apache-2.0
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。